Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,11 @@ Third-party context providers: [Code Wiki by Google](https://codewiki.google/git

## Two Ways to Build

Air gives you two paths to HTML. Start with whichever fits your workflow.
Air gives you two paths to rendering HTML. Start with whichever fits your workflow.

### Start with HTML
### 1. Start with HTML

Have your AI generate an HTML mockup, or write one yourself. Drop it in a template, wire it up with minimal Python:
Have your AI generate an HTML mockup, or write one yourself. Drop it in a template, then wire it up with minimal Python:

`templates/index.html`:

Expand Down Expand Up @@ -149,7 +149,7 @@ def index(request: air.Request):

### Start with Python

Write HTML as typed Python classes. Your editor autocompletes attributes, your type checker validates nesting:
Write HTML as typed Python classes. Your editor can autocomplete attributes and your type checker validates nesting:
Comment thread
audreyfeldroy marked this conversation as resolved.
Outdated

`main.py`:

Expand All @@ -161,17 +161,21 @@ app = air.Air()

@app.page
def index():
return air.Html(air.H1("Hello, world!"))
return air.Html(
air.H1("Hello, world!"),
)
```

### Run either one
## Running Air's Development Server

Choosing either of the above approach, both paths produces the same thing: a working web page.

To see the result, run the following command and open <http://127.0.0.1:8000> in your browser.

```sh
air run
```

Open <http://127.0.0.1:8000> to see the result. Both paths produce the same thing: a working web page.

## Use FastAPI Alongside Air

Air is powered by FastAPI. You get Air's HTML tools for your pages and FastAPI's full capabilities for your API, all in one app.
Expand Down
4 changes: 3 additions & 1 deletion docs/api/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ def get_users(is_htmx: bool = Depends(air.is_htmx_request)):
# Return full page for regular requests
return air.Html(
[
air.Head(air.Title("Users")),
air.Head(
air.Title("Users"),
),
air.Body(
[
air.H1("User List"),
Expand Down
4 changes: 3 additions & 1 deletion docs/api/requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ app = air.Air()
async def login(request: Request):
form = await request.form()
return air.layouts.mvpcss(
air.Section(air.Aside({"username": form.get("username")}))
air.Section(
air.Aside({"username": form.get("username")}),
),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take it this reduces cognitive load / friction for new users? @MrValdez

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, as I read through the rest of the diff I could see it feels better with the spacing.

)
```

Expand Down
28 changes: 15 additions & 13 deletions docs/api/routing.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
Routing
## Routing

If you need to knit several Python modules with their own Air views into one, that's where Routing is used. They allow the near seamless combination of multiple Air apps into one. Larger sites are often built from multiple routers.
If you need to knit several Python modules with their own Air views into one, you will need to use Routing. This allow the near seamless combination of multiple Air apps into one. Larger sites are often built from multiple routers.

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()`:
For this example, let's imagine we have an e-commerce store with a shopping cart app with a `cart.py` and `main.py` file.

```python
# cart.py
```python title="cart.py"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoa, didn't know about this, cool!

import air

router = air.AirRouter()


@router.page
def cart():
def cart_page():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch

return air.H1("I am a shopping cart")
```

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

```python
```python title="cart.py"
import air
from cart import router as cart_router

Expand All @@ -31,21 +30,24 @@ def index():
return air.H1("Home page")
```

Note that the router allows sharing of sessions and other application states.
`AirRouter` allows the sharing of sessions and other application states between routes.

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

```python
```python title="main.py"
import air
from cart import router as cart_router, cart
from cart import router as cart_router, cart_page

app = air.Air()
app.include_router(cart_router)


@app.page
def index():
return air.Div(air.H1("Home page"), air.A("View cart", href=cart.url()))
return air.Div(
air.H1("Home page"),
air.A("View cart", href=cart_page.url()),
)
```

## Query Parameters
Expand Down
24 changes: 21 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ uv add "fastapi[standard]"

## A Simple Example

### main.py

Create a `main.py` with:

```python
Expand All @@ -139,13 +141,25 @@ app = air.Air()

@app.get("/")
async def index():
return air.Html(air.H1("Hello, world!", style="color: blue;"))
return air.Html(
air.H1("Hello, world!", style="color: blue;"),
)
```

!!! note

This example uses [Air Tags](api/tags/index.md), which are Python classes that render as HTML. Air Tags are typed and documented, designed to work well with any code completion tool.

### Running Air

To run the development server, run the following command in your terminal:

```sh
air run
```

Open <http://127.0.0.1:8000> to see the above example running.

## Combining FastAPI and Air

Air is just a layer over FastAPI. So it is trivial to combine sophisticated HTML pages and a REST API into one app.
Expand All @@ -162,10 +176,14 @@ api = FastAPI()
@app.get("/")
def landing_page():
return air.Html(
air.Head(air.Title("Awesome SaaS")),
air.Head(
air.Title("Awesome SaaS"),
),
air.Body(
air.H1("Awesome SaaS"),
air.P(air.A("API Docs", target="_blank", href="/api/docs")),
air.P(
air.A("API Docs", target="_blank", href="/api/docs"),
),
),
)

Expand Down
29 changes: 27 additions & 2 deletions docs/learn/air_tags.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,20 @@ renders as
</script>
```

### Passing reserved words as kwargs

Alternately, we can pass reserved keywords as kwargs.

```python
air.Label("Email", **{"class": "plain", "for": "email"})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it :)

