Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/api/requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ async def create_item(request: Request):

### Reading Form Data

<!-- blacken-docs:off -->
```python
import air
from air.requests import Request
Expand All @@ -76,6 +77,7 @@ async def login(request: Request):
air.Section(air.Aside({"username": form.get("username")}))
)
```
<!-- blacken-docs:on -->

### Accessing the HTMX object

Expand Down
10 changes: 10 additions & 0 deletions docs/api/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ If you need to knit several Python modules with their own Air views into one, th

Let's imagine we have an e-commerce store with a shopping cart app. Use instantiate a `router` object using `air.AirRouter()` just as we would with `air.App()`:

<!-- blacken-docs:off -->
```python
# cart.py
import air
Expand All @@ -15,9 +16,11 @@ router = air.AirRouter()
def cart():
return air.H1("I am a shopping cart")
```
<!-- blacken-docs:on -->

Then in our main page we can load that and tie it into our main `app`.

<!-- blacken-docs:off -->
```python
import air
from cart import router as cart_router
Expand All @@ -30,11 +33,13 @@ app.include_router(cart_router)
def index():
return air.H1("Home page")
```
<!-- blacken-docs:on -->

Note that the router allows sharing of sessions and other application states.

In addition, we can add links through the `.url()` method available on route functions, which generates URLs programmatically:

<!-- blacken-docs:off -->
```python
import air
from cart import router as cart_router, cart
Expand All @@ -47,11 +52,13 @@ app.include_router(cart_router)
def index():
return air.Div(air.H1("Home page"), air.A("View cart", href=cart.url()))
```
<!-- blacken-docs:on -->

## Query Parameters

Air supports query parameters through FastAPI's `Query()` validator, which you can import as `air.Query()`:

<!-- blacken-docs:off -->
```python
import air

Expand All @@ -70,9 +77,11 @@ def index():
air.A("Search", href=search.url(query_params={"q": "air", "page": 1}))
)
```
<!-- blacken-docs:on -->

The `.url()` method accepts a `query_params` argument for generating URLs with query strings. This works with both scalar values and lists:

<!-- blacken-docs:off -->
```python
@app.get("/filter")
def filter_items(
Expand All @@ -84,6 +93,7 @@ def filter_items(
# Generates: /filter?tags=python&tags=web
url = filter_items.url(query_params={"tags": ["python", "web"]})
```
<!-- blacken-docs:on -->

---

Expand Down
12 changes: 8 additions & 4 deletions docs/learn/airmodel.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,12 @@ Insert, update, or delete hundreds of rows in a single SQL statement:

```python
# Insert multiple rows at once
fruits = await DragonFruit.bulk_create([
{"name": "Pink Pitaya", "color": "magenta"},
{"name": "Yellow Dragon", "color": "yellow"},
])
fruits = await DragonFruit.bulk_create(
[
{"name": "Pink Pitaya", "color": "magenta"},
{"name": "Yellow Dragon", "color": "yellow"},
]
)

# Update all matching rows
count = await DragonFruit.bulk_update({"color": "red"}, name__contains="Dragon")
Expand Down Expand Up @@ -274,6 +276,7 @@ async def submit_contact(request: air.Request):

## A complete app in 30 lines

<!-- blacken-docs:off -->
```python title="main.py"
import air
from air import AirForm
Expand Down Expand Up @@ -318,6 +321,7 @@ async def sign(request: air.Request):
await GuestBookEntry.create(name=form.data.name, message=form.data.message)
return air.RedirectResponse("/")
```
<!-- blacken-docs:on -->

Model, form, database, HTML, validation, pagination, and two routes. Set `DATABASE_URL` and run it.

Expand Down
10 changes: 10 additions & 0 deletions docs/learn/cookbook/bigger-applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ When building larger applications with Air, you may find yourself needing to org

Let's imagine we have a landing page that links to a sophisticated dashboard. While our example dashboard is trivial, let's assume it is complicated enough that we want it in a separate Python module yet share state. We design the `main.py` as we would a normal Air application:

<!-- blacken-docs:off -->
```python title="main.py"
import air

Expand All @@ -20,9 +21,11 @@ app = air.Air()
def index():
return air.layouts.mvpcss(air.H1("Avatar Data"), air.P(air.A("Dashboard", href="/dashboard")))
```
<!-- blacken-docs:on -->

Now for the dashboard, instead of using the typical `air.Air` tool to instantiate our application, we use `air.AirRouter` like so:

<!-- blacken-docs:off -->
```python title="dashboard.py" hl_lines="3"
import air

Expand All @@ -33,9 +36,11 @@ router = air.AirRouter()
def dashboard():
return air.layouts.mvpcss(air.H1("Avatar Data Dashboard"), air.P(air.A("<- Home", href="/")))
```
<!-- blacken-docs:on -->

Now if we go back to our `main.py` we can use the `app.include_router()` method to include the dashboard in our app:

<!-- blacken-docs:off -->
```python title="main.py" hl_lines="2 5"
import air
from .dashboard import router
Expand All @@ -50,6 +55,7 @@ def index():
air.H1("Avatar Data"), air.P(air.A("Dashboard", href="/dashboard"))
)
```
<!-- blacken-docs:on -->

If run locally these links should work:

Expand All @@ -64,6 +70,7 @@ If run locally these links should work:

One of the really nice features of Air is the ability to mount apps inside each other. This allows you to create modular applications where different parts of your app can be developed and maintained independently. To do this, we lean on Starlette's `mount` functionality that Air inherits through FastAPI.

<!-- blacken-docs:off -->
```python
import air

