# Shuntgen Generates runnable [shunts](#artery-shunts) from the two things people actually have: a service contract, or a folder of spreadsheets. Writing a shunt by hand means copying `artery/shunts/example/` and filling in `responses.json` entry by entry. That is fine for three endpoints and untenable for eighty — and it is the wrong work anyway, because the endpoints are already described in the spec somebody handed you. ```bash # a spec you were handed python -m station.tools.shuntgen from-openapi -s api.yaml -o artery/shunts/petstore # sheets a client sent python -m station.tools.shuntgen from-tabular -s ./sheets -o artery/shunts/books python -m station.tools.shuntgen list ``` Run from `soleprint/`. Also in the browser at `/station/tools/shuntgen/`, where you can upload a spec, preview the routes it would serve, and generate. ## What comes out ``` artery/shunts// main.py builds the app from the spec run.py uvicorn entry point (PORT, or depot/config.json) shunt_runtime.py vendored runtime — no soleprint import models.py pydantic, via modelgen datagen_.py BaseDataGenerator subclass, via modelgen depot/spec.json routes, collections and schema depot/responses.json pinned overrides — yours, never overwritten depot/config.json delays, error rate, prefill — yours, never overwritten depot/data.json imported rows templates/index.html config UI README.md ``` ```bash cd artery/shunts/books && python run.py curl localhost:8098/customers ``` The routes are built at startup from `spec.json` rather than written out as source. That keeps the generated code short enough to read, and puts the behaviour in one reviewable place: fixing `runtime.py` fixes every shunt, and regenerating is a copy. ## Where a response comes from First hit wins: 1. `depot/responses.json` — a pinned override, keyed `"METHOD /path"` 2. the store — rows imported from sheets, plus anything POSTed since 3. the spec's `example`, if the source document carried one 4. `datagen_.py`, synthesising from the schema 5. `{}` The store is what makes it behave like a service rather than a random-value faucet: POST something and GET it back, ask for `/pets/7` and get the pet whose id is 7. Collections that arrived with no rows are prefilled with generated ones, so the first call answers with something. ## Two sources, one pipeline Both inputs are [modelgen](#station-modelgen) extractors, so the same shapes also generate pydantic, TypeScript, prisma and a [graphgen](#station-graphgen) schema: ```bash python -m station.tools.modelgen from-openapi -s api.yaml -o out/ -t pydantic,typescript python -m station.tools.modelgen from-tabular -s ./sheets -o out/ -t schema,datagen ``` | Source | Becomes | Routes | | --- | --- | --- | | OpenAPI 3.x / Swagger 2.0 | one model per schema; enums become real Enums, `$ref` becomes a relation | the operations the document declares | | `.csv` / `.tsv` / `.ods` | one model per file or sheet, types inferred per column | five CRUD routes per table | Keys and relations are inferred by name and then **checked against the data**: an `id` column that is not unique is not treated as a key, and `customer_id` is only a foreign key if a `customers` sheet came with it. ODS is read with `zipfile` and `ElementTree` — no odfpy, no pandas — which is what lets modelgen stay dependency-free and publishable on its own. ## Control endpoints Every generated shunt serves these: | Endpoint | Purpose | | --- | --- | | `GET /health` | liveness | | `GET /mock/spec` | the routes it was built from | | `GET /mock/stats` | call counts and row counts | | `POST /mock/reset` | restore the imported rows, clear counters | | `GET,POST /mock/config` | delays, error rate, `unknown_id`, page size | | `GET,POST /mock/responses` | pin an override; set a key to `null` to drop it | ```bash # make it slow and flaky, the way the real thing is curl -X POST localhost:8098/mock/config \ -H 'content-type: application/json' \ -d '{"enable_random_delays": true, "error_rate": 0.2}' # make one call answer something specific curl -X POST localhost:8098/mock/responses \ -H 'content-type: application/json' \ -d '{"GET /customers/1": {"id": 1, "name": "PINNED"}}' ``` `unknown_id` decides what an unknown key does: `generate` (the default) invents a record wearing the id that was asked for; `404` refuses it. Generate by default, because a client pointed at a fresh shunt should just work — flip it when the error path is what you are testing. ## Dependency containers `--cabinet postgres,redis` writes a `cabinet.json` declaring what the shunt expects. `build.py` composes those services into the room's compose file, and on a cluster they install as rig addons of the same name. See [Cabinets](#station-cabinets). ## Regenerating Everything is overwritten except `depot/responses.json` and `depot/config.json`. Those two are yours.