Skip to main content

Tenchi

Tenchi is a small, contract-first Python framework for building typed backends. HTTP contracts and application tools define validated boundaries, 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 documentation for the quickstart, mental model, complete contract and runtime guides, production handbook, comparisons, and module reference.

Quick start

Create and run a working application:

uvx tenchi new my_app
cd my_app
uv sync
uv run tenchi check
uv run tenchi dev

The generated app includes a todos feature, SQLite persistence, memory-backed unit tests, Swagger UI, health and OpenAPI routes, an agent guide, and a CI compatibility gate. It also includes .mcp.json, so MCP-aware coding agents can use Tenchi's structured inspection and validation tools after uv sync. With the server running, open http://127.0.0.1:8000/docs or call it directly:

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

Add MCP support with uv add --dev "tenchi[mcp]" and follow the MCP server guide to connect a coding agent.

Add the payload-safe OpenTelemetry bridge with uv add "tenchi[otel]" opentelemetry-sdk. Tenchi records finalized request, use-case, client, and retry-attempt outcomes through the providers your application configures; see the observability guide.

Follow the existing-project guide to add the first contract, use case, ASGI application, test, and OpenAPI baseline.

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,
    request_examples={"create": CreateTodo(title="Buy milk")},
    response_examples={
        "created": Todo(id="todo_123", title="Buy milk", completed=False)
    },
)

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, jobs, tasks, tools, evaluations, use cases
  shared/               # shared errors and domain concepts
  infra/                # concrete port implementations
  server/               # context, runtime, preflight, boundary 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 null explicitly. Declared media types are enforced against wire Content-Type: mismatched requests receive a framework-owned 415 and the typed client rejects mismatched responses. Charset-qualified text contracts are encoded and decoded strictly in both directions; unsupported declared charsets fail when the contract is built.
  • typing.Protocol ports and explicit dependency wiring instead of a DI container.
  • Declared application errors with a stable JSON envelope. OpenAPI enumerates the exact codes and application/framework source for every error status and documents the framework-owned 500 available on every operation.
  • Named request and response examples are validated, serialized with real wire aliases, and checked against the generated OpenAPI schema before publication.
  • A named exception hierarchy that distinguishes configuration mistakes from runtime application and transport failures.
  • A contract-driven async client with explicit bounded retries for transport failures, declared error codes, and selected raw HTTP statuses; per-attempt outcomes; OpenAPI 3.1 generation; and an optional Swagger UI route.
  • Lifespan resources, request-scoped contexts, authentication hooks, middleware, request deadlines, HTTP and use-case outcome observers, pagination, health checks, in-process testing helpers, and an optional OpenTelemetry bridge for logical spans and bounded-cardinality metrics.
  • Infrastructure-neutral idempotency with canonical validated-input fingerprints, durable store transitions, typed result replay, scoped keys, expiration, standard conflict/in-progress errors, and a concurrency-safe memory adapter for tests.
  • Checked contract metadata identifies retry-safe unsafe HTTP operations through the required Idempotency-Key header and a machine-readable OpenAPI extension; applications still own replay storage and transaction placement.
  • Infrastructure-neutral fixed-window rate limits with atomic store decisions, weighted costs, a standard declared 429 response, and a concurrency-safe memory adapter for tests.
  • Reusable idempotency and rate-limit adapter conformance checks for persistence, identity isolation, expiration boundaries, token fencing, and concurrent admission.
  • Signed inbound webhook bindings that verify exact, size-bounded bytes before parsing, attach service identity to the request context, and fail composition when a marked webhook contract has no verifier.
  • Queue-neutral background jobs with validated producer messages, composition-time handler bindings, validated consumer results, shared use-case outcomes, and application-map nodes. Applications keep ownership of queue persistence, acknowledgement, retries, and dead letters.
  • Provider-neutral AI evaluation gates with typed cases, normalized metric thresholds, bounded lifecycle-aware execution, optional token and cost budgets, redacted reports, CLI automation, and opt-in coding-agent MCP execution. Applications keep ownership of models, prompts, judges, and datasets.

public defaults to False. Set public=True for operations that an authentication hook should exempt, then inspect the metadata in the hook:

health = contract(method="GET", path="/health", response=Health, public=True)


def authenticate(info: RequestInfo, context: AppContext) -> AppContext | None:
    if info.contract.public:
        return None
    # Authenticate and return an enriched context, or raise AppError.

When OpenAPI security schemes are configured, public operations receive an empty per-operation security requirement. health_route() and openapi_route() and swagger_ui_route() are public by default; pass public=False to protect them. The metadata itself does not authenticate requests—application hooks remain in control.