Expand All @@ -90,11 +97,13 @@ def index():
# This allows you to access the shop at /shop
app.mount("/shop", shop)
```
<!-- blacken-docs:on -->

## Mounting FastAPI inside of Air apps

You can easily mount a FastAPI app inside an Air app. A common scenario is to have a FastAPI app that serves an API, while your main Air app serves the landing, billing, and usage frontends.

<!-- blacken-docs:off -->
```python
from fastapi import FastAPI

Expand Down Expand Up @@ -126,6 +135,7 @@ def api_root():
# Combining the Air and and FastAPI apps into one
app.mount("/api", api)
```
<!-- blacken-docs:on -->

## Mounting FastAPI apps inside each other

Expand Down
40 changes: 23 additions & 17 deletions docs/learn/cookbook/charts.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Air is great for building charts. Using [plotly](https://plotly.com/javascript/), here's a simple chart example.

<!-- blacken-docs:off -->
```python
import json

Expand All @@ -13,23 +14,25 @@ app = air.Air()
@app.get("/")
def index():
title = "Air Chart Demo"
data = json.dumps({
"data": [
{
"x": [0, 4, 5, 7, 8, 10],
"y": [2, 9, 0, 4, 3, 6],
"type": "scatter",
},
{
"x": [0, 1, 2, 4, 8, 10],
"y": [9, 2, 4, 3, 5, 0],
"type": "scatter",
},
],
"title": "Fun charts with Plotly and Air",
"description": ("This is a demonstration of how to build a chart using Plotly and Air"),
"type": "scatter",
})
data = json.dumps(
{
"data": [
{
"x": [0, 4, 5, 7, 8, 10],
"y": [2, 9, 0, 4, 3, 6],
"type": "scatter",
},
{
"x": [0, 1, 2, 4, 8, 10],
"y": [9, 2, 4, 3, 5, 0],
"type": "scatter",
},
],
"title": "Fun charts with Plotly and Air",
"description": ("This is a demonstration of how to build a chart using Plotly and Air"),
"type": "scatter",
}
)
return air.layouts.mvpcss(
air.Script(src="https://cdn.plot.ly/plotly-3.0.1.min.js"),
air.Title(title),
Expand All @@ -46,9 +49,11 @@ def index():
),
)
```
<!-- blacken-docs:on -->

Air makes it possible to build charts that pull data from servers and animate the results. Here's an example being supplied with random numbers for the Air server.

<!-- blacken-docs:off -->
```python
import air

Expand All @@ -69,3 +74,4 @@ air.Children(
),
)
```
<!-- blacken-docs:on -->
2 changes: 2 additions & 0 deletions docs/learn/cookbook/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Built on Pydantic's `BaseModel`, the `AirForm` class validates data from HTML forms with type-safe access to the validated result.

<!-- blacken-docs:off -->
```python
from airmodel import AirModel

Expand Down Expand Up @@ -54,6 +55,7 @@ async def flight_info(request: air.Request):
),
)
```
<!-- blacken-docs:on -->

## Enhanced Form Features

Expand Down
4 changes: 4 additions & 0 deletions docs/learn/def_vs_async_def.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ def user_detail(user_id: int):
user = db.get_user(user_id)
return air.H1(user.name)


@app.get("/about")
def about():
return air.H1("About")


@app.get("/data")
def get_data():
content = open("data.csv").read()
Expand All @@ -41,6 +43,7 @@ async def handle_form(request: air.Request):
return air.H1(f"Hello, {form.data.name}!")
return form.render()


@app.get("/external")
async def fetch_data():
async with httpx.AsyncClient() as client:
Expand All @@ -57,6 +60,7 @@ async def fetch_data():
async def get_data():
return open("big_file.csv").read()


# RIGHT: runs in a thread, other requests keep flowing
@app.get("/data")
def get_data():
Expand Down
17 changes: 9 additions & 8 deletions docs/learn/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,9 @@ After validation, `form.data` is the model instance with full type information:

```python
if form.is_valid:
form.data.route_name # your editor knows this is a str
form.data.destination # autocomplete works
form.data.orign # typo caught by the type checker
form.data.route_name # your editor knows this is a str
form.data.destination # autocomplete works
form.data.orign # typo caught by the type checker
```

In Django, `form.cleaned_data["route_name"]` is an untyped dict access. Typos become runtime bugs. In WTForms, `form.route_name.data` has no type information. Air is the first Python form system where your editor knows the shape of validated data, because `form.data` is the actual Pydantic model.
Expand Down Expand Up @@ -241,10 +241,11 @@ For context-aware visibility, use `Annotated` with AirField metadata types direc
from typing import Annotated
from airfield import Hidden, ReadOnly


class ArticleModel(AirModel):
title: str
slug: Annotated[str, Hidden("form")] # hidden in forms, visible in tables
internal: Annotated[str, ReadOnly("form")] # read-only in forms
slug: Annotated[str, Hidden("form")] # hidden in forms, visible in tables
internal: Annotated[str, ReadOnly("form")] # read-only in forms
```

Pydantic constraints like `min_length` and `max_length` automatically become HTML5 `minlength` and `maxlength` attributes, so browser-side validation matches server-side rules.
Expand All @@ -267,9 +268,9 @@ Use `excludes` to hide fields from display, saving, or both:
```python
class JeepneyRouteForm(AirForm[JeepneyRouteModel]):
excludes = (
"internal_id", # hidden from display and save
("origin", "display"), # not rendered, still in save_data()
("tracking_code", "save"), # rendered, excluded from save_data()
"internal_id", # hidden from display and save
("origin", "display"), # not rendered, still in save_data()
("tracking_code", "save"), # rendered, excluded from save_data()
)
```

Expand Down
Loading