Tenchi
Tenchi is a small, contract-first Python framework for typed JSON APIs. Contracts define the HTTP boundary, plain async functions implement use cases, and frozen dataclasses carry explicitly wired dependencies.
uv add tenchi
uvx tenchi new my_app
cd my_app
uv sync
uv run tenchi dev
Contracts
A contract declares the method, path, inputs, response, status, and application errors for an endpoint.
from pydantic import BaseModel, Field
from tenchi.contracts import contract
class CreateTodo(BaseModel):
title: str = Field(min_length=1)
class Todo(BaseModel):
id: str
title: str
completed: bool
create_todo_contract = contract(
method="POST",
path="/todos",
request=CreateTodo,
response=Todo,
status=201,
)
Use cases and ports
Ports are protocols. A frozen context carries their implementations. Use cases are ordinary async functions.
from dataclasses import dataclass
from typing import Protocol
class TodoRepository(Protocol):
async def create(self, *, title: str) -> Todo: ...
async def list(self) -> list[Todo]: ...
@dataclass(frozen=True, slots=True)
class AppContext:
todos: TodoRepository
async def create_todo(request: CreateTodo, context: AppContext) -> Todo:
return await context.todos.create(title=request.title)
Application wiring
Bind contracts to use cases, compose the routes, and provide concrete dependencies at the application root.
from tenchi.health import health_route
from tenchi.openapi import openapi_route
from tenchi.routes import route, route_group
from tenchi.server import create_app
todo_routes = route_group(
route(create_todo_contract, create_todo),
)
routes = route_group(
todo_routes,
openapi_route(todo_routes, title="Todos", version="1.0.0"),
health_route(),
)
app = create_app(
routes=routes,
context_factory=lambda: AppContext(todos=repository),
)
create_app() also accepts lifespan resources, hooks,
middleware, and request-body limits.
Errors
Define stable application errors and declare them on a contract or route group. Undeclared application errors become framework-owned 500s.
from tenchi.errors import AppError, ErrorDef
todo_not_found = ErrorDef(
code="TODO_NOT_FOUND",
status=404,
message="Todo not found",
)
get_todo_contract = contract(
method="GET",
path="/todos/{todo_id}",
params=GetTodoParams,
response=Todo,
errors=(todo_not_found,),
)
async def get_todo(params: GetTodoParams, context: AppContext) -> Todo:
todo = await context.todos.get(params.todo_id)
if todo is None:
raise AppError(todo_not_found, details={"todo_id": params.todo_id})
return todo
Authentication belongs in boundary hooks. Authorization stays in use cases and pure policy functions.
Client and OpenAPI
The async client serializes inputs and validates responses using the same contract.
from tenchi.client import Client
async with Client(base_url="https://api.example.com") as client:
todo = await client.call(
create_todo_contract,
request=CreateTodo(title="Buy milk"),
)
openapi_route() serves an OpenAPI 3.1 document from the
same declarations.
Workers and scripts
execute() validates job data against a use case’s request
annotation and applies the same context scoping as the server.
from tenchi.execution import execute
await execute(
notify_member_added,
request_json=job.payload,
context=context,
)
Pagination
Subclass PageQuery for filters, return
Page[Item], and build the result with page().
from tenchi.pagination import Page, PageQuery, page
class ListTodosQuery(PageQuery):
completed: bool | None = None
async def list_todos(
query: ListTodosQuery,
context: AppContext,
) -> Page[Todo]:
items = await context.todos.list()
start = query.offset
window = items[start : start + query.limit]
return page(window, total=len(items), query=query)
Testing
open_client provides the typed client in-process;
open_http exposes raw httpx responses. Both run the app
lifespan.
from tenchi.testing import open_client, open_http
async with open_client(app) as client:
todo = await client.call(
create_todo_contract,
request=CreateTodo(title="Buy milk"),
)
async with open_http(app) as http:
response = await http.get("/health")
assert response.status_code == 200
CLI
tenchi new my_app
tenchi make feature notes
tenchi make use-case notes create_note
tenchi routes
tenchi openapi
tenchi doctor
tenchi dev