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-asyncioCreate 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.pyThese 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: strDescribe 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:
passImplement 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 = trueThis 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 mapThe 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/mainUse 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 devOpen 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.