A contract-first, Python-native framework for building REST APIs around use cases, ports, and explicit wiring.
Project description
Tenchi
Tenchi is a small, contract-first Python framework for building typed HTTP APIs. Contracts define the boundary, plain async functions implement use cases, and frozen dataclasses carry explicitly wired dependencies.
Tenchi uses Pydantic for validation, Starlette for ASGI, and httpx for its typed client. It requires Python 3.12 or newer and is currently pre-1.0.
Read the one-page guide for examples of contracts, use cases, application wiring, errors, the typed client, workers, pagination, testing, and the CLI.
Quick start
Create and run a working application:
uvx tenchi new my_app
cd my_app
uv sync
uv run tenchi dev
The generated app includes a todos feature, an in-memory adapter, tests, and explicit server wiring. With the server running:
curl -X POST http://127.0.0.1:8000/todos \
-H 'content-type: application/json' \
-d '{"title": "Buy milk"}'
To add Tenchi to an existing project instead:
uv add tenchi
How it works
A contract declares the HTTP boundary:
class CreatedTodoHeaders(BaseModel):
location: str = Field(alias="Location")
create_todo_contract = contract(
method="POST",
path="/todos",
request=CreateTodo,
response=Todo,
response_headers=CreatedTodoHeaders,
status=201,
)
A use case is an ordinary async function whose dependencies come from the app context:
async def create_todo(request: CreateTodo, context: AppContext) -> Todo:
return await context.todos.create(title=request.title)
A route binds them together. Tenchi immediately checks that every boundary parameter and the return annotation exactly match the contract, so invalid wiring fails during application composition rather than on a request:
def create_todo_headers(todo: Todo) -> CreatedTodoHeaders:
return CreatedTodoHeaders(Location=f"/todos/{todo.id}")
routes = route_group(
route(
create_todo_contract,
create_todo,
response_headers=create_todo_headers,
),
)
The synchronous response-header projector keeps HTTP metadata at the route
boundary while the use case continues to return only domain data. Tenchi
validates and serializes those headers before the request scope commits. The
typed client validates them on every call; use call_with_response() when you
also want the typed headers and underlying httpx response.
Applications use this structure:
app/
features/<feature>/ # contracts, schemas, ports, routes, use cases
shared/ # shared errors and domain concepts
infra/ # concrete port implementations
server/ # context, hooks, route composition, ASGI app
tests/ # HTTP integration tests
The main pieces are:
- Pydantic validation for request bodies, path parameters, query parameters,
request and successful response headers, and response bodies; field aliases
are the names used on the wire and in OpenAPI, and nullable request types can
send JSON
nullexplicitly. typing.Protocolports and explicit dependency wiring instead of a DI container.- Declared application errors with a stable JSON envelope.
- A named exception hierarchy that distinguishes configuration mistakes from runtime application and transport failures.
- A contract-driven async client and OpenAPI 3.1 generation.
- Lifespan resources, request-scoped contexts, authentication hooks, middleware, request deadlines, outcome observers, pagination, health checks, and in-process testing helpers.
For endpoints with more than one successful status, declare named outcomes and
select one in a synchronous presenter. The same mechanism is Tenchi's
controlled HTTP escape hatch: a passthrough outcome may return a Starlette
StreamingResponse, FileResponse, or redirect while its status, media type,
media-type parameters, and headers remain contract-owned. No-body outcomes
accept only a concrete response with an empty materialized body; streaming
outcomes must declare their body type. The typed client reports the selected
outcome on ClientResponse.success and validates its declared body and headers.
from tenchi.responses import PresentedResponse, present, success
created = success(name="created", status=201, response=Todo)
existing = success(name="existing", status=200, response=Todo)
put_todo_contract = contract(
method="PUT",
path="/todos",
request=CreateTodo,
response=Todo,
successes=(created, existing),
timeout=5.0,
)
def present_put(result: PutTodoResult) -> PresentedResponse:
outcome = created if result.created else existing
return present(outcome, body=result.todo)
route(put_todo_contract, put_todo, present=present_put)
timeout= cooperatively cancels overdue work, lets request-scope cleanup and
rollback finish, then returns the framework's 504 even if application code
catches the injected cancellation. create_app(observers=...) delivers an
immutable RequestOutcome, including a read-only header mapping, after each
matched route has finalized. Observer failures are logged and never change the
response.
See examples/todos for the small teaching app and
examples/taskboard for a larger application with
authentication, authorization, SQLite transactions, optimistic concurrency
through ETag / If-Match, idempotent task creation, multiple successful
outcomes, request observation, deadlines, and background work.
CLI
tenchi new my_app
tenchi make feature notes
tenchi make use-case notes create_note
tenchi routes
tenchi openapi
tenchi doctor
tenchi dev
Run tenchi <command> --help for command options.
Development
uv sync
uv run pytest
uv run ruff format --check .
uv run ruff check .
uv run pyright
Tenchi is licensed under the MIT License.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tenchi-0.7.0.tar.gz.
File metadata
- Download URL: tenchi-0.7.0.tar.gz
- Upload date:
- Size: 215.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6bc7a4d50b000ef75d5417e6c17aba3d7c4559360dd5d5681243f0cb3b5e3992
|
|
| MD5 |
300ff8ea5e575b65b5fb1190803eae79
|
|
| BLAKE2b-256 |
889a037c5015454086aa6db4d7dc5210493679d82855067c5157fe058e2ec8ae
|
File details
Details for the file tenchi-0.7.0-py3-none-any.whl.
File metadata
- Download URL: tenchi-0.7.0-py3-none-any.whl
- Upload date:
- Size: 66.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
951182a0d76d449499a3e353bdb8b39cd033315bf207df00ed66040f69ae0217
|
|
| MD5 |
56475923162a6a6d68dd726db1362a73
|
|
| BLAKE2b-256 |
761e8563ec0350902c9eaf078af4e04546173a5b5fcba126beb3ac99a9309dd5
|