| name | python-wiring-success |
| description | Build the three plumbing constructs — config, route, composition root — the only places environment reads, transport ingress, and program assembly are allowed. Fires before any os.environ read, dotenv parse, or settings dict; before writing a handler that parses/computes/decides on request or message input; or before writing main.py, a runner, a pipeline, an orchestrator, or a step-list function. |
Wiring
The plumbing constructs: the outermost ring, no domain logic. Config reads the environment once; the route is transport ingress; the composition root assembles everything and is the only place main() belongs.
Config
Definition
A frozen BaseSettings model, the only structure that reads environment values. Every field is a declared scalar or secret type, and it is constructed once by the composition root and injected.
Required Form
class VenueUrl(RootModel[str], frozen=True):
root: str = Field(min_length=1)
class PositionConfig(BaseSettings):
model_config = SettingsConfigDict(frozen=True, extra="forbid", env_prefix="VENUE_")
url: VenueUrl
token: SecretStr
Sorting Rules
A configured value used in the domain is carried as its declared scalar, never re-read from the environment. Client instantiation from config values belongs to the composition root. A value that is domain state rather than environment fact belongs on the consistency model (python-state-success).
Replaced Forms
An os.environ read scatters the environment through the program; config proves it once at startup. A settings dict carries unproven values; a config singleton hides the read behind import order.
Construct-Specific Doctrine
A secret is never a bare str: SecretStr keeps it out of every dump, repr, and log. get_secret_value() is called exactly once, at client instantiation in the composition root.
BaseSettings lives in the pydantic-settings package; if it is not installed, that is a gap to report, not a reason to read the environment directly.
Allowed Patterns
- one frozen
BaseSettings model per program with SettingsConfigDict(frozen=True, extra="forbid")
- an
env_prefix naming the program's environment namespace
- every field a declared scalar or
SecretStr
- constructed once in the composition root and injected
Forbidden
- an
os.environ read anywhere
- a settings dict or config singleton
- a secret typed as bare
str
get_secret_value() outside the composition root
Halt Rule
Halt when an environment value has no declared scalar or secret type to land in, or when pydantic-settings is absent. Report the row and the value: the environment fact is not yet modeled, and the table is not finished.
Route
Definition
The function at transport ingress. It constructs a contract model or foreign model from raw transport data, unwraps transport wrapper structure, dispatches the value the verb consumes, and serializes the reply. It defines no types and computes no domain fact.
Required Form
def fill_route(raw: str, model: PositionConsistencyModel) -> str:
message = VenueFillMessage.model_validate_json(raw)
model.book(message.fill)
return OrderReceipt(order_id=message.fill.order_id).model_dump_json()
The route dispatches message.fill, the innermost value the verb consumes, never the transport wrapper. The reply is one of the two legal serialization sites.
Sorting Rules
The shape the route constructs belongs to whoever owns it: a contract model when this program publishes the API, a foreign model when the caller's shape is another system's (python-adapters-success). Domain work belongs to the verb the route dispatches to (python-verbs-success). Wiring and registration belong to the composition root.
Replaced Forms
A handler that parses fields by hand restates the construction the model performs in one call. A handler that computes or decides is domain meaning escaped to the edge; the route turns transport into a construction and back, nothing more.
Allowed Patterns
- one function per ingress: one construction from raw transport data, one dispatch of the innermost value, one serialized reply
- contract and foreign models imported from the files that declare them
model_dump_json on the reply contract, the route being a legal serialization site
Forbidden
- parsing transport fields by hand
- transforming or computing domain data
- branching on a domain case
- dispatching a transport wrapper into a verb
- defining any type in the route's file
Halt Rule
Halt when ingress would require a computation or a decision, or when the ingress shape, the dispatched verb, or the reply names no row. Report the row and the requirement: the surface, the foreign thing, or the verb is not yet modeled, and the table is not finished.
Composition Root
Definition
The program entrypoint. It constructs config, instantiates concrete clients, passes them to bindings, constructs the consistency model, and registers or invokes routes. It holds no domain logic and defines no domain model.
Required Form
def main() -> None:
config = PositionConfig()
bus = BusClient(config.url.root, config.token.get_secret_value())
ledger = LedgerClient(config.url.root, config.token.get_secret_value())
model = PositionBinding().connect(bus=bus, ledger=ledger, opening=Flat())
run_ingress(lambda raw: fill_route(raw, model))
A long-running program's root is the same shape made async; signal handling and graceful teardown are wiring and live here, nowhere else.
async def main() -> None:
config = PositionConfig()
bus = BusClient(config.url.root, config.token.get_secret_value())
ledger = LedgerClient(config.url.root, config.token.get_secret_value())
model = PositionBinding().connect(bus=bus, ledger=ledger, opening=Flat())
shutdown = asyncio.Event()
for sig in (signal.SIGTERM, signal.SIGINT):
asyncio.get_running_loop().add_signal_handler(sig, shutdown.set)
await shutdown.wait()
await bus.drain()
.root and get_secret_value() are legal here because the composition root is a client binding site, one of the two places the program meets the wire.
Sorting Rules
Domain construction belongs to the consistency model and its verbs; the root only wires. Client binding belongs to the binding; the root instantiates clients and hands them over. Request handling belongs to routes; the root registers or invokes them. Environment reads belong to config; the root constructs it once.
Replaced Forms
A runner, pipeline, orchestrator, or step list is a hand-kept copy of an order the construction graph already determines: a value cannot construct before its inputs, so evaluation order is the sequence. A function that calls everything in order means the terminal object has not been named; name it and construct it.
Allowed Patterns
- one
main() that constructs config, instantiates clients, binds through bindings, constructs the consistency model, and registers or invokes routes
- the async form with signal handlers, a shutdown event, and client drain
.root and get_secret_value() at client instantiation
- input read and output emitted only at the edges of
main
Forbidden
- an orchestrator, pipeline, or step-runner sequencing domain work
- a domain computation in the entrypoint
- a domain model defined in the entrypoint's file
- an environment read outside config
Halt Rule
Halt when wiring would require a domain decision, a domain computation, or a model definition. Report the row and the meaning that has no wiring home: the terminal object is not yet named in the table, and the table is not finished.