```

Renders as:

```html
<label class="plain" for="email">Email</label>
```

### Attributes starting with special characters

To get around that in Python we can't begin function arguments with special characters, we lean into how **Air Tags** is kwargs friendly.
Comment thread
audreyfeldroy marked this conversation as resolved.
Outdated
Expand Down Expand Up @@ -275,7 +289,11 @@ Subclasses are not the only way to create custom Air Tags. You can also use func

```python
def card(*content, header: str, footer: str):
return air.Article(air.Header(header), *content, air.Footer(footer))
return air.Article(
air.Header(header),
*content,
air.Footer(footer),
)
```

We can use this function to create a card:
Expand Down Expand Up @@ -365,5 +383,12 @@ air.BaseTag.from_html_to_source("""
This generates:

```python
air.Html(air.Body(air.Main(air.H1("Hello, World", class_="header"))))
air.Html(
air.Head(),
air.Body(
air.Main(
air.H1('Hello, World', class_='header'),
),
),
)
```
12 changes: 10 additions & 2 deletions docs/learn/airmodel.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,16 @@ async def submit_contact(request: air.Request):
form = await ContactForm.from_request(request)
if form.is_valid:
await ContactMessage.create(**form.save_data())
return air.Html(air.H1("Message sent"))
return air.Html(air.Form(form.render(), method="post", action="/contact"))
return air.Html(
air.H1("Message sent"),
)
return air.Html(
air.Form(
form.render(),
method="post",
action="/contact"
),
)
```

