90 lines
2.5 KiB
Markdown
90 lines
2.5 KiB
Markdown
# Composition Root
|
|
|
|
## Summary
|
|
|
|
All wiring happens in exactly one module. It builds a **flat resource
|
|
registry** and exposes it to the HTTP layer and to scripts.
|
|
|
|
---
|
|
|
|
## The resource registry
|
|
|
|
`init.py` returns a plain `dict[str, Any]`. Keys follow two conventions:
|
|
|
|
- `<resource>_repo` — a repository adapter for a resource.
|
|
- `<resource>_svc` — a service for a resource.
|
|
|
|
Plus named infrastructure adaptors:
|
|
|
|
- `http_cache_store` — response cache backend.
|
|
- `progress_bus` — progress/event bus.
|
|
- `<vendor>_<kind>` — fetchers, clients, stores.
|
|
|
|
The API stores the dict on `app.state.resources` during lifespan startup.
|
|
Handlers read from it; nothing imports concrete adapters.
|
|
|
|
## Shape
|
|
|
|
```python
|
|
_resources: dict = {}
|
|
_initialized = False
|
|
|
|
async def init(settings=None) -> dict:
|
|
global _initialized
|
|
if _initialized:
|
|
return _resources
|
|
|
|
await setup_database(uri=settings.mongo_uri, db_name=settings.mongo_db)
|
|
|
|
# 1. leaf repositories
|
|
user_repo = create_user_repo()
|
|
|
|
# 2. dependent repositories (dependencies passed by keyword)
|
|
wallet_repo = create_wallet_repo(user_repo=user_repo)
|
|
|
|
# 3. services (build via the DI library, injecting the repo)
|
|
_resources["user_repo"] = user_repo
|
|
_resources["wallet_repo"] = wallet_repo
|
|
_resources["user_svc"] = get_user_service(user_repo)
|
|
_resources["wallet_svc"] = get_wallet_service(wallet_repo)
|
|
|
|
# 4. infrastructure adaptors (optional)
|
|
_resources["http_cache_store"] = HttpCacheStore(...)
|
|
_resources["progress_bus"] = ProgressEventBus()
|
|
|
|
_initialized = True
|
|
return _resources
|
|
|
|
async def shutdown() -> None:
|
|
global _initialized
|
|
await close_stores(_resources)
|
|
await shutdown_database()
|
|
_resources.clear()
|
|
_initialized = False
|
|
```
|
|
|
|
## Typed accessors
|
|
|
|
Expose a `get_svc(name)` helper with one `@overload` per key so scripts get
|
|
typing without importing concrete classes:
|
|
|
|
```python
|
|
@overload
|
|
def get_svc(name: Literal["user_svc"]) -> UserService: ...
|
|
@overload
|
|
def get_svc(name: Literal["wallet_svc"]) -> WalletService: ...
|
|
|
|
def get_svc(name: str):
|
|
return _resources[name]
|
|
```
|
|
|
|
Also expose `get_db()` (raw database handle) and pure helpers such as
|
|
`compute_fingerprint(payload)` (canonical hash used for deduplication).
|
|
|
|
## Rules
|
|
|
|
- `init()` is **idempotent**.
|
|
- The composition root is the only place that imports concrete adapters.
|
|
- Handlers never call `init()`; they read `request.app.state.resources`.
|
|
- Scripts call `init()` / `shutdown()` around their work.
|