Metadata-Version: 2.5
Name: py2axum
Version: 0.3.1
Summary: Ahead-of-time compiler from FastAPI + SQLAlchemy + Pydantic applications to Rust (axum), with response-level conformance testing
Project-URL: Homepage, https://github.com/larrymotalavigne/py2axum
Project-URL: Issues, https://github.com/larrymotalavigne/py2axum/issues
Author: Larry Motalavigne
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: axum,compiler,fastapi,performance,pydantic,rust,sqlalchemy,transpiler
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Rust
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Software Development :: Compilers
Requires-Python: >=3.12
Provides-Extra: conformance
Requires-Dist: aiohttp<3.15,>=3.13.0; extra == 'conformance'
Requires-Dist: asyncpg<0.32,>=0.31.0; extra == 'conformance'
Requires-Dist: fastapi<0.143,>=0.137.0; extra == 'conformance'
Requires-Dist: httpx<0.29,>=0.28.1; extra == 'conformance'
Requires-Dist: mcp<2.3,>=2.2.0; extra == 'conformance'
Requires-Dist: psycopg[binary]<3.4,>=3.2.12; extra == 'conformance'
Requires-Dist: pydantic-core<2.47,>=2.41.1; extra == 'conformance'
Requires-Dist: pydantic-settings<2.16,>=2.11.0; extra == 'conformance'
Requires-Dist: pydantic[email]<2.14,>=2.12.0; extra == 'conformance'
Requires-Dist: sqlalchemy[asyncio]<2.2,>=2.0.44; extra == 'conformance'
Requires-Dist: starlette<1.8,>=1.0.0; extra == 'conformance'
Requires-Dist: uvicorn[standard]>=0.30; extra == 'conformance'
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: difftest
Requires-Dist: httpx>=0.27; extra == 'difftest'
Requires-Dist: hypothesis>=6.140; extra == 'difftest'
Requires-Dist: psycopg[binary]>=3.2; extra == 'difftest'
Requires-Dist: schemathesis>=4.29; extra == 'difftest'
Description-Content-Type: text/markdown

# py2axum