Signed webhook contracts use a separate exact-body authentication boundary:

delivery = contract(
    method="POST",
    path="/webhooks/delivery",
    request=Delivery,
    status=204,
    public=True,
    webhook=True,
)

app = create_app(
    routes=route_group(route(delivery, receive_delivery)),
    context_factory=create_context,
    webhooks=(webhook(delivery, verify_delivery),),
)

The verifier sees the immutable wire bytes and read-only headers before Pydantic parses the body. It may return an enriched context carrying verified service identity. See the signed webhook guide for HMAC verification, replay protection, and error declarations.

Application-level quotas stay in use cases and use authenticated scope:

permit = await enforce_rate_limit(
    context.rate_limits,
    namespace="tasks.create",
    scope=user.id,
    limit=5,
    window_seconds=60,
)

Exhausted windows raise the declared RATE_LIMITED error with Retry-After. Use the built-in MemoryRateLimitStore in tests and supply an atomic shared store in multi-process deployments. See rate limiting for idempotency placement, transaction choices, and edge-limit boundaries.

For endpoints with more than one successful status, declare response definitions and select one in a synchronous presenter. Their body and header types become the contract's aggregate typed-client result, so they are the only source of truth. The same mechanism is Tenchi's controlled HTTP escape hatch: a passthrough definition may return a Starlette StreamingResponse, FileResponse, or redirect while its status, media type, media-type parameters, and headers remain contract-owned. No-body definitions accept only a concrete response with an empty materialized body; streaming definitions must declare their body type. The typed client reports the selected definition on ClientResponse.definition and validates its body and headers.

from tenchi.responses import PresentedResponse, present, response

created = response(Todo, status=201)
existing = response(Todo, status=200)

put_todo_contract = contract(
    method="PUT",
    path="/todos",
    request=CreateTodo,
    responses=(created, existing),
    timeout=5.0,
)


def present_put(result: PutTodoResult) -> PresentedResponse:
    outcome = created if result.created else existing
    return present(outcome, result.todo)


route(put_todo_contract, put_todo, present=present_put)

When one status permits alternative top-level body schemas, pass them as separate positional alternatives so Pyright preserves the exact union:

flexible = response(Todo, str, status=200)  # ResponseDef[Todo | str, None]

Nested unions retain their ordinary spelling, such as response(list[Todo | str], status=200). Each response definition has one fixed object-shaped header schema; differing header shapes belong in separate definitions.

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. create_app(use_case_observers=...) and execute(use_case_observers=...), task runners, and tool runners deliver the same immutable UseCaseOutcome after context cleanup for every use case that was actually invoked. Finalized outcomes include a UTC completed_at captured before observer delivery, so delayed observers do not move the event's timestamp. Client(observers=...) delivers immutable ClientOutcome values for outbound contract calls, including transport failures, invalid responses, and cancellation, without carrying input, URL, header, body, or exception payloads.

Named operational tasks add validated input and result contracts plus application lifecycle wiring around selected use cases. Use them for operator-invoked backfills, repairs, replays, and maintenance—not scheduling or queue consumption:

repair_members_task = task(
    "projects.repair_members",
    repair_project_members,
    description="Replace malformed project member lists.",
)

Application tools expose selected use cases to AI and other machine callers without choosing a model provider or transport:

search_projects_tool = tool(
    "projects.search",
    result=list[Project],
    description="List projects owned by the authenticated user.",
    errors=(unauthorized,),
    read_only=True,
    open_world=False,
)

tools = tool_group(
    tool_handler(search_projects_tool, list_projects),
)

ToolRunner validates input before application resources open, validates the result and its serialized form before context commit, preserves only declared application errors, and masks unexpected failures. tool_manifest() returns deterministic input and output JSON Schemas plus safety metadata for a transport adapter; TOOL_MANIFEST_VERSION identifies that versioned protocol. See the application tools guide.

Install tenchi[mcp] and use create_tool_mcp_server() to publish the same group over MCP. The adapter authenticates discovery and invocation, can filter visible tools per principal, denies destructive tools without an explicit approval decision, and returns versioned structured results through the stable 2.x line of the official MCP Python SDK. Failed structured results also set MCP's standard isError flag so generic hosts do not treat them as successful tool output:

from app.server.runtime import DATABASE_PATH
from tenchi.mcp import create_tool_mcp_server

mcp = create_tool_mcp_server(
    tools=tools,
    authenticate=authenticate_mcp_request,
    runner_factory=lambda user: create_user_tool_runner(
        database_path=DATABASE_PATH,
        user=user,
    ),
)

The application still owns identity, approval policy, and use-case authorization. See Serve application tools over MCP.