`AirForm[ContactMessage]` gives you type-safe validated data. `ContactMessage.create()` writes it to PostgreSQL. Your editor knows the types at every step.
Expand Down
19 changes: 15 additions & 4 deletions docs/learn/cookbook/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ async def index(request: air.Request):
action = air.Tags(
air.H1(request.session["username"]),
air.P(request.session.get("logged_in_at")),
air.P(air.A("Logout", href="/logout")),
air.P(
air.A("Logout", href="/logout"),
),
)
else:
# login the user
Expand Down Expand Up @@ -111,7 +113,9 @@ def require_login(request: air.Request):
async def dashboard(request: air.Request, user=Depends(require_login)):
return air.layouts.mvpcss(
air.H1(f"Dashboard for {request.session['user']['username']}"),
air.P(air.A("Logout", href="/logout")),
air.P(
air.A("Logout", href="/logout")
),
)
```

Expand Down Expand Up @@ -143,7 +147,12 @@ def require_login(request: air.Request):
# --- Routes ---
@app.page
async def index(request: air.Request):
return air.layouts.mvpcss(air.H1("Landing page"), air.P(air.A("Dashboard", href="/dashboard")))
return air.layouts.mvpcss(
air.H1("Landing page"),
air.P(
air.A("Dashboard", href="/dashboard"),
),
)


@app.page
Expand Down Expand Up @@ -179,7 +188,9 @@ async def login():
async def dashboard(request: air.Request, user=Depends(require_login)):
return air.layouts.mvpcss(
air.H1(f"Dashboard for {request.session['user']['username']}"),
air.P(air.A("Logout", href="/logout")),
air.P(
air.A("Logout", href="/logout"),
),
)


Expand Down
38 changes: 31 additions & 7 deletions docs/learn/cookbook/bigger-applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ app = air.Air()

@app.page
def index():
return air.layouts.mvpcss(air.H1("Avatar Data"), air.P(air.A("Dashboard", href="/dashboard")))
return air.layouts.mvpcss(
air.H1("Avatar Data"),
air.P(
air.A("Dashboard", href="/dashboard"),
),
)
```

Now for the dashboard, instead of using the typical `air.Air` tool to instantiate our application, we use `air.AirRouter` like so:
Expand All @@ -31,7 +36,12 @@ router = air.AirRouter()

@router.page
def dashboard():
return air.layouts.mvpcss(air.H1("Avatar Data Dashboard"), air.P(air.A("<- Home", href="/")))
return air.layouts.mvpcss(
air.H1("Avatar Data Dashboard"),
air.P(
air.A("<- Home", href="/"),
),
)
```

Now if we go back to our `main.py` we can use the `app.include_router()` method to include the dashboard in our app:
Expand All @@ -47,7 +57,10 @@ app.include_router(router)
@app.page
def index():
return air.layouts.mvpcss(
air.H1("Avatar Data"), air.P(air.A("Dashboard", href="/dashboard"))
air.H1("Avatar Data"),
air.P(
air.A("Dashboard", href="/dashboard"),
),
)
```

Expand All @@ -73,7 +86,12 @@ app = air.Air(title="Air")

@app.page
def index():
return air.layouts.mvpcss(air.H1("Air landing page"), air.P(air.A("Shop", href="/shop")))
return air.layouts.mvpcss(
air.H1("Air landing page"),
air.P(
air.A("Shop", href="/shop"),
),
)


# Creating a separate app for the shop,
Expand All @@ -83,7 +101,9 @@ shop = air.Air(title="Air shop")

@shop.page
def index():
return air.layouts.mvpcss(air.H1("Shop for Air things"))
return air.layouts.mvpcss(
air.H1("Shop for Air things"),
)


# Mount the shop app to the main app
Expand All @@ -107,10 +127,14 @@ app = air.Air()
@app.get("/")
def landing_page():
return air.Html(
air.Head(air.Title("Awesome SaaS")),
air.Head(
air.Title("Awesome SaaS"),
),
air.Body(
air.H1("Awesome SaaS"),
air.P(air.A("API Docs", target="_blank", href="/api/docs")),
air.P(
air.A("API Docs", target="_blank", href="/api/docs"),
),
),
)

Expand Down
8 changes: 6 additions & 2 deletions docs/learn/layouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ Air's layout functions automatically sort your tags into the right places using
# Verbose Way
air.Html(
air.Head(
air.Title("My App"), air.Link(rel="stylesheet", href="style.css")
air.Title("My App"),
air.Link(rel="stylesheet", href="style.css"),
),
air.Body(
air.H1("Welcome"),
air.P("Content here"),
),
air.Body(air.H1("Welcome"), air.P("Content here")),
)

# Air Layouts
Expand Down
Loading