# {{ service_name }}

Generated by `tonin service new --lang python`. Read `AGENTS.md` first if
you're a coding agent; this README is for humans landing on the repo.

## Layout

```
{{ service_name }}/
├── proto/                  (1) wire contract — language-agnostic
├── server/                 (2) the service (Python + tonin + grpc.aio)
├── client-python/          (3) typed Python SDK — peers depend on this
├── tonin.toml              service config (db, cache, mesh, etc.)
├── Dockerfile              builds server/
└── k8s/                    rendered manifests (regenerate via `tonin k8s generate`)
```

Each top-level folder is **independently buildable** with its own
toolchain. A peer service that only wants the typed client installs
`client-python/` and gets it without dragging in the server framework.

## How to develop

```sh
# 1. Generate stubs from proto (idempotent; run once after `git clone`
#    and whenever proto/ changes)
(cd server          && bash codegen.sh)
(cd client-python   && bash codegen.sh)

# 2. Server
cd server
uv sync                     # creates .venv and installs deps
uv run pytest               # runs the e2e test suite (real grpc.aio)
uv run python -m {{ service_name_snake }}_server.main   # start the gRPC server on :50051

# 3. Client (for tests or as a sanity check)
cd ../client-python
uv sync
uv run python -c "from {{ service_name_snake }}_client import {{ ServiceCamel }}Stub, HelloRequest"
```

Async by default — every I/O method on the framework is `async def`.
asyncpg replaces psycopg2 for Postgres, redis.asyncio replaces sync
`redis.Redis`, httpx.AsyncClient replaces requests. Don't add sync
calls into handlers.

## How a peer service depends on this client

```toml
# In some-other-service/pyproject.toml
[project]
dependencies = [
  "{{ service_name }}-client @ git+https://github.com/<org>/{{ service_name }}#subdirectory=client-python",
]
```

`pip` and `uv` both walk the subdirectory and install just the client
package — the server framework is not pulled in.

## State (DB + cache + storage)

The scaffold's `main.py` opens connections lazily from env at startup:

| Env var          | Activates                                                |
|------------------|----------------------------------------------------------|
| `DATABASE_URL`   | `state.pg` returns a live `asyncpg.Pool`                 |
| `REDIS_URL`      | `state.redis` returns a live `redis.asyncio.Redis`       |
| `STORAGE_*`      | only if scaffolded with `--with-storage` (opendal-backed)|

Neither set → state stays empty, `state.require_pg()` / `require_redis()`
raise a clear error. Boot probe: misconfigured-but-reachable deps fail
fast at startup, not at handler time. Pool tuning: `TONIN_PG_MAX_CONNECTIONS`.

## Auth

Auth is wired by default. The scaffold uses `tonin.auth.verifier()` —
which builds a `JwtValidator` from env (`TONIN_AUTH_ISSUER`,
`TONIN_AUTH_AUDIENCE`, `TONIN_AUTH_JWKS_URL`) and falls back to insecure-dev
mode when those aren't set. To swap to a different verifier (Okta SDK,
API keys, etc.), edit `server/src/{{ service_name_snake }}_server/auth.py`.