Application-owned evaluations declare typed cases, normalized metric thresholds, per-case timeouts, and optional token and cost budgets. The runner uses the same lifespan and scoped-context pattern as other entrypoints while returning only scores, usage, status, durations, and stable failure codes. Budget status distinguishes measured limit overruns from usage that could not be verified. See the AI evaluations guide.

See examples/todos for a complete application with authentication, SQLite persistence, direct use-case tests, typed-client tests, and OpenAPI compatibility checks. Use the production handbook for transactions, retries, background work, observability, and deployment guidance.

See examples/fieldnotes for a cited AI reference backend. It saves owner-scoped research, indexes passages through a durable background job, exposes search and cited answers through HTTP and authenticated application tools, and gates retrieval, required evidence, and citation integrity with deterministic evaluations. The default answer provider needs no credentials; replace its feature-owned protocol adapter when connecting a model SDK.

CLI

tenchi new my_app
tenchi make feature notes --dry-run
tenchi make feature notes --json
tenchi make use-case notes create_note --dry-run
tenchi routes
tenchi map
tenchi map --feature notes --kind route,tool,use-case,port --json
tenchi jobs
tenchi jobs --diff jobs.json
tenchi jobs --diff-ref origin/main --snapshot jobs.json
tenchi jobs --check jobs.json
tenchi jobs --write jobs.json
tenchi tools
tenchi tools --diff tools.json
tenchi tools --diff-ref origin/main --snapshot tools.json
tenchi tools --check tools.json
tenchi tools --write tools.json
tenchi openapi
tenchi openapi --diff openapi.json
tenchi openapi --diff-ref origin/main --snapshot openapi.json
tenchi openapi --check openapi.json
tenchi openapi --write openapi.json
tenchi doctor --json
tenchi check
tenchi verify --base-ref origin/main --json
tenchi preflight
tenchi preflight --json
tenchi eval list
tenchi eval snapshot --diff evaluations.json
tenchi eval snapshot --diff-ref origin/main --snapshot evaluations.json
tenchi eval snapshot --check evaluations.json
tenchi eval snapshot --write evaluations.json
tenchi eval run support.answer_quality --json
tenchi task list
tenchi task run projects.repair_members --input '{"dry_run": true}'
tenchi mcp
tenchi dev

openapi --write stores canonical, key-sorted JSON. Before accepting a changed snapshot, run openapi --diff to classify changes as breaking, additive, metadata-only, or unknown. Breaking and unknown changes return a non-zero status; additive and metadata-only changes pass. Use --diff-format json for machine-readable output. --diff-ref reads the snapshot at a Git commit, which keeps a pull-request gate historical even when the branch updates its snapshot. openapi --check remains the exact drift check for tests and CI. Like check and verify, standalone openapi defaults to app.server.routes:api_routes and discovers literal OPENAPI_TITLE, OPENAPI_VERSION, OPENAPI_DESCRIPTION, and OPENAPI_SECURITY declarations from that module. Flags override those declarations for nonstandard composition. Run --diff before replacing the baseline with --write. --output and -o remain aliases for --write. For programmatic checks, import analyze_openapi_compatibility from tenchi.compatibility.

tools --write stores the registered application-tool manifest as canonical JSON. Before replacing it, run tools --diff or tools --diff-ref to classify changes to stable names, input and output schemas, declared application errors, descriptions, and safety annotations. Removed tools, narrower inputs, wider outputs, newly possible errors, and less-safe annotations fail compatibility. Use --diff-format json for a versioned result, or import analyze_tool_compatibility from tenchi.compatibility.

jobs --write stores the registered durable job-message manifest as canonical JSON without queued payloads or handler results. Run jobs --diff or jobs --diff-ref before replacing it. Removed jobs and narrower consumer input schemas fail compatibility; additive jobs, wider accepted inputs, and description-only changes can pass. Import analyze_job_compatibility from tenchi.compatibility for programmatic checks.

tenchi map combines source declarations with the composed route group into a deterministic graph of features, contracts, routes, operational tasks, background jobs, application tools, evaluations, use cases, policies, ports, adapters, context, entrypoints, and tests. Every relationship includes source evidence and a confidence level. Use --feature for a feature plus its direct cross-feature dependencies, --kind for a comma-separated node projection, and --json for the versioned result.

tenchi verify --base-ref <ref> produces one completion receipt for the finished tree. It resolves the historical ref to an immutable commit, runs tenchi check, requires an application map without diagnostics or unresolved relationships, and compares the OpenAPI, durable job-message, application-tool, and evaluation-policy boundaries with their snapshots at that commit. It never updates snapshots and exits non-zero when any evidence fails.

