Pyganini
Pyganini is a server-first Python web framework for HTML applications. You organize routes as Python packages, render Jinja templates, and use generated URL helpers instead of handwritten paths. Pyganini supports visible HTMX interactions while your application keeps control of Starlette or FastAPI, middleware, data access, and deployment.
If Pyganini helps your project, please star the repository.
Why Pyganini?
Server-rendered applications often accumulate route tables, string URLs, and rendering conventions that drift apart. Pyganini derives those surfaces from one filesystem route tree.
- Route packages keep handlers, templates, layouts, and middleware together.
- Generation produces a deterministic Starlette router and typed URL helpers.
pyganini checkcatches stale generated files and invalid route source.- Jinja stays focused on HTML, including visible
hx-*attributes. - Your application owns its server, middleware, sessions, persistence, and deployment policy.
Pyganini supports CPython 3.13 and newer.
Try a Complete App First
The full-feature example is a contact directory with nested layouts, dynamic pages, forms, HTMX fragments, route kits, navigation, assets, browser helpers, and generated error pages.
git clone https://github.com/mobiletoly/pyganini.git
cd pyganini/examples/full_feature
uv sync --locked --all-groups --python 3.14
uv run --locked uvicorn app.main:app --host 127.0.0.1 --port 8000
Open http://127.0.0.1:8000.
Quick Start
This small application serves one page through an application-owned Starlette host. Install Pyganini, Starlette, and Uvicorn in a new directory:
mkdir hello-pyganini
cd hello-pyganini
mkdir -p app/routes
touch app/__init__.py app/routes/__init__.py
Create pyproject.toml:
[project]
name = "hello-pyganini"
version = "0.0.0"
requires-python = ">=3.13"
dependencies = [
"pyganini==0.1.0",
"starlette>=1.6.0,<1.7",
"uvicorn>=0.52.4,<0.53",
]
[tool.pyganini]
Add the root route in app/routes/route.py:
from pyganini import route
from .handlers import page
Route = route(page=page, template="page.jinja")
Return a page from app/routes/handlers.py:
from pyganini import Page, PageMetadata
from starlette.requests import Request
from app._pyganini.urls import urls
def page(_: Request) -> Page:
return Page(
context={"home_url": urls.root.path},
metadata=PageMetadata(title="Hello from Pyganini"),
layout={"product": "Pyganini"},
)
Add app/routes/page.jinja:
<main>
<h1>Hello from Pyganini</h1>
<p>The generated URL for this page is <code>{{ home_url }}</code>.</p>
</main>
Create an empty layout marker at app/routes/layout.py, then add
app/routes/layout.jinja:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ metadata.title }}</title>
</head>
<body>
<header>{{ layout.product }}</header>
{{ child }}
</body>
</html>
Mount the generated router from app/main.py:
from starlette.applications import Starlette
from starlette.routing import Mount
from app._pyganini.asgi import router
app = Starlette(routes=[Mount("/", app=router)])
Generate the router and URL helpers before importing app.main, then check and
run the application:
uv sync
uv run pyganini generate
uv run pyganini check
uv run uvicorn app.main:app --host 127.0.0.1 --port 8000
Open http://127.0.0.1:8000. Continue with Getting Started to add a dynamic user page and inspect the generated route graph.
What Pyganini Gives You
Build pages from the filesystem
Each package below app/routes maps to a URL segment. A route declaration
connects a handler to its adjacent Jinja template. layout.py and
layout.jinja mark layout ownership for that route subtree.
Generate checked application interfaces
pyganini generate writes the router and typed URL tree below
app/_pyganini. pyganini check rebuilds the expected bytes in memory and
reports missing, stale, or invalid output without changing files.
Keep HTMX in the template
Templates use ordinary hx-get, hx-post, hx-target, and hx-swap
attributes. Generated URL values remove string paths without hiding the
browser interaction.
Inspect before you run
The CLI can list routes, show layout chains, explain one request path, find template route references, and report render units from source. These commands do not import application modules.
Own the application boundary
Pyganini generates a Starlette router. Your application decides whether to serve it through Starlette or mount it in FastAPI. It also owns authentication, sessions, storage, middleware, static serving, and process management.
How Pyganini Apps Are Shaped
A larger application can grow from the same route tree:
pyproject.toml
app/
__init__.py
main.py
routes/
__init__.py
route.py
handlers.py
layout.py
layout.jinja
page.jinja
users/
__init__.py
route.py
handlers.py
page.jinja
by_user_id/
__init__.py
route.py
handlers.py
page.jinja
shared/
reports/
__init__.py
page.jinja
_pyganini/
.pyganini-root
__init__.py
asgi.py
urls.py
Static package names map underscores to URL hyphens. A package named
by_user_id creates {user_id} in the path. Each route.py contains one
static Route declaration:
from pyganini import action, fragment_route, route
from .handlers import fragment_table, page, post_save
Route = route(
page=page,
template="page.jinja",
fragments=(fragment_route("/table", fragment_table, template="table.jinja"),),
actions=(action("POST", "/save", post_save, template="form.jinja"),),
)
Pyganini reads declarations without importing application code. Generation writes one live route graph for dispatch, URL helpers, checks, and inspection. Application code imports generated products after generation:
from app._pyganini.asgi import router
from app._pyganini.urls import urls
user_path = urls.users.by_user_id("42").path
Pages inherit layouts from their route ancestry. Fragments render without page layouts. Route kits let several live route owners share typed handlers and templates while each owner keeps its path and URL surface. Mounted route subtrees provide filesystem-shaped reuse with explicit selection.
Run source inspection from the application root:
uv run pyganini routes list
uv run pyganini routes layouts
uv run pyganini routes explain /users/42
uv run pyganini routes refs
uv run pyganini routes render-units
The generated router can serve on its own or mount below Starlette or FastAPI. Pyganini runs asynchronous handlers on the event loop and synchronous handlers in an AnyIO worker thread.
More Examples
- Full-feature contact directory combines the public route, rendering, navigation, HTMX, asset, SSE, browser, and error surfaces.
- React island mounts one application-owned React editor inside a server-rendered page.
- Svelte island mounts the same bounded interaction with Svelte while keeping the server route model unchanged.
Documentation
- Getting Started builds a two-page application with a shared layout, a dynamic route, generated URL helpers, route inspection, and an application-owned Starlette host.
- Installation covers Python support, package installation, runtime dependencies, and host ownership.
- Project layout covers the application marker, fixed source and generated paths, and file ownership.
- Command line covers generation, non-writing checks, root selection, output, diagnostics, and exit codes.
- Assets covers final build projection, fingerprinted output, generated lookup, commands, cleanup, host composition, and development flow.
- Bounded client islands covers explicit application-owned roots, lifecycle cleanup, JSON policy, and build ownership.
- Route inspection covers source-only declaration, implementation, navigation, mounted-selection, layout, and error-render evidence.
- Routes covers live route packages, declarations, path mapping, layouts, and static validation.
- Route middleware covers live route-tree middleware, ordering, mounted-owner scope, diagnostics, and host boundaries.
- Error composition covers the optional generated-route error callback, response contracts, and host ownership.
- Route kits covers shared implementations, typed kit values, owner-specific URLs, shared templates, and invocation boundaries.
- Mounted route subtrees covers reusable filesystem-shaped source trees, exact selection, owner-bound URLs, and mounted layout order.
- Navigation covers request-local trails, dynamic labels, typed destinations, alternate keys, Back, return safety, and mounted ownership.
- Generated URL interfaces covers typed route-shaped paths, dynamic values, explicit base paths, and query and request-context boundaries.
- Rendering and responses covers Jinja environments, pages, layouts, fragments, render values, and direct Starlette responses.
- Server-Sent Event helpers covers UTF-8 wire framing, comments, reconnection headers, Starlette streaming, and application-owned operational policy.
- Browser helpers covers the optional fixed-resource app, named SSE event swaps, template overlay delivery, explicit mounting, and cache revalidation.
- Template inspection covers typed development modes, render markers, explicit Jinja call blocks, and the optional browser overlay.
- HTMX and async forms covers visible attributes, generated URL values, header helpers, Starlette form parsing, uploads, and validation.
- CSRF helpers covers the optional signed-cookie helper, visible form and HTMX values, validation ordering, and application-owned policy.
Contributing
The repository uses uv 0.12.3 and Python 3.14 for development while retaining Python 3.13 compatibility:
uv python install 3.13 3.14
uv lock --check
uv sync --locked --all-groups --python 3.14
uv run --locked --python 3.14 ruff format --check .
uv run --locked --python 3.14 ruff check .
uv run --locked --python 3.14 mypy src/pyganini
uv run --locked --python 3.14 pyright src/pyganini
uv run --locked --python 3.14 pytest -q tests/test_csrf_typing.py tests/test_sse_typing.py tests/test_assets.py::test_generated_asset_consumers_are_checked_by_both_type_checkers
uv run --locked --python 3.14 pytest
uv sync --locked --all-groups --python 3.13
uv run --locked --python 3.13 pytest
Maintainer architecture records live under docs/arch. The
release guide records candidate and trusted-publisher
steps.
License
Pyganini is licensed under the Apache License 2.0.
Metadata
Release files for pyganini 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyganini-0.1.0.tar.gz | 139.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyganini-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 289.8 kB
Release files / pyganini-0.1.0.tar.gz
| Download URL | pyganini-0.1.0.tar.gz |
|---|---|
| Size | 139.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eb4ad2f7b97a98c009ec1f5533e1515b1de4092cb188bc09db1e5f69e7b455e5
|
|
BLAKE2b-256 checksum How to use checksums |
d4d7cd151aef0bbad0a5927aa138c341c2c5bbb08cc2cc1de5ed3c280f9796b3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.
Transparency logRelease files / pyganini-0.1.0-py3-none-any.whl
| Download URL | pyganini-0.1.0-py3-none-any.whl |
|---|---|
| Size | 150.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4902394370523991422428f69aadfab112fed1d1c48851f59560b074681261c8
|
|
BLAKE2b-256 checksum How to use checksums |
3dea4406c11899c0f3dbf82008f55c4ec356e2b19fedea20729a483978c19b45
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.
Transparency log