Skip to content

Commit 6b9770a

Browse files
authored
Merge pull request #78 from volfpeter/feat/v3
Version 3 refactoring
2 parents 9d3dbe9 + 9e14226 commit 6b9770a

File tree

24 files changed

+314
-399
lines changed

24 files changed

+314
-399
lines changed

.github/workflows/linters.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@ jobs:
3030
- name: Install dependencies
3131
run: poetry install
3232

33-
- name: Run static checks
34-
run: poetry run poe static-checks
33+
- name: Run checks
34+
run: poetry run poe check

README.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ FastAPI server-side rendering with built-in HTMX support.
2222
- **Correct typing** makes it possible to apply other (typed) decorators to your routes.
2323
- Works with both **sync** and **async routes**.
2424

25+
## Testimonials
26+
27+
"Thank you for your work on `fasthx`, as well as `htmy`! I've never had an easier time developing with another stack." ([ref](https://github.com/volfpeter/fasthx/discussions/77))
28+
29+
"One of the main parts of the `FastAPI` -> `fasthx` -> `htmy` integration I'm falling in love with is its explicitness, and not too much magic happening." ([ref](https://github.com/volfpeter/fasthx/issues/54))
30+
31+
"Thank you for your work on `htmy` and `fasthx`, both have been very pleasant to use, and the APIs are both intuitive and simple. Great work." ([ref](https://github.com/volfpeter/fasthx/issues/54))
32+
2533
## Support
2634

2735
Consider supporting the development and maintenance of the project through [sponsoring](https://buymeacoffee.com/volfpeter), or reach out for [consulting](https://www.volfp.com/contact?subject=Consulting%20-%20FastHX) so you can get the most out of the library.
@@ -41,10 +49,9 @@ The package has optional dependencies for the following **official integrations*
4149

4250
## Core concepts
4351

44-
The core concept of FastHX is to let FastAPI routes do their usual job of handling the business logic and returning the result, while the FastHX decorators take care
45-
of the entire rendering / presentation layer using a declarative, decorator-based approach.
52+
The core concept of FastHX is to let FastAPI routes do their usual job of handling the business logic and returning the result, while the FastHX decorators take care of the entire rendering and presentation layer using a declarative, decorator-based approach.
4653

47-
Interally, FastHX decorators always have access to the decorated route's result, all of its arguments (sometimes called the request context), and the current request. Integrations convert these values into data that can be consumed by the used rendering engine (for example `htmy` or `jinja`), run the rendering engine with the selected component (more on this below) and the created data, and return the result to the client. For more details on how data conversion works and how it can be customized, please see the API documentation of the rendering engine integration of your choice.
54+
Internally, FastHX decorators always have access to the decorated route's result, all of its arguments (sometimes called the request context), and the current request. Integrations convert these values into data that can be consumed by the used rendering engine (for example `htmy` or `jinja`), run the rendering engine with the selected component (more on this below) and the created data, and return the result to the client. For more details on how data conversion works and how it can be customized, please see the API documentation of the rendering engine integration of your choice.
4855

4956
The `ComponentSelector` abstraction makes it possible to declaratively specify and dynamically select the component that should be used to render the response to a given request. It is also possible to define an "error" `ComponentSelector` that is used if the decorated route raises an exception -- a typical use-case being error rendering for incorrect user input.
5057

@@ -98,12 +105,12 @@ def index() -> None: ...
98105

99106
Requires: `pip install fasthx[jinja]`.
100107

101-
To start serving HTML and HTMX requests, all you need to do is create an instance of `fasthx.Jinja` and use its `hx()` or `page()` methods as decorators on your routes. `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML. See the example code below:
108+
To start serving HTML and HTMX requests, all you need to do is create an instance of `fasthx.jinja.Jinja` and use its `hx()` or `page()` methods as decorators on your routes. `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML. See the example code below:
102109

103110
```python
104111
from fastapi import FastAPI
105112
from fastapi.templating import Jinja2Templates
106-
from fasthx import Jinja
113+
from fasthx.jinja import Jinja
107114
from pydantic import BaseModel
108115

109116
# Pydantic model of the data the example API is using.
@@ -145,7 +152,7 @@ See the full working example [here](https://github.com/volfpeter/fasthx/tree/mai
145152

146153
Requires: `pip install fasthx`.
147154

148-
If you would like to use a rendering engine without FastHX integration, you can easily build on the `hx()` and `page()` decorators which give you all the functionality you will need. All you need to do is implement the `HTMLRenderer` protocol.
155+
If you would like to use a rendering engine without FastHX integration, you can easily build on the `hx()` and `page()` decorators which give you all the functionality you will need. All you need to do is implement the `RenderFunction` protocol.
149156

150157
Similarly to the Jinja case, `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML. See the example code below:
151158

@@ -158,7 +165,7 @@ from fasthx import hx, page
158165
# Create the app.
159166
app = FastAPI()
160167

161-
# Create a dependecy to see that its return value is available in the render function.
168+
# Create a dependency to see that its return value is available in the render function.
162169
def get_random_number() -> int:
163170
return 4 # Chosen by fair dice roll.
164171

docs/api/core-decorators.md

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
# Core Decorators
2-
3-
## ::: fasthx.hx
4-
5-
options:
6-
show_root_heading: true
7-
8-
## ::: fasthx.page
1+
# ::: fasthx.core_decorators
92

103
options:
114
show_root_heading: true

docs/api/dependencies.md

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
# FastAPI Dependencies
2-
3-
## ::: fasthx.DependsHXRequest
4-
5-
options:
6-
show_root_heading: true
7-
8-
## ::: fasthx.get_hx_request
1+
# ::: fasthx.dependencies
92

103
options:
114
show_root_heading: true

docs/api/jinja.md

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,4 @@
1-
# `Jinja`
1+
# ::: fasthx.jinja
22

3-
## ::: fasthx.Jinja
4-
options:
5-
show_root_heading: true
6-
7-
## ::: fasthx.JinjaContext
8-
options:
9-
show_root_heading: true
10-
11-
## ::: fasthx.TemplateHeader
12-
options:
13-
show_root_heading: true
14-
15-
## ::: fasthx.JinjaPath
16-
options:
17-
show_root_heading: true
18-
19-
## ::: fasthx.JinjaContextFactory
20-
members:
21-
- __call__
223
options:
234
show_root_heading: true

docs/api/typing.md

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,4 @@
1-
# Typing
1+
# ::: fasthx.typing
22

3-
## ::: fasthx.HTMLRenderer
4-
options:
5-
show_root_heading: true
6-
7-
## ::: fasthx.typing.SyncHTMLRenderer
8-
members:
9-
- __call__
10-
options:
11-
show_root_heading: true
12-
13-
## ::: fasthx.typing.AsyncHTMLRenderer
14-
members:
15-
- __call__
16-
options:
17-
show_root_heading: true
18-
19-
## ::: fasthx.typing.RequestComponentSelector
20-
options:
21-
show_root_heading: true
22-
23-
## ::: fasthx.typing.ComponentSelector
243
options:
254
show_root_heading: true

docs/examples/custom-templating.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Custom Templating
22

3-
If you're not into Jinja templating, the `hx()` and `page()` decorators give you all the flexibility you need: you can integrate any HTML rendering or templating engine into `fasthx` simply by implementing the `HTMLRenderer` protocol. Similarly to the Jinja case, `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML. See the example code below:
3+
If you're not into Jinja templating, the `hx()` and `page()` decorators give you all the flexibility you need: you can integrate any HTML rendering or templating engine into `fasthx` simply by implementing the `RenderFunction` protocol. Similarly to the Jinja case, `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML. See the example code below:
44

55
```python
66
from typing import Annotated, Any
@@ -11,7 +11,7 @@ from fasthx import hx, page
1111
# Create the app.
1212
app = FastAPI()
1313

14-
# Create a dependecy to see that its return value is available in the render function.
14+
# Create a dependency to see that its return value is available in the render function.
1515
def get_random_number() -> int:
1616
return 4 # Chosen by fair dice roll.
1717

@@ -20,7 +20,7 @@ DependsRandomNumber = Annotated[int, Depends(get_random_number)]
2020
# Create the render methods: they must always have these three arguments.
2121
# If you're using static type checkers, the type hint of `result` must match
2222
# the return type annotation of the route on which this render method is used.
23-
def render_index(result: list[dict[str, str]], *, context: dict[str, Any], request: Request) -> str:
23+
def render_index(result: Any, *, context: dict[str, Any], request: Request) -> str:
2424
return "<h1>Hello FastHX</h1>"
2525

2626
def render_user_list(result: list[dict[str, str]], *, context: dict[str, Any], request: Request) -> str:

docs/examples/htmy.md

Lines changed: 38 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ First, let's create an `htmy_app.py` file, import everything that is required fo
88
import random
99
from dataclasses import dataclass
1010
from datetime import date
11+
from typing import Any
1112

1213
from fastapi import FastAPI
14+
from fasthx.htmy import HTMY, ComponentHeader, CurrentRequest, RouteParams
1315
from htmy import Component, Context, html
1416
from pydantic import BaseModel
1517

16-
from fasthx.htmy import HTMY, ComponentHeader, CurrentRequest, RouteParams
17-
1818

1919
class User(BaseModel):
2020
"""User model."""
@@ -23,24 +23,19 @@ class User(BaseModel):
2323
birthday: date
2424
```
2525

26-
The main content on the user interface will be a user list, so let's start by creating a simple `UserListItem` component:
26+
The main content on the user interface will be a user list, so let's start by creating a simple `user_list_item` component factory (see the [htmy components guide](https://volfpeter.github.io/htmy/components-guide/) for more information):
2727

2828
```python
29-
@dataclass
30-
class UserListItem:
31-
"""User list item component."""
32-
33-
user: User
34-
35-
def htmy(self, context: Context) -> Component:
36-
return html.li(
37-
html.span(self.user.name, class_="font-semibold"),
38-
html.em(f" (born {self.user.birthday.isoformat()})"),
39-
class_="text-lg",
40-
)
29+
def user_list_item(user: User) -> html.li:
30+
"""User list item component factory."""
31+
return html.li(
32+
html.span(user.name, class_="font-semibold"),
33+
html.em(f" (born {user.birthday.isoformat()})"),
34+
class_="text-lg",
35+
)
4136
```
4237

43-
As you can see, the component has a single `user` property and it renders an `<li>` HTML element with the user's name and birthday in it.
38+
As you can see, the component factory expects a `user` and it creates an `<li>` HTML element with the user's name and birthday in it.
4439

4540
The next component we need is the user list itself. This is going to be the most complex part of the example:
4641

@@ -70,7 +65,7 @@ class UserOverview:
7065
rerenders: int = route_params["rerenders"]
7166

7267
# Create the user list item generator.
73-
user_list_items = (UserListItem(u) for u in self.users)
68+
user_list_items = (user_list_item(u) for u in self.users)
7469

7570
# Create the ordered or unordered user list.
7671
user_list = (
@@ -92,7 +87,7 @@ class UserOverview:
9287
user_list,
9388
# -- HTMX directives.
9489
hx_trigger="load delay:1000",
95-
hx_get=f"/users?rerenders={rerenders+1}",
90+
hx_get=f"/users?rerenders={rerenders + 1}",
9691
hx_swap="outerHTML",
9792
# Send the next component variant in an X-Component header.
9893
hx_headers=f'{{"X-Component": "{next_variant}"}}',
@@ -107,37 +102,33 @@ Most of this code is basic Python and `htmy` usage (including the `hx_*` `HTMX`
107102
- The use of `RouteParams.from_context()` to get access to every route parameter (resolved FastAPI dependency) as a mapping.
108103
- The `context["user-agent"]` lookup that accesses a value from the context which will be added by a _request processor_ later in the example.
109104

110-
We need one last `htmy` component, the index page. Most of this component is just the basic HTML document structure with some TailwindCSS styling and metadata. There is also a bit of `HTMX` in the `body` for lazy loading the actual page content, the user list we just created.
105+
We need one last `htmy` component, the index page. Most of this component (component factory to be more precise) is just the basic HTML document structure with some TailwindCSS styling and metadata. There is also a bit of `HTMX` in the `body` for lazy loading the actual page content, the user list we just created.
111106

112107
```python
113-
@dataclass
114-
class IndexPage:
115-
"""Index page with TailwindCSS styling."""
116-
117-
def htmy(self, context: Context) -> Component:
118-
return (
119-
html.DOCTYPE.html,
120-
html.html(
121-
html.head(
122-
# Some metadata
123-
html.title("FastHX + HTMY example"),
124-
html.meta.charset(),
125-
html.meta.viewport(),
126-
# TailwindCSS
127-
html.script(src="https://cdn.tailwindcss.com"),
128-
# HTMX
129-
html.script(src="https://unpkg.com/[email protected]"),
130-
),
131-
html.body(
132-
# Page content: lazy-loaded user list.
133-
html.div(hx_get="/users", hx_trigger="load", hx_swap="outerHTML"),
134-
class_=(
135-
"h-screen w-screen flex flex-col items-center justify-center "
136-
" gap-4 bg-slate-800 text-white"
137-
),
108+
def index_page(_: Any) -> Component:
109+
return (
110+
html.DOCTYPE.html,
111+
html.html(
112+
html.head(
113+
# Some metadata
114+
html.title("FastHX + HTMY example"),
115+
html.meta.charset(),
116+
html.meta.viewport(),
117+
# TailwindCSS
118+
html.script(src="https://cdn.tailwindcss.com"),
119+
# HTMX
120+
html.script(src="https://unpkg.com/[email protected]"),
121+
),
122+
html.body(
123+
# Page content: lazy-loaded user list.
124+
html.div(hx_get="/users", hx_trigger="load", hx_swap="outerHTML"),
125+
class_=(
126+
"h-screen w-screen flex flex-col items-center justify-center "
127+
" gap-4 bg-slate-800 text-white"
138128
),
139129
),
140-
)
130+
),
131+
)
141132
```
142133

143134
With all the components ready, we can now create the `FastAPI` and `fasthx.htmy.HTMY` instances:
@@ -159,11 +150,11 @@ Note how we added a _request processor_ function to the `HTMY` instance that tak
159150

160151
All that remains now is the routing. We need two routes: one that serves the index page, and one that renders the ordered or unordered user list.
161152

162-
The index page route is trivial. The `htmy.page()` decorator expects a component factory (well more precisely a `fasthx.ComponentSelector`) that accepts the route's return value and returns an `htmy` component. Since `IndexPage` has no properties, we use a simple `lambda` to create such a function:
153+
The index page route is trivial. The `htmy.page()` decorator expects a component factory (well more precisely a `fasthx.ComponentSelector`) that accepts the route's return value and returns an `htmy` component. `index_page` is implemented exactly like this, so we can use it directly in the decorator:
163154

164155
```python
165156
@app.get("/")
166-
@htmy.page(lambda _: IndexPage())
157+
@htmy.page(index_page)
167158
def index() -> None:
168159
"""The index page of the application."""
169160
...

docs/examples/jinja-templating.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
## Basics
44

5-
To start serving HTML and HTMX requests, all you need to do is create an instance of `fasthx.Jinja` and use its `hx()` or `page()` methods as decorators on your routes. `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML, saving you some boilerplate code. See the example code below:
5+
To start serving HTML and HTMX requests, all you need to do is create an instance of `fasthx.jinja.Jinja` and use its `hx()` or `page()` methods as decorators on your routes. `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML, saving you some boilerplate code. See the example code below:
66

77
```python
88
from fastapi import FastAPI
99
from fastapi.templating import Jinja2Templates
10-
from fasthx import Jinja
10+
from fasthx.jinja import ComponentHeader, Jinja
1111
from pydantic import BaseModel
1212

1313
# Pydantic model of the data the example API is using.
@@ -43,10 +43,9 @@ def htmx_only() -> list[User]:
4343
return [User(first_name="Billy", last_name="Shears")]
4444
```
4545

46-
## Using `TemplateHeader`
46+
## Using `ComponentHeader`
4747

48-
In the basic example, routes always rendered a fixed HTML template. `TemplateHeader` lifts this restriction by letting the client submit the _key_ of the required template,
49-
automatically looking up the corresponding template, and of course rendering it.
48+
In the basic example, routes always rendered a fixed HTML template. `ComponentHeader` lifts this restriction by letting the client submit the _key_ of the required template, automatically looking up the corresponding template, and of course rendering it.
5049

5150
This can be particularly helpful when multiple templates/UI components require the same data and business logic.
5251

@@ -56,7 +55,7 @@ jinja = Jinja(Jinja2Templates("templates"))
5655

5756
@app.get("/profile/{id}")
5857
@jinja.hx(
59-
TemplateHeader(
58+
ComponentHeader(
6059
"X-Component",
6160
{
6261
"card": "profile/card.jinja",

0 commit comments

Comments
 (0)