tenchi task list reports the runner's task names and JSON Schemas. tenchi task run validates input, owns one application lifespan and scoped context, and validates output before transactional cleanup commits. Both commands support versioned JSON results.

tenchi preflight runs the read-only, timeout-bounded observations declared by app.server.preflight:checks against the selected deployment environment. It is deliberately separate from deterministic tenchi check; use it after migrations and before traffic shifts to verify database connectivity and schema compatibility, secret-manager access, outbound dependencies, and worker readiness. Structured results expose only static names, descriptions, failure codes, statuses, and durations. See the deployment preflight guide.

tenchi eval list discovers registered suites without exposing case inputs or running providers. tenchi eval snapshot writes and compares a payload-free policy manifest containing names, case schemas, case execution order, metrics, thresholds, timeouts, and budgets. Lowered thresholds, removed or reordered cases, removed metrics, larger budgets or timeouts, and deterministic-to-model changes fail compatibility. A missing historical snapshot fails unless first adoption is explicitly authorized with --allow-missing-baseline. The manifest intentionally excludes case inputs and does not run evaluators. tenchi eval run applies the snapshotted policy to real cases and returns a payload-safe result. Evaluation execution remains separate from check and verify because it may be nondeterministic, contact external systems, and incur cost; those commands verify only the declared policy.

Generator --dry-run output lists every file without writing it. make, routes, map, jobs, tools, preflight, eval list|run, task list|run, doctor, check, and verify accept --json and return versioned results for agents and automation. tenchi check runs Ruff formatting and linting, Pyright, pytest, doctor, and the OpenAPI, job-message, application-tool, and evaluation-policy snapshot checks even when an earlier step fails; failed output is bounded and each step reports its duration. Tenchi snapshots the JSON Schema for these results and the MCP tool surface; breaking protocol changes require a new schema_version. Expected failures that occur before an operation-specific result exists still emit one versioned operation_error object with a stable operation and code. The machine-readable failure excludes application exception text and payloads, and the process retains a nonzero exit status. See the coding-agent workflow for the complete inspect, preview, edit, validate, and compatibility loop.

The tenchi mcp CLI serves the same versioned map, route, job-message, application-tool, preflight, evaluation-discovery, task-discovery, doctor, generator-preview, OpenAPI-diff, job-diff, tool-diff, evaluation-policy-diff, check, and verification results over stdio. Evaluation execution is absent unless the server starts with --allow-evaluation-runs; task execution is absent unless it starts with --allow-task-runs. Preflight remains available as a read-only tool but deliberately contacts the environment captured by the server process. Inspection and preview tools do not write application files; project-owned commands executed by check retain their normal side effects. Generated apps register the command in .mcp.json. Existing apps install the optional development dependency with uv add --dev "tenchi[mcp]". This coding-agent server is separate from the application MCP adapter described above.

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

The documentation is a separate Bun and Next.js application:

cd docs
bun install
bun run dev

Run bun run check in docs/ to lint, type-check, test, and build the static GitHub Pages export. Search data and llms.txt files are generated during the build from the registered MDX pages.

Tenchi is licensed under the MIT License.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tenchi-0.13.0.tar.gz (831.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tenchi-0.13.0-py3-none-any.whl (243.0 kB view details)

Uploaded Python 3

File details

Details for the file tenchi-0.13.0.tar.gz.

File metadata

  • Download URL: tenchi-0.13.0.tar.gz
  • Upload date:
  • Size: 831.5 kB
  • Tags: Source
  • Uploaded using 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}

File hashes

Hashes for tenchi-0.13.0.tar.gz
Algorithm Hash digest
SHA256 2794a34b8fbd9916fdc3943f76c8fad21e648f3a8ca78b7151d4049aaa7bf9fd
MD5 60fcc5c79b90ad1cf625674a7f3b33f0
BLAKE2b-256 6f87fc08928271075c68d599a18a99c9ecaec2c09355b93240767f26adf83357

See more details on using hashes here.

File details

Details for the file tenchi-0.13.0-py3-none-any.whl.

File metadata

  • Download URL: tenchi-0.13.0-py3-none-any.whl
  • Upload date:
  • Size: 243.0 kB
  • Tags: Python 3
  • Uploaded using 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}

File hashes

Hashes for tenchi-0.13.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e8fee562595ff98e84ea380ac6d89ffe72001f4d9a3edc75229f84c49187428d
MD5 25f62d1501cd48f66fdfc1722f4f862b
BLAKE2b-256 c5e6d72e29cf6ee5bf0fe34c44ff768843c3f1f2d85c2a61ffba7f9082728ad1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page