**Compile a FastAPI + SQLAlchemy + Pydantic application ahead of time into a Rust ([axum](https://github.com/tokio-rs/axum)) binary — and prove, response by response, that it behaves like the Python app.**

You keep writing, testing and debugging your API in idiomatic Python. `py2axum` reads the source (it never imports or runs it), compiles every route and the project code it reaches into Rust over a small runtime that reproduces CPython, Pydantic v2, SQLAlchemy 2.0 (async) and Starlette semantics, and produces a self-contained binary for production.

```
app/ (FastAPI)  ──py2axum──▶  Rust crate (axum + tokio + sqlx)  ──cargo──▶  one static-ish binary
```

On the bundled example ([`examples/notes`](examples/notes)), same machine (Apple M1 Pro), same PostgreSQL, 4 uvicorn workers vs 4 tokio threads, 64 connections:

| Endpoint | FastAPI + uvicorn | py2axum binary | |
|---|---:|---:|---:|
| `GET /notes/1` (1 SELECT + relationship) | 4 276 req/s, p99 40 ms | **17 674 req/s, p99 4.1 ms** | ×4.1 |
| `GET /notes?limit=20` | 3 149 req/s, p99 47 ms | **11 822 req/s, p99 6.9 ms** | ×3.8 |
| `GET /health` | 76 712 req/s | **130 105 req/s** | ×1.7 |

The binary used 19 MB of RSS under load. Gains depend on how much of your request time is Python (validation, serialization, ORM) versus the database: measure your own app with `bench/`.

> **Status: alpha.** py2axum translates a *closed, growing subset* of Python and of a list of libraries. Anything outside it is **refused at compile time with `file:line`** — never approximated silently. `py2axum check` tells you, route by route, what translates and what blocks.

## Why

- **Equivalence you can check.** Every supported behaviour is verified by differential testing: the same requests are sent to FastAPI and to the binary, and status, headers, content type and bodies must match **byte for byte** (JSON key order, Pydantic 422 error details and messages included). The project's own suites run 350+ such requests.
- **No rewrite, no fork.** The Python app stays the source of truth: unit tests, debugging, local development and the OpenAPI docs keep working as before.
- **Incremental.** Routes or ASGI apps that cannot be translated can stay in Python (`--python-side PATH`, or `--python-side auto` to move every route that does not translate): the binary relays them to a Python process, so one deployment serves both.

## Quick start

```bash
pip install git+https://github.com/larrymotalavigne/py2axum     # Python ≥ 3.12, no dependencies (PyPI soon)

# what would translate, route by route (nothing is generated)
py2axum check examples/notes/app --root examples/notes

# generate the Rust crate, build it, run it
py2axum examples/notes/app --root examples/notes --backend dyn -o build/notes --name notes
cargo build --release --manifest-path build/notes/Cargo.toml
DATABASE_URL=postgresql://postgres@localhost/notes PORT=8080 ./build/notes/target/release/notes
```

The binary reads `DATABASE_URL`, `HOST` (`0.0.0.0`), `PORT` (`8080`), plus whatever your code reads from the environment (`os.environ`, pydantic-settings). It does not create tables: run your migrations as usual.

Run the transpiler on a Python **at least as recent** as the one your app targets (the parser only knows its own syntax). Library behaviours that changed between versions (Starlette's CORS, Pydantic error URLs, CPython messages) follow the versions pinned in your project's `uv.lock`; versions outside the [tested ranges](docs/supported.md#supported-versions) (FastAPI 0.137–0.142, Starlette 1.0–1.7, Pydantic 2.12–2.13, SQLAlchemy 2.0.44–2.1, Python 3.12–3.14...) are refused rather than translated.

## Check an application

`py2axum check` runs the whole translation without writing a crate and prints, for each route, whether it is
**native** (compiled into the binary), **python-side** (left to a Python process with `--python-side PATH`, or
moved by `--python-side auto`) or **refused**, with the reason at `file:line`, then the blocking constructs
ranked by how many routes each one blocks:

```
$ py2axum check proj --root .
py2axum check proj (dyn backend)

refused      GET /api/items/me/info    proj/views/items.py:22
             └ proj/deps.py:5: library call `jwt.decode()` is not supported (not in the py2axum library map)
refused      GET /api/items/stamp/now  proj/views/items.py:18
             └ proj/schemas.py:10: unsupported type annotation `SecretStr` (pydantic.SecretStr)
native       GET /api/items/{item_id}  proj/views/items.py:11
native       GET /users/count          proj/views/users.py:7
...
Blockers (routes touched, routes for which it is the only blocker):
     2    2  library jwt  (proj/deps.py:5)
     2    2  type SecretStr  (proj/schemas.py:10)

2/6 routes native (33.3 %), 0 python-side, 4 refused — generation would fail
hint: --python-side auto leaves the refused routes to a Python process next to the binary
```

Errors that concern the whole application (an unsupported middleware, a library version outside the
[tested ranges](docs/supported.md#supported-versions)) are listed first: they refuse generation whatever the
routes. It accepts the generation options (`--root`, `--backend`, `--python-side`), so the answer is the one
generation would give.

In CI, `check` exits with 1 when generation would fail; `--fail-under PCT` also fails below a share of
native routes, and `--json` gives the same data to scripts:

```bash
py2axum check api --root . --python-side auto --fail-under 80
py2axum check api --root . --json > coverage.json
```

`--report coverage.md` remains for a detailed Markdown/JSON report (internal labels, greedy order of fixes).

## In a Dockerfile

```dockerfile
FROM python:3.13-slim AS transpile
RUN pip install --no-cache-dir git+https://github.com/larrymotalavigne/py2axum
WORKDIR /src
COPY . .
RUN py2axum app --root . --backend dyn -o /crate --name api

FROM rust:1-bookworm AS build
WORKDIR /crate
COPY --from=transpile /crate .
RUN cargo build --release

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=build /crate/target/release/api /usr/local/bin/api
EXPOSE 8080
CMD ["api"]
```

See [docs/docker.md](docs/docker.md) for caching the cargo build, the hybrid setup (binary + Python sidecar for `--python-side` routes) and a docker-compose file.

## Verify your own app

```bash
# reference and candidate on the same database, reset between runs
python tests/conformance.py http://127.0.0.1:8000 http://127.0.0.1:8080 --scenario myapp
```

A scenario is a list of requests plus a `reset()` function ([docs/conformance.md](docs/conformance.md), [`tests/scenarios/notes.py`](tests/scenarios/notes.py)). The harness prints every difference with both responses.

## What is supported

A summary — the full list, with every documented difference from CPython, is in [docs/supported.md](docs/supported.md).

- **FastAPI / Starlette**: routes and routers (prefixes, factories, conditional registration), path/query/header/body/form/file parameters with FastAPI's exact 422 errors, dependencies (`Depends`, `yield`, per-request cache, `Annotated` aliases, OAuth2/HTTPBearer), `response_model` filtering, returned `Response` objects, `StreamingResponse`/SSE, `BackgroundTasks`, exception handlers, the middleware stack (`CORSMiddleware`, `GZipMiddleware`, `BaseHTTPMiddleware`, `@app.middleware("http")`), Starlette routing (405, trailing-slash redirects).
- **Pydantic v2**: lax-mode validation with pydantic-core's error types and messages, unions, enums, `Field` constraints and aliases, validators (`field_validator`/`model_validator`, before/after), computed defaults, `model_dump`/`model_validate`/`model_copy`, `TypeAdapter`, frozen models, URL and email types, `BaseSettings`.
- **SQLAlchemy 2.0 async**: declarative models (composite keys, `Identity`, JSON/JSONB, enums, `Numeric`, `TypeDecorator`), sessions (identity map, autoflush, `expire_on_commit`, savepoints, refresh, rollback), relationships and loader strategies, `select`/`update`/`delete`/`insert ... on conflict`, joins, aliases, subqueries, aggregates, `case`, `text()`.
- **Python**: functions, closures, decorators, classes (with `__eq__`/`__str__`/`__aenter__`...), dataclasses, exceptions, comprehensions, generators, `match`, coroutines and `asyncio` (`gather`, tasks, locks), `threading`, `pickle` (CPython-compatible bytes), `datetime`/`decimal`/`uuid`/`re`/`json`/`csv`/`base64`/`hashlib`/`hmac`/`math`/`urllib.parse`...
- **WebSockets**: `@app.websocket` / `@router.websocket` with Starlette's `WebSocket` API (dyn backend; see docs/supported.md).
- **Libraries**: httpx and aiohttp clients, redis.asyncio, python-jose (HMAC), bcrypt, pyotp, itsdangerous, cryptography's Fernet, jinja2 + `email` + aiosmtplib, pywebpush, google-auth id tokens, alembic config, psutil.

Not translated (stay in Python with `--python-side`, or refused): `lifespan`, other libraries, arbitrary `eval`/reflection, C extensions.

## How it works

1. **Frontend** — indexes the package's modules and imports statically (`ast`), finds the FastAPI app(s), routers, routes, models and schemas.
2. **Backends** — a *typed* backend emits idiomatic statically-typed Rust for simple CRUD handlers; the *dyn* backend (the general one) compiles every reachable project function to Rust over a dynamic value type with CPython semantics. Library calls go through a closed map (`py2axum/libmap.py`): anything unlisted is refused.
3. **Runtime** — `py2axum/runtime/dynrt/` (Rust, copied into each generated crate): values and operators, Pydantic validation and serialization, the SQLAlchemy session and SQL compiler (on sqlx/PostgreSQL), Starlette's middleware and routing, and the supported libraries.

Details: [docs/how-it-works.md](docs/how-it-works.md).

**How is this different from RustPython?** RustPython is a Python interpreter written in Rust; py2axum is a compiler for one framework stack, with no interpreter at run time. A FastAPI app cannot run on RustPython today (pydantic-core is a compiled extension) and would not be faster there. See [the comparison](docs/how-it-works.md#6-how-it-differs-from-rustpython).

## Project layout

```
py2axum/            the transpiler (Python, stdlib only) and the Rust runtime it embeds
examples/notes/     a small API + Dockerfile, translated in full
fixtures/           reference apps exercising the supported subset (used by the test suites)
tests/              conformance harness and scenarios, rejection tests
bench/              throughput / latency / memory benchmarks
docs/               documentation
```

## Contributing

Contributions are welcome — especially failing constructs from real applications. See [CONTRIBUTING.md](CONTRIBUTING.md). The rule that keeps the project honest: every new behaviour comes with a conformance case comparing it to the Python reference, and every unsupported case is refused with a test.

## License

Apache License 2.0 — see [LICENSE](LICENSE).
