Skip to content

Add Tenchi to an existing project

Use this path when you already have a Python project and want to add a Tenchi API without starting from the generated application. The result is one working JSON operation, a direct use-case test, OpenAPI and health routes, and the same complete validation command used by generated projects.

Install Tenchi and development tools

Tenchi requires Python 3.12 or newer. From the project root, add the runtime and local development dependencies:

uv add tenchi
uv add --dev uvicorn ruff pyright pytest pytest-asyncio

Create this initial structure. Empty __init__.py files make each package explicit:

app/
  __init__.py
  features/
    __init__.py
    greetings/
      __init__.py
      contracts.py
      routes.py
      schemas.py
      use_cases/
        __init__.py
        greet.py
      tests/
        __init__.py
        test_greet.py
  server/
    __init__.py
    asgi.py
    context.py
    routes.py
    runtime.py

These are the modules used in this example. The required server modules are asgi.py, context.py, and routes.py; runtime.py keeps this example's shared resource wiring in one place. Background jobs, operational tasks, application tools, and evaluations each add one module under app/server/ when the application needs them; until then tenchi map, tenchi check, and tenchi verify treat those boundaries as not configured. Preflight checks add app/server/preflight.py when you adopt that deployment gate.

Declare the boundary

Define the validated query and response in app/features/greetings/schemas.py:

from pydantic import BaseModel, Field


class GreetQuery(BaseModel):
    name: str = Field(min_length=1)


class Greeting(BaseModel):
    message: str

Describe the HTTP operation in app/features/greetings/contracts.py:

from tenchi.contracts import contract

from .schemas import Greeting, GreetQuery

greet_contract = contract(
    method="GET",
    path="/greet",
    query=GreetQuery,
    response=Greeting,
    name="greet",
    summary="Greet someone",
    public=True,
)

public=True gives a future authentication hook an explicit exemption signal. It does not change access by itself.

Implement and bind the use case

Create the application context in app/server/context.py. This first operation has no external dependencies, so the context is empty:

from dataclasses import dataclass


@dataclass(frozen=True, slots=True)
class AppContext:
    pass

Implement the behavior in app/features/greetings/use_cases/greet.py:

from app.server.context import AppContext

from ..schemas import Greeting, GreetQuery


async def greet(query: GreetQuery, context: AppContext) -> Greeting:
    return Greeting(message=f"Hello, {query.name}!")

Bind the contract to the use case in app/features/greetings/routes.py:

from tenchi.routes import route, route_group

from .contracts import greet_contract
from .use_cases.greet import greet

routes = route_group(route(greet_contract, greet))

route() checks the query and return annotations immediately. Importing this module fails if the use-case signature no longer matches the contract.

Compose the ASGI application

Compose the application and documentation routes in app/server/routes.py:

from tenchi.health import health_route
from tenchi.openapi import openapi_route, swagger_ui_route
from tenchi.routes import route_group

from app.features.greetings.routes import routes as greeting_routes

OPENAPI_TITLE = "Existing app"
OPENAPI_VERSION = "0.1.0"
OPENAPI_DESCRIPTION = "Greeting API"

api_routes = route_group(greeting_routes)

routes = route_group(
    api_routes,
    openapi_route(
        api_routes,
        title=OPENAPI_TITLE,
        version=OPENAPI_VERSION,
        description=OPENAPI_DESCRIPTION,
    ),
    swagger_ui_route(title=f"{OPENAPI_TITLE} documentation"),
    health_route(),
)

Put entrypoint-neutral context wiring in app/server/runtime.py:

from app.server.context import AppContext


def create_context() -> AppContext:
    return AppContext()

Expose the Starlette application from app/server/asgi.py:

from tenchi.server import create_app

from app.server.routes import routes
from app.server.runtime import create_context

app = create_app(routes=routes, context_factory=create_context)

When the application needs a database or SDK client, replace the direct context factory with the lifespan and request-scope pattern from routes and server.

Add a direct behavior test

Create app/features/greetings/tests/test_greet.py:

from app.features.greetings.schemas import GreetQuery
from app.features.greetings.use_cases.greet import greet
from app.server.context import AppContext


async def test_greet() -> None:
    result = await greet(GreetQuery(name="Tenchi"), AppContext())

    assert result.message == "Hello, Tenchi!"

Merge these settings into pyproject.toml so the aggregate check knows where to find the application and tests:

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["app"]
pythonpath = ["."]

[tool.ruff]
line-length = 88

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]

[tool.pyright]
include = ["app"]
typeCheckingMode = "strict"
pythonVersion = "3.12"

Keep any existing test and source paths when merging these tables into the project's configuration.

Declare the verification policy

Create tenchi.toml at the application root:

schema_version = 1

[verify]
check = true
architecture = true
openapi = true

This makes the repository's definition of done explicit for humans, agents, and CI. Stages you omit, such as jobs, tools, and evaluations, are recorded as not configured until you add them. A project without this file receives the same behavior from Tenchi's built-in policy, which requires those three stages plus any optional boundary whose composition module exists, so adding the file is a metadata-only adoption. Later verification compares it with the selected Git baseline, retains any stronger historical requirement for the current run, and rejects a removed or weakened policy.

Create the baseline and validate the app

Before running verification, make expected local artifacts invisible to Git. Tenchi's source digest deliberately includes every nonignored untracked path, so a cache or development database created during a check otherwise invalidates the receipt. Keep source, snapshots, lockfiles, and configuration visible; add only reproducible or environment-local artifacts to .gitignore. For example:

__pycache__/
.venv/
.pytest_cache/
.ruff_cache/
.coverage
.coverage.*
*.log
*.db
*.db-shm
*.db-wal

Write the first canonical OpenAPI snapshot, then run the local project gates:

uv run tenchi openapi --write openapi.json
uv run tenchi check
uv run tenchi map

The snapshot establishes the application's compatibility baseline. Commit it before using tenchi verify: a ref that predates openapi.json cannot support a historical compatibility claim, so verify correctly fails when a required baseline is absent. On the next change, compare with the committed baseline:

uv run tenchi verify --base-ref origin/main

Use tenchi check as the CI gate on the adoption change. After the baseline commit reaches the target branch, run tenchi verify with the pull-request base ref for later changes so CI checks both exact snapshot drift and historical compatibility.

When you later add a durable job, application tool, or evaluation, add its module, write its snapshot, and set the matching [verify] stage to true in the same change. tenchi verify treats the missing historical snapshot as a first adoption when the module did not exist at the baseline. Background jobs, application tools, and AI evaluations each describe that step.

Start the development server after the checks pass:

uv run tenchi dev

Open http://127.0.0.1:8000/docs, or call the operation directly:

curl "http://127.0.0.1:8000/greet?name=Tenchi"

The response is {"message":"Hello, Tenchi!"}. Continue with app architecture before adding ports, adapters, policies, and authenticated operations.