Skip to content

Commit

Permalink
added the project + tests + docs + CI
Browse files Browse the repository at this point in the history
  • Loading branch information
volfpeter committed Jan 24, 2024
1 parent 2f47ce6 commit f19e7bb
Show file tree
Hide file tree
Showing 22 changed files with 789 additions and 1 deletion.
27 changes: 27 additions & 0 deletions .github/workflows/build-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Build documentation
on:
push:
branches:
- main
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v3

- name: Install python
uses: actions/setup-python@v4
with:
python-version-file: pyproject.toml

- uses: actions/cache@v2
with:
key: ${{ github.ref }}
path: .cache

- run: pip install mkdocs-material mkdocstrings[python]

- run: mkdocs gh-deploy --force
25 changes: 25 additions & 0 deletions .github/workflows/linters.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Linters
on: push
jobs:
Linters:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v3

- name: Install python
uses: actions/setup-python@v4
with:
python-version-file: pyproject.toml

- name: Install poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
- name: Install dependencies
run: |
poetry install
- name: Run static checks
run: |
poetry run poe static-checks
25 changes: 25 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Tests
on: push
jobs:
Linters:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v3

- name: Install python
uses: actions/setup-python@v4
with:
python-version-file: pyproject.toml

- name: Install poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
- name: Install dependencies
run: |
poetry install
- name: Run tests
run: |
poetry run poe test
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ ipython_config.py
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
poetry.lock

# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
Expand Down
115 changes: 115 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
![Tests](https://github.com/volfpeter/fasthx/actions/workflows/tests.yml/badge.svg)
![Linters](https://github.com/volfpeter/fasthx/actions/workflows/linters.yml/badge.svg)
![Documentation](https://github.com/volfpeter/fasthx/actions/workflows/build-docs.yml/badge.svg)
![PyPI package](https://img.shields.io/pypi/v/fasthx?color=%2334D058&label=PyPI%20Package)

**Source code**: [https://github.com/volfpeter/fasthx](https://github.com/volfpeter/fasthx)

**Documentation and examples**: [https://volfpeter.github.io/fasthx](https://volfpeter.github.io/fasthx/)

# FastHX

FastAPI and HTMX, the right way.

Key features:

- **Decorator syntax** that works with FastAPI as one would expect, no need for unused or magic dependencies in routes.
- Works with **any templating engine** or server-side rendering rendering library, e.g. `markyp-html` or `dominate`.
- Built-in **Jinja2 templating support** (even with multiple template folders).
- Gives the rendering engine **access to all dependencies** of the decorated route.
- FastAPI **routes will keep working normally by default** if they receive **non-HTMX** requests, so the same route can serve data and render HTML at the same time.
- **Correct typing** makes it possible to apply other (typed) decorators to your routes.
- Works with both **sync** and **async routes**.

## Installation

The package is available on PyPI and can be installed with:

```console
$ pip install fasthx
```

## Examples

### Jinja2 templating

To start serving HTMX requests, all you need to do is create an instance of `fasthx.Jinja` and use it as a decorator on your routes like this:

```python
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from fasthx import Jinja

# Create the app.
app = FastAPI()

# Create a FastAPI Jinja2Templates instance and use it to create a
# FastHX Jinja instance that will serve as your decorator.
jinja = Jinja(Jinja2Templates("templates"))

@app.get("/htmx-or-data")
@jinja("user-list.html") # Render the response with the user-list.html template.
def htmx_or_data() -> dict[str, list[dict[str, str]]]:
return {"users": [{"name": "Joe"}]}

@app.get("/htmx-only")
@jinja.template("user-list.html", no_data=True) # Render the response with the user-list.html template.
def htmx_only() -> dict[str, list[dict[str, str]]]:
# no_data is set to True, so this route can not serve JSON, it only responds to HTMX requests.
return {"users": [{"name": "Joe"}]}
```

### Custom templating

Custom templating offers more flexibility than the built-in `Jinja` renderer by giving access to all dependencies of the decorated route to the renderer function:

```python
from typing import Annotated

from fastapi import Depends, FastAPI
from fasthx import hx

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

DependsRandomNumber = Annotated[int, Depends(get_random_number)]

# Create the render method: it must always have these three arguments.
# If you're using static type checkers, the type hint of `result` must match the return type
# annotation of the route on which this render method is used.
def render_user_list(result: list[dict[str, str]], *, context: dict[str, Any], request: Request) -> str:
# The value of the `DependsRandomNumber` dependency is accessible with the same name as in the route.
random_number = context["random_number"]
lucky_number = f"<h1>{random_number}</h1>"
users = "".join(("<ul>", *(f"<li>{u.name}</li>" for u in result), "</ul>"))
return f"{lucky_number}\n{users}"

@app.get("/htmx-or-data")
@hx(render_user_list)
def htmx_or_data(random_number: DependsRandomNumber) -> list[dict[str, str]]:
return [{"name": "Joe"}]

@app.get("/htmx-only")
@hx(render_user_list, no_data=True)
async def htmx_only(random_number: DependsRandomNumber) -> list[dict[str, str]]:
return [{"name": "Joe"}]
```

## Dependencies

The only dependency of this package is `fastapi`.

## Development

Use `ruff` for linting and formatting, `mypy` for static code analysis, and `pytest` for testing.

The documentation is built with `mkdocs-material` and `mkdocstrings`.

## Contributing

All contributions are welcome.

## License - MIT

The package is open-sourced under the conditions of the [MIT license](https://choosealicense.com/licenses/mit/).
3 changes: 3 additions & 0 deletions docs/api-DependsHXRequest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `DependsHXRequest`

::: fasthx.main.DependsHXRequest
3 changes: 3 additions & 0 deletions docs/api-HTMXRenderer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `HTMXRenderer`

::: fasthx.main.HTMXRenderer
3 changes: 3 additions & 0 deletions docs/api-Jinja.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `Jinja`

::: fasthx.main.Jinja
3 changes: 3 additions & 0 deletions docs/api-get_hx_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `get_hx_request()`

::: fasthx.main.get_hx_request
3 changes: 3 additions & 0 deletions docs/api-hx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `hx()`

::: fasthx.main.hx
115 changes: 115 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
![Tests](https://github.com/volfpeter/fasthx/actions/workflows/tests.yml/badge.svg)
![Linters](https://github.com/volfpeter/fasthx/actions/workflows/linters.yml/badge.svg)
![Documentation](https://github.com/volfpeter/fasthx/actions/workflows/build-docs.yml/badge.svg)
![PyPI package](https://img.shields.io/pypi/v/fasthx?color=%2334D058&label=PyPI%20Package)

**Source code**: [https://github.com/volfpeter/fasthx](https://github.com/volfpeter/fasthx)

**Documentation and examples**: [https://volfpeter.github.io/fasthx](https://volfpeter.github.io/fasthx/)

# FastHX

FastAPI and HTMX, the right way.

Key features:

- **Decorator syntax** that works with FastAPI as one would expect, no need for unused or magic dependencies in routes.
- Works with **any templating engine** or server-side rendering rendering library, e.g. `markyp-html` or `dominate`.
- Built-in **Jinja2 templating support** (even with multiple template folders).
- Gives the rendering engine **access to all dependencies** of the decorated route.
- FastAPI **routes will keep working normally by default** if they receive **non-HTMX** requests, so the same route can serve data and render HTML at the same time.
- **Correct typing** makes it possible to apply other (typed) decorators to your routes.
- Works with both **sync** and **async routes**.

## Installation

The package is available on PyPI and can be installed with:

```console
$ pip install fasthx
```

## Examples

### Jinja2 templating

To start serving HTMX requests, all you need to do is create an instance of `fasthx.Jinja` and use it as a decorator on your routes like this:

```python
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from fasthx import Jinja

# Create the app.
app = FastAPI()

# Create a FastAPI Jinja2Templates instance and use it to create a
# FastHX Jinja instance that will serve as your decorator.
jinja = Jinja(Jinja2Templates("templates"))

@app.get("/htmx-or-data")
@jinja("user-list.html") # Render the response with the user-list.html template.
def htmx_or_data() -> dict[str, list[dict[str, str]]]:
return {"users": [{"name": "Joe"}]}

@app.get("/htmx-only")
@jinja.template("user-list.html", no_data=True) # Render the response with the user-list.html template.
def htmx_only() -> dict[str, list[dict[str, str]]]:
# no_data is set to True, so this route can not serve JSON, it only responds to HTMX requests.
return {"users": [{"name": "Joe"}]}
```

### Custom templating

Custom templating offers more flexibility than the built-in `Jinja` renderer by giving access to all dependencies of the decorated route to the renderer function:

```python
from typing import Annotated

from fastapi import Depends, FastAPI
from fasthx import hx

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

DependsRandomNumber = Annotated[int, Depends(get_random_number)]

# Create the render method: it must always have these three arguments.
# If you're using static type checkers, the type hint of `result` must match the return type
# annotation of the route on which this render method is used.
def render_user_list(result: list[dict[str, str]], *, context: dict[str, Any], request: Request) -> str:
# The value of the `DependsRandomNumber` dependency is accessible with the same name as in the route.
random_number = context["random_number"]
lucky_number = f"<h1>{random_number}</h1>"
users = "".join(("<ul>", *(f"<li>{u.name}</li>" for u in result), "</ul>"))
return f"{lucky_number}\n{users}"

@app.get("/htmx-or-data")
@hx(render_user_list)
def htmx_or_data(random_number: DependsRandomNumber) -> list[dict[str, str]]:
return [{"name": "Joe"}]

@app.get("/htmx-only")
@hx(render_user_list, no_data=True)
async def htmx_only(random_number: DependsRandomNumber) -> list[dict[str, str]]:
return [{"name": "Joe"}]
```

## Dependencies

The only dependency of this package is `fastapi`.

## Development

Use `ruff` for linting and formatting, `mypy` for static code analysis, and `pytest` for testing.

The documentation is built with `mkdocs-material` and `mkdocstrings`.

## Contributing

All contributions are welcome.

## License - MIT

The package is open-sourced under the conditions of the [MIT license](https://choosealicense.com/licenses/mit/).
5 changes: 5 additions & 0 deletions fasthx/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .main import DependsHXRequest as DependsHXRequest
from .main import HTMXRenderer as HTMXRenderer
from .main import Jinja as Jinja
from .main import get_hx_request as get_hx_request
from .main import hx as hx
Loading

0 comments on commit f19e7bb

Please sign in to comment.