FastAPI adapter¶
tripack_container.fastapi is the L3 adapter that turns the
core Tripack container into a drop-in FastAPI experience.
Three public symbols:
from tripack_container import Inject # L1 marker (re-exported at top level)
from tripack_container.fastapi import TripackAPI, TripackRouter
Plus an optional extra so the dependency stays opt-in:
Inject¶
The marker for Annotated[T, Inject] parameters in route
handlers. Supports four forms:
| Annotation | Meaning |
|---|---|
Annotated[T, Inject] |
Resolve T from the container; raise on miss. |
Annotated[T \| None, Inject] |
Resolve T; return None if unbound. |
Annotated[T, Inject(optional=True)] |
Explicit optional flag (same effect as the \| None union). |
Annotated[T, Inject(token="named")] |
Resolve a named token instead of T. |
The marker is L1 (framework-agnostic); TripackAPI is the L3
adapter that gives it meaning inside a FastAPI route.
TripackAPI¶
A FastAPI subclass that takes a container_factory and
wires everything else automatically:
- container build at lifespan startup,
acloseat shutdown (delegated tocontainer_lifespanfromtripack_container.asgi); - per-request
container.ascope()viaContainerScopeMiddleware; Annotated[T, Inject]parameters on every route rewritten toAnnotated[T, Depends(...)]so FastAPI's own dependency machinery resolves the token.
from typing import Annotated, Protocol
from tripack_container import Inject
from tripack_container.fastapi import TripackAPI
from tripack_container.loaders import load_json
class Clock(Protocol):
def now(self) -> float: ...
app = TripackAPI(container_factory=lambda: load_json("container.json"))
@app.get("/now")
def now(clock: Annotated[Clock, Inject]) -> dict[str, float]:
return {"now": clock.now()}
A user-supplied lifespan= keyword still runs - it is
composed inside the container lifecycle, so user-startup code
can already read app.state.container. The same
Annotated[T, Inject] syntax that handlers use also works on
the lifespan signature: TripackAPI introspects it at
startup and resolves the marked keyword parameters from the
freshly built container before invoking the user lifespan:
@asynccontextmanager
async def lifespan(
app: FastAPI,
*,
cache: Annotated[Cache, Inject],
) -> AsyncIterator[None]:
await cache.warmup()
yield
app = TripackAPI(container_factory=build, lifespan=lifespan)
SCOPED tokens are not resolvable at startup (no scope is
active) - SINGLETON / TRANSIENT only. The introspection is
identical to the one tripack_lifespan provides
for non-TripackAPI frameworks; TripackAPI just does it
automatically.
Middleware injection: TripackMiddleware¶
For middleware that needs to resolve from the container per
request, subclass TripackMiddleware. The same
Annotated[T, Inject] keyword-only syntax applies on the
dispatch method:
from tripack_container.asgi import TripackMiddleware
class RequestStampMiddleware(TripackMiddleware):
async def dispatch(
self, scope, receive, send,
*,
log: Annotated[EventLog, Inject],
rid: Annotated[RequestId, Inject],
) -> None:
log.record(0, rid.value, f"middleware:{scope['path']}")
await self.app(scope, receive, send)
app.add_middleware(RequestStampMiddleware)
TripackAPI.add_middleware detects TripackMiddleware
subclasses and inserts them inner to
ContainerScopeMiddleware so SCOPED tokens (like
RequestId above) are resolvable. Non-Tripack middleware
keep the standard outer placement.
TripackRouter¶
An APIRouter subclass whose route_class defaults to the
inject-aware variant. Required for sub-routers that use
Annotated[T, Inject] - the default APIRouter analyses its
endpoints at decoration time (before include_router runs)
and rejects the bare Inject marker as a non-Pydantic field.
from tripack_container.fastapi import TripackRouter
sub = TripackRouter(prefix="/v2")
@sub.get("/now")
def v2_now(clock: Annotated[Clock, Inject]) -> dict[str, float]:
return {"now": clock.now()}
app.include_router(sub)
TripackAPI.include_router also promotes a default
APIRouter to the inject-aware route class - safe net for
sub-routers that happen not to use Inject today but might
later.
Design choice: subclass over composite¶
TripackAPI is a subclass of FastAPI, not a composite
holding a FastAPI instance. The trade-offs:
| Pattern | Pro | Con |
|---|---|---|
| Subclass (chosen) | isinstance(app, FastAPI) stays truthy; every FastAPI tool keeps working. Constructor surface matches FastAPI(...). |
Tight coupling to FastAPI internals. |
Composite (app.fastapi) |
Loose coupling. | Breaks ASGI app expectations; every consumer needs .fastapi indirection. |
Function (bind_container(app, ...)) |
Works with any user subclass. | Lifespan composition is awkward - FastAPI fixes the lifespan at construction. |
The L1 marker and the L2 ASGI primitives are framework-agnostic; the subclass coupling is intentional and confined to this L3 module.
API¶
class TripackAPI(FastAPI):
def __init__(
self,
*args: Any,
container_factory: ContainerFactory,
**kwargs: Any,
) -> None: ...
def include_router(
self,
router: APIRouter,
*args: Any,
**kwargs: Any,
) -> None: ...
class TripackRouter(APIRouter):
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
For an end-to-end walkthrough (JSON wiring, interface-driven handlers, optional dependencies, chained interfaces) see the FastAPI integration example.