diff --git a/README.md b/README.md index 77c0fd3c7..e33c1c700 100644 --- a/README.md +++ b/README.md @@ -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`: @@ -147,9 +147,9 @@ def index(request: air.Request): return jinja(request, name="index.html") ``` -### Start with Python +### 2. Start with Python -Write HTML as typed Python classes. Your editor autocompletes attributes, your type checker validates nesting: +Write HTML as typed Python classes. Using Python allows your editor to autocomplete attributes, and your type checker to validate nesting: `main.py`: @@ -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 + +Either approach produces the same thing: a working web page. + +To see the result, run the following command and open in your browser. ```sh air run ``` -Open 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. diff --git a/docs/api/dependencies.md b/docs/api/dependencies.md index 820700fda..8533e25a3 100644 --- a/docs/api/dependencies.md +++ b/docs/api/dependencies.md @@ -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"), diff --git a/docs/api/requests.md b/docs/api/requests.md index 3fd2ef5eb..d8debb25d 100644 --- a/docs/api/requests.md +++ b/docs/api/requests.md @@ -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")}), + ), ) ``` diff --git a/docs/api/routing.md b/docs/api/routing.md index 414f19fe5..84d69420b 100644 --- a/docs/api/routing.md +++ b/docs/api/routing.md @@ -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" import air router = air.AirRouter() @router.page -def cart(): +def cart_page(): 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="main.py" import air from cart import router as cart_router @@ -31,13 +30,13 @@ 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) @@ -45,7 +44,10 @@ 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 diff --git a/docs/index.md b/docs/index.md index 52f6b6b2a..6bdf34ba1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -129,6 +129,8 @@ uv add "fastapi[standard]" ## A Simple Example +### main.py + Create a `main.py` with: ```python @@ -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 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. @@ -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"), + ), ), ) diff --git a/docs/learn/air_tags.md b/docs/learn/air_tags.md index 564f26ec6..0fbd0755f 100644 --- a/docs/learn/air_tags.md +++ b/docs/learn/air_tags.md @@ -169,9 +169,23 @@ renders as ``` +### Passing reserved words as kwargs + +Alternately, we can pass reserved keywords as kwargs. + +```python +air.Label("Email", **{"class": "plain", "for": "email"}) +``` + +Renders as: + +```html + +``` + ### 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. +To get around that in Python we can't begin function arguments with special characters, we lean into how **Air Tags** are kwarg-friendly. ```python air.P("Hello", class_="plain", **{"@data": 6}) @@ -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: @@ -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'), + ), + ), +) ``` diff --git a/docs/learn/airmodel.md b/docs/learn/airmodel.md index 34e3d4c0f..feb1c5942 100644 --- a/docs/learn/airmodel.md +++ b/docs/learn/airmodel.md @@ -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. diff --git a/docs/learn/cookbook/authentication.md b/docs/learn/cookbook/authentication.md index e5dba6832..ac5ab61b5 100644 --- a/docs/learn/cookbook/authentication.md +++ b/docs/learn/cookbook/authentication.md @@ -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 @@ -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") + ), ) ``` @@ -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 @@ -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"), + ), ) diff --git a/docs/learn/cookbook/bigger-applications.md b/docs/learn/cookbook/bigger-applications.md index 6f1a9fcdd..e11cc2f8f 100644 --- a/docs/learn/cookbook/bigger-applications.md +++ b/docs/learn/cookbook/bigger-applications.md @@ -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: @@ -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: @@ -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"), + ), ) ``` @@ -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, @@ -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 @@ -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"), + ), ), ) diff --git a/docs/learn/layouts.md b/docs/learn/layouts.md index a19fcc009..366f6b5bf 100644 --- a/docs/learn/layouts.md +++ b/docs/learn/layouts.md @@ -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 diff --git a/docs/learn/quickstart.md b/docs/learn/quickstart.md index f52a0e2c0..4bd5a9645 100644 --- a/docs/learn/quickstart.md +++ b/docs/learn/quickstart.md @@ -41,7 +41,10 @@ app = air.Air() @app.get("/") async def index(): - return air.layouts.mvpcss(air.H1("Hello, Air!"), air.P("Breathe it in.")) + return air.layouts.mvpcss( + air.H1("Hello, Air!"), + air.P("Breathe it in."), + ) ``` Serve your app with: @@ -83,7 +86,10 @@ app = air.Air() @app.get("/") def index(): - return air.layouts.mvpcss(air.H1("Hello, Air!"), air.P("Breathe it in.")) + return air.layouts.mvpcss( + air.H1("Hello, Air!"), + air.P("Breathe it in."), + ) @app.get("/air-is-grounded") @@ -119,7 +125,10 @@ app = air.Air() @app.page # Renders as '/' def index(): # (1)! - return air.layouts.mvpcss(air.H1("Hello, Air!"), air.P("Breathe it in.")) + return air.layouts.mvpcss( + air.H1("Hello, Air!"), + air.P("Breathe it in."), + ) @app.page # Renders as '/air-is-grounded' @@ -151,7 +160,10 @@ app = air.Air() @app.get("/users/{username}") # (1)! def user_detail(username: str): # (2)! - return air.layouts.mvpcss(air.Title(username), air.H1(username)) + return air.layouts.mvpcss( + air.Title(username), + air.H1(username), + ) ``` 1. We've specified a variable called `username`. @@ -173,7 +185,10 @@ app = air.Air() @app.get("/users") def user_detail(username: str): # (1)! - return air.layouts.mvpcss(air.Title(username), air.H1(username)) + return air.layouts.mvpcss( + air.Title(username), + air.H1(username), + ) ``` 1. We have defined a function argument named `username`. Because `username` is not part of the decorator's URL path ('/users'), Air automatically treats it as a query parameter. @@ -451,11 +466,15 @@ app = air.Air() @app.get("/avatar") def avatar(request: air.Request): + fragment = air.Div( + air.P("We are fans of the Last Avatar"), + ) # (1)! + return app.jinja( request, "avatar.html", title="Hello, Air Benders", - fragment=air.Div(air.P("We are fans of the Last Avatar"), class_="thing"), # (1)! + fragment=fragment, ) ``` diff --git a/src/air/templating.py b/src/air/templating.py index ae5b0f23f..6c6eac55a 100644 --- a/src/air/templating.py +++ b/src/air/templating.py @@ -50,8 +50,15 @@ class JinjaRenderer: Example: + import air + + from air.requests import Request + + + app = air.Air() + # Instantiate the render callable - jinja = JinjaRenderer('templates') + jinja = air.JinjaRenderer('templates') # Use for returning Jinja from views @app.get('/') @@ -73,7 +80,9 @@ async def home(request: Request): return jinja( request, 'home.html', - content=air.Article(air.P('Cheddar')) + content=air.Article( + air.P('Cheddar'), + ) ) """