App & Factory
The top-level entry points for creating and running a Lauren application.
LaurenFactory
class LaurenFactoryProduces LaurenApp instances via the 7-phase pipeline.
LaurenFactory.create
def create(root_module: type, strict_lifecycle: bool = True, global_middlewares: Iterable[type] | None = None, global_guards: Iterable[type] | None = None, global_interceptors: Iterable[type] | None = None, global_exception_handlers: Iterable[Any] | None = None, global_providers: Iterable[Any] | None = None, global_ws_guards: Iterable[type] | None = None, global_ws_interceptors: Iterable[type] | None = None, max_body_size: int = 1048576, app_state: AppState | None = None, logger: Logger | None = None, openapi_info: dict[str, Any] | None = None, openapi_servers: list[dict[str, Any]] | None = None, openapi_security_schemes: dict[str, Any] | None = None, openapi_url: str | None = None, docs_url: str | None = None, redoc_url: str | None = None, arena: RequestArena | None = None, arena_capacity: int | None = None, json_encoder: JSONEncoder | None = None, signals: SignalBus | None = None, error_format: str = 'default', root_path: str = '', mounts: dict[str, Any] | None = None, sessions: SessionConfig | None = None, content: ContentConfig | None = None, problem_type_base: str | None = None, debug: bool | DebugConfig = False, lazy_startup: bool = True, hosted_services: bool = True, scheduler: bool = True, validation: ValidationPolicy | None = None, strict_request_scope: bool = False, response_validation: str = 'off', json_fallback: str = 'warn', pagination: PaginationConfig | None = None) -> LaurenAppBuild a LaurenApp from a root @module class.
The seven-phase pipeline is logged through logger (defaults to a
no-op NullLogger when not provided, so
existing test suites remain silent). Pass
logger=lauren.logging.default_logger() for a production-ready
configuration that auto-detects TTY vs JSON output.
Passing any of openapi_url, docs_url, or redoc_url
exposes the corresponding documentation endpoint. The common
pattern is openapi_url='/openapi.json', docs_url='/docs',
redoc_url='/redoc' — matching FastAPI conventions.
global_guards and global_exception_handlers mirror NestJS's
app-level guards and exception handlers: every request runs through
them after the route is resolved (guards) or whenever the handler
raises (handlers). Per-route @use_guards and
@use_exception_handlers declarations compose with these globals —
route handlers are tried first, then globals.
content sets the request-body content policy
(ContentConfig). Left unset, apps with
sessions= log mismatched JSON Content-Type values ("warn")
and other apps do not check them.
error_format picks the error wire shape: "default" (the
{"error": {...}} envelope), "rfc9457" (RFC 9457 Problem
Details) or "rfc7807" (the legacy problem-details shape).
problem_type_base (e.g. "https://errors.example.com/") builds
the problem-details type as base + code for errors without
their own problem_type.
debug=True (or a DebugConfig) enables
development error pages for unhandled exceptions, served to loopback
clients only by default. It is never enabled implicitly; create()
logs a warning whenever it is on, and refuses it (StartupError,
reason="debug_in_production") when the configuration module's
active profile is a production profile.
lazy_startup (default True) makes the app run LaurenApp.startup
on its first HTTP or WebSocket request when the ASGI server never
sent lifespan events (httpx.ASGITransport), logging a warning.
Pass False to keep such an app cold until startup() is awaited.
hosted_services=False discovers and validates @hosted_service
classes but does not start them; scheduler=False does the same for
scheduled jobs (SchedulerRegistry.trigger still works). Tests and
one-off commands use them to build the full app without background work.
validation (a ValidationPolicy) sets how
request values are validated: the stdlib-model mode ("warn" by
default in 1.x), JSON strictness, unknown keys, input echo and the
issue cap. Every extractor, WebSocket payload and stream item type is
compiled once here by the app's type compiler (app.type_compiler).
strict_request_scope=True makes resolving a Scope.REQUEST
provider outside any request, WebSocket connection or app.scope()
raise DIScopeViolationError
(reason="no_active_scope"). The default keeps the 1.x fallback — a
new, never torn down instance per resolution — and logs a warning once
per provider.
response_validation is the default response contract of every route
(@get(..., response_validation=...) overrides it): "off" (the
default) serializes the returned value as it is, "filter" projects
it onto the declared return type so undeclared fields never leave, and
"strict" also validates every value. json_fallback decides what
an encoder does with an object it has no rule for: "dict" sends its
public attributes, "warn" (the default) does too and logs once per
type, "error" fails the response with a 500.
pagination (a PaginationConfig) sets the
app-wide paging defaults, maxima, parameter names, page envelope and
cursor secret; Annotated[OffsetPagination, PaginationConfig(...)]
overrides per parameter. A route using CursorPagination without a
cursor secret fails here (reason="pagination_secret_missing").
LaurenFactory.create_context
def create_context(root_module: type, hosted_services: bool = False, scheduler: bool = False, drain_timeout: float = 10.0, create_kwargs: Any = {}) -> ApplicationContextBuild the app with create and wrap it in an ApplicationContext.
The graph is the server's — every phase runs, routes included, so if
a command or script boots, the server boots — but nothing is served.
async with LaurenFactory.create_context(AppModule) as ctx: runs
startup() on enter and shutdown(drain_timeout=...) on exit.
Hosted services and the scheduler are off by default (pass
hosted_services=True for a worker); create_kwargs go to
create unchanged.
Lauren
class Lauren(title: str = 'lauren application', version: str = '1.0.0', description: str | None = None, openapi_url: str | None = '/openapi.json', docs_url: str | None = '/docs', redoc_url: str | None = '/redoc', servers: list[dict[str, Any]] | None = None, security_schemes: dict[str, Any] | None = None, debug: bool | DebugConfig = False, max_body_size: int = 1048576, strict_lifecycle: bool = True, app_state: AppState | None = None, logger: Any | None = None, global_middlewares: list[type] | None = None, global_guards: list[type] | None = None, global_interceptors: list[type] | None = None, global_exception_handlers: list[Any] | None = None, global_providers: list[Any] | None = None, global_ws_guards: list[type] | None = None, global_ws_interceptors: list[type] | None = None, sessions: Any | None = None, content: Any | None = None, json_encoder: Any | None = None, error_format: str = 'default', problem_type_base: str | None = None, root_path: str = '', mounts: dict[str, Any] | None = None, signals: Any | None = None, arena: Any | None = None, arena_capacity: int | None = None, lazy_startup: bool = True, hosted_services: bool = True, scheduler: bool = True, validation: Any | None = None, strict_request_scope: bool = False, response_validation: str = 'off', json_fallback: str = 'warn', pagination: Any | None = None)A FastAPI-inspired ASGI application over lauren's module pipeline.
All constructor arguments have defaults; the canonical call is simply
Lauren(). Routes are added with the verb decorators
(get / post / put / delete / patch / head /
options), modules are merged with include_module, and
middleware is appended with add_middleware.
Lauren.get
def get(self, path: str = '/', kw: Any = {}) -> Callable[[Callable[..., Any]], Callable[..., Any]]Register a GET route. Mirrors FastAPI's @app.get.
Lauren.post
def post(self, path: str = '/', kw: Any = {}) -> Callable[[Callable[..., Any]], Callable[..., Any]]Lauren.put
def put(self, path: str = '/', kw: Any = {}) -> Callable[[Callable[..., Any]], Callable[..., Any]]Lauren.patch
def patch(self, path: str = '/', kw: Any = {}) -> Callable[[Callable[..., Any]], Callable[..., Any]]Lauren.delete
def delete(self, path: str = '/', kw: Any = {}) -> Callable[[Callable[..., Any]], Callable[..., Any]]Lauren.head
def head(self, path: str = '/', kw: Any = {}) -> Callable[[Callable[..., Any]], Callable[..., Any]]Lauren.options
def options(self, path: str = '/', kw: Any = {}) -> Callable[[Callable[..., Any]], Callable[..., Any]]Lauren.include_module
def include_module(self, module_cls: type) -> NoneMerge a NestJS-style @module class into this application.
Every provider declared inside the included module (and anything it
re-exports) becomes visible to the synthetic app-level module. Call
as many times as needed before startup.
Lauren.include_router
def include_router(self, other: 'Lauren', prefix: str = '') -> NoneMerge another Lauren instance's routes and modules.
Every buffered route on other is copied into self with
prefix prepended, and other's included modules are pulled
in too. Neither instance compiles until startup runs on
self. other must itself be uncompiled.
Lauren.add_middleware
def add_middleware(self, cls: type) -> NoneRegister a middleware class globally.
The class must be decorated with @middleware() (defining a
dispatch(request, call_next) coroutine). Middleware runs in
insertion order around every request.
Lauren.add_guard
def add_guard(self, cls: type) -> NoneRegister a guard class globally.
The class must define can_activate(context). Global guards
run before any per-route @use_guards chain on every request.
Equivalent to passing global_guards=[...] to
LaurenFactory.create.
Lauren.add_interceptor
def add_interceptor(self, cls: type) -> NoneRegister an interceptor class globally.
The class must be decorated with @interceptor. Global interceptors
run after guards and before route handlers on every request. Equivalent
to passing global_interceptors=[...] to LaurenFactory.create.
Lauren.add_exception_handler
def add_exception_handler(self, handler: Any) -> NoneRegister an exception handler globally.
handler must already be decorated with @exception_handler.
It runs whenever a request handler raises an exception that
matches the handler's declared exception tuple, after any
per-route or controller-level handlers have been tried.
Lauren.add_provider
def add_provider(self, provider: Any) -> NoneRegister a provider globally (visible to every module).
Accepts the same shapes as the module providers= list: an
@injectable class, a function provider, or the result of
use_value / use_class / use_factory / use_existing.
Must be called before the application compiles.
Lauren.mount
def mount(self, path: str, app: Any) -> NoneMount an ASGI sub-application at path (see LaurenApp.mount).
Buffered until the application compiles, like routes and modules;
calling it afterwards raises LifecycleViolationError.
Lauren.on_startup
def on_startup(self, fn: Callable[[], Any]) -> Callable[[], Any]Register a callable to run when the application starts.
Accepts both sync and async callables. Hooks run in registration order
inside LaurenApp.startup() (the ASGI lifespan, TestClient, an
ApplicationContext) after all @post_construct hooks have fired
and before hosted services start or any request is dispatched. A
failing hook aborts startup with a StartupError.
Lauren.on_shutdown
def on_shutdown(self, fn: Callable[[], Any]) -> Callable[[], Any]Register a callable to run during graceful shutdown.
Executes after in-flight requests drain and before
@pre_destruct hooks on singleton providers.
Lauren.startup
def startup(self) -> AnyCompile the application without starting it.
Safe to call multiple times — later calls return the already-compiled
LaurenApp. Starting the app (building singletons, running
@post_construct and then on_startup hooks) is the ASGI
lifespan's job; TestClient compiles and
starts the app on its first request. Returns the underlying
LaurenApp so tests can introspect it.
Lauren.shutdown
def shutdown(self, drain_timeout: float = 10.0) -> NoneGracefully stop the underlying LaurenApp.
A no-op if the app never compiled (nothing to shut down).
Lauren.openapi
def openapi(self) -> dict[str, Any]Return the OpenAPI 3.1 document.
Requires the app to have been compiled (either via startup
or an ASGI request). Raises LifecycleViolationError if
called before compilation.
Lauren.routes
def routes(self) -> list[Any]Return the compiled route list.
Before compilation this reflects buffered routes as a list of
(method, path, handler) tuples; afterwards it returns the
underlying LaurenApp's RouteEntry objects for
parity with the classic API.
Lauren.scope
def scope(self, name: str = 'operation', values: Mapping[Any, Any] | None = None) -> _context.OperationScopeOpen an OperationScope on the compiled app.
Same as LaurenApp.scope. Raises
LifecycleViolationError before the app has compiled.
Lauren.begin_draining
def begin_draining(self, reason: str = '') -> boolSame as LaurenApp.begin_draining; False before the app has compiled.
LaurenApp
class LaurenApp(router: Router, container: DIContainer, module_graph: ModuleGraph, lifecycle: LifecycleScheduler, compiled_handlers: dict[tuple[str, str], CompiledHandler], global_middlewares: list[type], app_state: AppState, strict_lifecycle: bool = True, max_body_size: int = 1048576, logger: Logger | None = None, ws_router: Router | None = None, ws_gateways: dict[str, Any] | None = None, arena: RequestArena | None = None, signals: SignalBus | None = None, error_format: str = 'default', global_guards: list[type] | None = None, global_exception_handlers: list[Any] | None = None, global_interceptors: list[type] | None = None, global_providers: list[Any] | None = None, global_ws_guards: list[type] | None = None, global_ws_interceptors: list[type] | None = None, strict_content_type: bool | str = False, problem_type_base: str | None = None, debug: DebugConfig | None = None, guard_denials: dict[type, Any] | None = None, lazy_startup: bool = True, validation: ValidationRuntime | None = None, response_validation: str = 'off', json_fallback: str = 'warn')A compiled, ready-to-serve ASGI application.
Instances of this class are produced exclusively by LaurenFactory.create.
LaurenApp.begin_draining
def begin_draining(self, reason: str = '') -> boolAnnounce termination: move READY → DRAINING so readiness probes fail.
Requests are still served normally; the drain itself is
shutdown's job. Call it before the server stops accepting
connections (a preStop window, a signal handler —
install_signal_handlers does) so load
balancers polling readiness take the instance out of rotation first.
Synchronous and idempotent: returns True when it changed the
state, False in any state other than READY.
LaurenApp.routes
def routes(self) -> list[RouteEntry]LaurenApp.openapi
def openapi(self) -> dict[str, Any]LaurenApp.on_shutdown
def on_shutdown(self, callback: Callable[[], Any]) -> Callable[[], Any]Register a callback to run during shutdown.
Complements @pre_destruct by letting callers attach arbitrary
cleanup coroutines (or plain callables) without needing to express
them as DI-scoped providers. Usable as a decorator:
@app.on_shutdown
async def flush_buffers() -> None:
...Callbacks run in reverse registration order (LIFO) after in-flight
requests have drained and before @pre_destruct hooks, so
they can use the DI graph if needed.
LaurenApp.mount
def mount(self, path: str, app: Any) -> NoneMount an ASGI sub-application at path.
All requests whose path starts with path (or equals it exactly) are
forwarded to app after stripping the prefix. The stripped portion
is appended to scope["root_path"] so the sub-application can
reconstruct absolute URLs correctly.
Mounts are checked in descending prefix-length order so a more-specific
prefix (/api/v2) always wins over a shorter one (/api).
Example:
app = LaurenFactory.create(AppModule)
app.mount("/legacy", legacy_asgi_app)
app.mount("/files", static_files_app)The same result can be achieved at build time via
LaurenFactory.create(..., mounts={"/legacy": legacy_asgi_app}).
LaurenApp.scope
def scope(self, name: str = 'operation', values: Mapping[Any, Any] | None = None) -> OperationScopeOpen a DI scope for work outside an HTTP request or WebSocket connection.
Returns a single-use OperationScope to use as
an async context manager. Scope.REQUEST providers resolved through
it get one instance per scope, torn down on exit (@pre_destruct,
then aclose(), newest first). values seeds framework values
({JobContext: ctx}) that constructors and OperationScope.resolve
receive as-is. Use it while the app is running (after
startup), so singletons have had their @post_construct:
async with app.scope("backfill") as scope:
db = await scope.resolve(DbSession)LaurenApp.context
def context(self, hosted_services: bool = False, scheduler: bool = False, drain_timeout: float = 10.0) -> ApplicationContextWrap this built, not yet started app in an ApplicationContext.
For tooling that only knows main:app; async with app.context() as ctx:
starts the app without serving HTTP and shuts it down on exit. Hosted
services and the scheduler stay off unless enabled here (and on the
app). Raises LifecycleViolationError when
the app was already started: one lifecycle has one owner.
LaurenApp.startup
def startup(self) -> NoneLaurenApp.shutdown
def shutdown(self, drain_timeout: float = 10.0) -> NoneGracefully stop the application.
Steps (each logged as an event):
- Mark the app not-running — no new request scheduling — and move
READY → DRAINING(a no-op afterbegin_draining); the scheduler stops firing (hosted services keep running). - Drain in-flight requests (up to
drain_timeoutseconds). - Await in-flight scheduled runs (
ScheduleModulejob_drain_timeout, default what is left ofdrain_timeout), then cancel them. - Stop hosted services in reverse start order.
- Await running
@on_eventhandler tasks with what is left of the budget (orEventsModule'sdrain_timeout), then cancel them — after jobs and services, so events they emit while stopping run. - Invoke user-registered
on_shutdowncallbacks in reverse order. - Run
@pre_destructhooks in reverse topological order.
The app ends STOPPED (a FAILED app stays FAILED).
Idempotent: concurrent or repeated calls return as soon as the first
shutdown has completed.
LaurenApp.handle
def handle(self, request: Request) -> ResponseDispatch a Request through middleware, guards and handler.
Global middlewares run before routing so they can intercept every request — including OPTIONS preflight — regardless of whether a matching route exists. Per-route and controller middlewares run after routing (they need access to the compiled handler).
Every dispatch acquires a RequestAllocation bundle from
the app's RequestArena. The bundle's request_cache
holds request-scoped DI instances; framework_values is
re-populated per call with the live Request so the DI
container can short-circuit Request lookups; kwargs
holds handler arguments assembled from the extractor plan.
Every container returned from the lease is cleared on exit, so pooled allocations never leak user data across requests.