# lauren

> A metadata-first, high-performance Python web framework for building production ASGI services. Inspired by Rust's Axum, NestJS, and FastAPI. Every route, DI binding, module boundary, lifecycle hook, WebSocket gateway, and exception handler is declared with decorators and resolved into an immutable execution graph at startup — the request path is pure traversal.

Targets Python 3.11+. Zero runtime magic: what you declare is what you get.

**Hard dependency:** `anyio>=4.0` only. Pydantic, msgspec, and all binary wheels are optional.

```bash
pip install lauren                  # anyio only
pip install "lauren[pydantic]"      # + pydantic>=2.0
pip install "lauren[msgspec]"       # + msgspec>=0.18
pip install "lauren[full]"          # pydantic + msgspec
```

## Core pillars

- **Radix-tree router** with O(depth) lookup, static > param > wildcard priority, per-method dispatch with `Allow` header on 405.
- **Dependency Injection** with `SINGLETON`, `REQUEST`, and `TRANSIENT` scopes, Protocol binding, multi-bindings (`list[T]` injection), circular detection, scope-violation checks, the four NestJS-style custom-provider recipes (`use_value`, `use_class`, `use_factory`, `use_existing`) plus `Token` and `Inject`, and **generator function providers** (FastAPI-style lifecycle: code before `yield` = setup, yielded value = dependency, code after `yield` = teardown — scoped to `SINGLETON` or `REQUEST`). **Operation scopes** (`async with app.scope("job") as scope: await scope.resolve(DbSession)`) give scripts and background work the request scope without a request, torn down on exit.
- **Extractor system** for typed request decomposition (`Path[int]`, `Query[str]`, `Header[str]`, `Cookie[str]`, `Json[Model]`, `Form[Model]`, `Bytes`, `ByteStream`, `UploadFile`, `State`, `Depends[T]`), model extraction for Pydantic / `msgspec.Struct` / Python `dataclass` / `TypedDict` types, **pydantic-free discriminated unions** via `Discriminated[A | B, "key"]`, plus user-defined extractors via an `ExtractionMarker.extract` instance method (legacy classmethod form still supported).
- **Module system** with explicit imports/exports and circular-import detection (NestJS-style).
- **Lifecycle hooks** (`@post_construct`, `@pre_destruct`) run in topological order (at `startup()` / shutdown). Both sync and async hooks are supported; `@pre_destruct` hooks have a per-hook timeout and sync ones run in a thread pool so they never block the event loop.
- **First-class WebSockets** — `@ws_controller(path)`, `@on_connect`, `@on_message("event")`, `@on_disconnect`, `@on_error`, with typed validated frames, discriminated-union dispatch (`Discriminated[A | B, "key"]`), `BroadcastGroup` rooms, and native `@use_guards` / `@use_interceptors`: on the gateway class they run once before `@on_connect` (`WsConnectionContext`, duck-types with `ExecutionContext`); on an `@on_message` handler they run for every frame (`WsMessageContext`). Placements WebSockets cannot honour are rejected at startup.
- **Server-Sent Events** — `EventStream`, `ServerSentEvent`, `last_event_id`. Per the HTML living standard. Optional keep-alive heartbeats.
- **Typed streaming** — `StreamingResponse[T]` content-negotiates between SSE / NDJSON / JSON Lines from the `Accept` header, including non-Pydantic item types.
- **Response shaping** — `Response.file(...)`, `Response.xml(...)`, immutable `with_*` builders, and custom `Response` subclasses that pass through dispatch unchanged while preserving their concrete type.
- **Socket.IO compatibility** — Engine.IO v4 / Socket.IO v5 adapter with `@socketio_controller` and `@on_socketio_event`.
- **ASGI runtime** with `LaurenFactory.create()` seven-phase startup, lifespan protocol, graceful drain.
- **Sync handler support**: plain `def` handlers are automatically offloaded to a thread pool via `anyio.to_thread.run_sync`; they never block the event loop. All extractors, return-type coercion, and binding styles (instance / classmethod / static) work identically for sync and async handlers.
- **OpenAPI 3.1** generation from Pydantic models, dataclasses, TypedDict, `msgspec.Struct`, and `Discriminated` unions (`oneOf` + `discriminator.mapping`) — complete schemas without pydantic installed; Swagger UI / ReDoc-ready.
- **Middleware (onion)**, **Guards**, and **Exception Handlers** — all attachable globally, per-controller, or per-route. Guards work on both HTTP controllers and WebSocket gateways; the same guard class is accepted for both via duck-typing. `@exception_handler` may declare several types at once (`@exception_handler(A, B)`) or by stacking — stacking accumulates the types.
- **`@propagate_metadata(source)`** — copy `@use_guards`, `@use_interceptors`, `@use_middlewares`, `@use_encoder`, `@use_exception_handlers`, and `@set_metadata` annotations from a source class or function to a target. Like `functools.wraps` for Lauren metadata. List-based entries are prepended (source runs outermost); encoder is copied only when target has none; user metadata is merged with target keys winning.
- **`lauren.reflect`** — metadata introspection API. Static class readers: `reflect_controller`, `reflect_module`, `reflect_injectable`, `reflect_ws_controller`, `reflect_routes`, `reflect_ws_messages`, `reflect_exception_handlers`, `get_controller_metadata`, `get_module_metadata`, `reflect_user_metadata`, `reflect_encoder`. App-level readers (post-startup): `get_all_routes`, `get_all_ws_gateways`, `get_route_metadata`. WS cross-cutting: `WsConnectionContext`, `WsUpgradeRequest`, `reflect_guards`, `reflect_interceptors`, `reflect_middlewares`, `reflect_all`, `apply_guards`, `apply_interceptors`. Result types: `ReflectedRoute`, `ReflectedWsMessage`, `ReflectedController`, `ReflectedModule`, `ReflectedWsGateway`. All class readers read from `cls.__dict__` only (no inheritance).
- **Interceptors** — `@interceptor()` wraps handler execution for timing, caching, or response transforms. `CallHandler.handle()` always returns a `Response`.
- **Pipes** — `Pipe` and `pipe()` for parameter-level transforms and validation before handler execution.
- **Auto-serialization**: return dicts, Pydantic models, dataclasses, `msgspec.Struct` values, custom `Response` subclasses, or `(body, status)` tuples and lauren builds the Response for you.
- **Response contracts**: `@post(..., status_code=201)` and typed results (`Ok[T]`, `Created[T]`, `Accepted[T]`, `NoContent`) declare a route's statuses for the wire and OpenAPI; `response_validation="filter"` projects the returned value onto the annotated type so internal fields (`password_hash`) never leave, `"strict"` also validates it; `@raises(NotFoundError)` documents error responses; `json_fallback=` stops arbitrary objects shipping their `__dict__`.
- **Pluggable JSON encoders** — `StdlibJSONEncoder` (default), `OrjsonEncoder`, `MsgspecEncoder`, `PydanticEncoder`. Set app-wide via `LaurenFactory.create(json_encoder=…)`; override per-controller or per-route with `@use_encoder(enc)`. The encoder is threaded through every output path: handler responses, error bodies, SSE events, and WebSocket `send_json`.
- **Strict inheritance**: subclasses are controllers / injectables / WS gateways / exception handlers / middlewares only when explicitly re-decorated.
- **Structured logging** with NestJS-style `ConsoleLogger` (coloured) and `JsonLogger` (production); all four built-in loggers are `@injectable(provides=(Logger,))` — pass one in `global_providers=[ConsoleLogger]` and any service can declare `log: Logger` to receive it.
- **Graceful shutdown hooks** via `app.on_shutdown(fn)` plus `install_signal_handlers` for SIGTERM/SIGINT-driven drain (`shutdown_delay=` keeps serving while readiness fails).
- **Lifecycle state** — `app.lifecycle_state` (`AppLifecycleState`: `CREATED` → `STARTING` → `READY` → `DRAINING` → `STOPPED`, or `FAILED`), `LifecycleStateChanged` on every transition, and `app.begin_draining()` to fail readiness before the server stops accepting connections.
- **Health probes** — `HealthModule.for_root()` serves `/health/live`, `/health/ready`, `/health/startup` and a full report (`lauren.health`); `@health_check("name")` methods on singletons are discovered and validated at startup, run concurrently under per-check timeouts, single-flighted and optionally cached. Liveness never checks dependencies by default; details are hidden unless allowed (`details="always"` / `"when_authorized"` + guard); probe routes carry `PUBLIC_METADATA_KEY` for global guards; built-in event-loop-lag, disk and memory indicators; IETF `application/health+json` format.
- **Signals & lifecycle events** — `SignalBus` with `StartupBegin`, `StartupComplete`, `ShutdownBegin`, `RequestReceived`, `RequestComplete`, `BackgroundTaskStarted`, `BackgroundTaskComplete`, `BackgroundTaskFailed`, `EventHandlerFailed`, `HostedServiceStarted` / `Stopped` / `Failed`, `JobStarted` / `Completed` / `Failed` / `Skipped`, `LifecycleStateChanged`, `HealthChanged` (framework telemetry, not domain events).
- **Application events** — typed in-process domain events (`lauren.events`): `@on_event` handlers on any provider (event class inferred from the annotation; base-class handlers see subclasses), discovered and validated in `LaurenFactory.create`; publish with the singleton `EventBus` or the request-scoped `EventPublisher` (inline handlers share the request's DI scope, `"background"` ones run after the response). Modes `inline` / `concurrent` / `background`, `priority`, `errors="propagate" | "isolate"`, `EmitResult`, `EventContext`, `after=TransactionPhase.COMMIT` via a `UnitOfWork`, shutdown draining, `bus.recording()` / `bus.drain()` for tests.
- **Sub-application mounting** — `app.mount("/prefix", sub_app)` for composing multiple Lauren or ASGI apps.
- **Sessions** — first-class signed-cookie sessions via `LaurenFactory.create(..., sessions=SessionConfig(...))`. A mutable `session: Session` is injected into handlers at zero per-request reflection cost; pluggable async `SessionStore` (`InMemorySessionStore`, stateless `SignedCookieSessionStore`, or a Redis impl). HMAC-signed, `HttpOnly`/`Secure`/`SameSite=Lax` by default, with `regenerate_id()` (fixation defence) and `invalidate()` (logout). Unsafe configs are rejected at startup.
- **Stores & caching** — one async key-value `Store` protocol (`lauren.stores`: bounded `MemoryStore`, `Lock`, `StoresModule.for_root(default=..., named=...)` with lifecycle-managed per-app stores; any `Store` works as `SessionConfig(store=..., revocation_store=...)`) behind the `Cache` service (`lauren.cache`: `CacheModule.for_root()`, `get_or_set` with single-flight / `lock=` / `stale_ttl=`, typed decode, O(1) tag invalidation), `@cache_response(ttl=..., vary=..., key=...)` server-side response caching (GET/HEAD, guards first, never stores `Set-Cookie` / private / streamed responses) and `@idempotent()` `Idempotency-Key` handling (replay, 409 in flight, 422 on mismatch). Missing `CacheModule` fails startup.
- **Typed configuration** — settings classes (frozen dataclasses, pydantic models, `msgspec.Struct`) loaded by `ConfigModule.for_root(...)` / `for_feature(...)` from `.env`, `.env.{profile}`, TOML/YAML, secret files and the environment (which always wins; `os.environ` is never written), validated inside `LaurenFactory.create` with one aggregated `ConfigValidationError` listing every key and source, injected through DI; `Secret[T]` is redacted in `repr`, logs, JSON and errors; profiles via `LAUREN_PROFILE`.
- **Pagination** — `paging: OffsetPagination` (or `PageNumberPagination`, or `CursorPagination[C]`) plus a `Page[T]` / `CursorPage[T]` return value: parameters validated with hard maxima (422 on `loc: ["query", "limit"]`), one envelope per app (`items` + `meta`, JSON:API-style `data` + `links`, or a bare array), RFC 8288 `Link` and `X-Total-Count` headers built from the request URL, opaque HMAC-signed cursors with key rotation and expiry (a tampered cursor is a 400 `invalid_cursor`), `Page.from_rows` / `CursorPage.from_rows` for the `limit + 1` probe, `paginate_sequence` for in-memory lists, and named OpenAPI components (`UserOutPage`). Configured by `PaginationConfig` app-wide or per parameter (`Annotated[...]`).
- **Content-type policy** — `ContentConfig(strict_content_type=True)` rejects non-JSON `Content-Type`s on JSON body parameters with 415 (session apps default to `"warn"`).
- **Static file serving** — `StaticFilesModule.for_root("/assets", directory="./public")` as a feature module; dotfiles hidden by default (`dotfiles=`).
- **Hosted services & scheduling** — `@hosted_service()` classes whose `run(ctx)` the framework starts after `@post_construct`, supervises (restart policy, exponential backoff, health) and stops in reverse order before `@pre_destruct`; `@interval` / `@cron` (stdlib cron with IANA zones and a defined DST policy) / `@run_once` on singleton methods with overlap control, jitter, timeouts, misfire handling and a fresh DI scope per run, enabled by `ScheduleModule.for_root()`; `SchedulerRegistry` for status and `trigger()`; `exclusive=True` + a pluggable `SchedulerLock` for one runner across workers; `ManualClock` for tests.
- **Standalone contexts & commands** — `LaurenFactory.create_context(AppModule)` (or `app.context()`) runs the server's DI graph and lifecycle without HTTP (`async with ... as ctx:`; `ctx.get`, `ctx.scope()`, hosted services off by default); `@command("db:seed")` turns a provider method into a CLI command whose arguments come from its signature (stdlib `argparse`, validated at startup) and that runs in its own DI scope — `python -m lauren exec app.main:AppModule db:seed --count 3`, `python -m lauren commands`, `python -m lauren console`, and `await ctx.run_command(...)` in tests. Unscoped resolution of request-scoped providers warns once (`strict_request_scope=True` raises). Never expose admin tasks as HTTP endpoints.
- **Background tasks** — `BackgroundTasks` for fire-and-forget work after the response; `add_task` returns a `TaskHandle` (`status`, `done`, `cancel()` for tasks that have not started, `await result()` from another coroutine).
- **Closed error catalog** with stable `code` strings and the everyday HTTP errors built in (`BadRequestError`, `NotFoundError`, `ConflictError`, `GoneError`, `PreconditionFailedError`, `TooManyRequestsError`, `ServiceUnavailableError`, `RequestTimeoutError`, …); any error carries `headers=` (`UnauthorizedError(challenge=…)`, `retry_after=`, `allow=`). Consistent `{error: {code, message, detail}}` envelope, or RFC 9457 problem details with `error_format="rfc9457"` (`problem_type_base=`); `error_catalog()` lists the catalog as data. Guards pick their denial with `deny_error = UnauthorizedError`.
- **Debug error pages** — `LaurenFactory.create(..., debug=True | DebugConfig(...))`: traceback, route chain and a redacted request summary for unhandled errors, as HTML or a JSON `debug` member; loopback-only by default, warned about at startup, refused in a production profile.
- **Testing toolkit** (`lauren.testing`) — `TestClient` with one persistent event loop per client, `with TestClient(app)` lifespan (startup on enter, full shutdown on exit), a browser-like cookie jar, `data=` / `files=` / streamed bodies, `client.stream(...)` with `iter_sse()`, simulated client aborts (`disconnect_after=`), `raise_server_exceptions`, `AsyncTestClient`, `Test.create_testing_module(AppModule).override_provider(X).use_value(fake).compile()` (startup-validated provider / guard / interceptor / middleware / exception-handler / module overrides), and an opt-in pytest plugin.
- **Native validation, pydantic-optional** — stdlib models (dataclass, `TypedDict`, `NamedTuple`) are validated natively: nested models, `__post_init__`, `Annotated` constraints (`Ge`, `Le`, `MinLen`, `MaxLen`, `Pattern` from `lauren.validation`), `@validation_config(extra="forbid")`. Pydantic and `msgspec.Struct` keep their own validators. Every 422 — HTTP, WebSocket or stream — carries structured `detail.issues` (`{loc, type, msg}`) for every failing parameter at once; `ValidationPolicy(stdlib_models="warn" | "enforce" | "legacy")` controls the 1.x migration; `TypeCompiler` / `app.type_compiler` validate outside requests.

## LaurenFactory.create() parameters

`LaurenFactory.create(root_module, *, strict_lifecycle=True, global_middlewares=None, global_guards=None, global_interceptors=None, global_ws_guards=None, global_ws_interceptors=None, global_exception_handlers=None, global_providers=None, max_body_size=1048576, app_state=None, logger=None, openapi_info=None, openapi_servers=None, openapi_security_schemes=None, openapi_url=None, docs_url=None, redoc_url=None, arena=None, arena_capacity=None, json_encoder=None, signals=None, error_format="default", root_path="", mounts=None, sessions=None, content=None, problem_type_base=None, debug=False, lazy_startup=True, hosted_services=True, scheduler=True, validation=None, strict_request_scope=False, response_validation="off", json_fallback="warn", pagination=None)`

`LaurenFactory.create_context(root_module, *, hosted_services=False, scheduler=False, drain_timeout=10.0, **create_kwargs)` builds the same app wrapped in an `ApplicationContext` for scripts, commands and workers.

See [full reference](llms-full.txt) for complete parameter documentation.

## Companion packages

- **lauren-middlewares** — CORS, rate limit, GZip, security headers, request id, trusted hosts, request log, HTTPS redirect, body size limit, timeout. Each as a factory function returning a `@middleware()`-decorated class.
- **lauren-logging** — Configurable logging module modelled after NestJS's `LoggerModule.forRoot`. Processor pipeline, contextvars binding, request-logging middleware, pluggable backends (stdlib, structlog, console, file, fan-out, queue, …).
- **lauren-guards** — Authentication and authorization guards: `bearer_token`, `jwt_bearer`, `api_key`, `basic_auth`, `oauth2_introspection`, `session_cookie`, `require_authenticated`, `require_roles`, `require_scopes`, `csrf`, `ip_allowlist`.

## Documentation for LLMs

- [Docs MCP server](https://lauren-py.dev/mcp): `https://lauren-py.dev/api/mcp` (Streamable HTTP, read-only) — tools `search_docs`, `get_doc`, `list_docs`, `get_reference`.
- [Full reference](llms-full.txt): complete API surface, all decorators, extractors, response factories, types, errors, and examples — including WebSocket gateways, SSE, structured streaming, custom providers, custom responses, and exception handlers.

## Examples

- [Quickstart](llms-full.txt#minimal-example): routing + DI + extractors in 20 lines.
- [WebSocket gateways](llms-full.txt#websockets): `@ws_controller`, `@on_message`, broadcast.
- [Server-Sent Events](llms-full.txt#server-sent-events-eventstream): `EventStream` with keep-alive and resumability.
- [Typed streaming](llms-full.txt#typed-streaming): `StreamingResponse[T]` with content negotiation.
- [Custom providers](llms-full.txt#custom-providers): `use_value` / `use_class` / `use_factory` / `use_existing`.
- [Standalone context & commands](llms-full.txt#standalone-context--commands): `create_context`, `@command`, `python -m lauren exec`.
- [Custom extractors](llms-full.txt#custom-extractors-plugin-api): creating domain extractors like `CurrentUser`.
- [Responses](llms-full.txt#types-request--response--state--headers): subclassing `Response`, file downloads, XML output.
- [Guards & middleware](llms-full.txt#guards--middleware): applying authorization at class or route level.
- [Exception handlers](llms-full.txt#exception-handlers): typed error → response mapping.
- [Interceptors](llms-full.txt#interceptors): timing, caching, response transforms via `CallHandler`.
- [Validation](llms-full.txt#validation): native stdlib-model validation, constraint markers, the `issues` payload, `ValidationPolicy`, `TypeCompiler`.
- [Pipes](llms-full.txt#pipes): parameter-level transforms and validation.
- [Application events](llms-full.txt#application-events): `@on_event` handlers, `EventBus` / `EventPublisher`, delivery modes.
- [Health & readiness](llms-full.txt#health--readiness): `HealthModule.for_root()`, `@health_check`, probe semantics, draining.
- [Sub-application mounting](llms-full.txt#mounting-sub-applications): composing multiple ASGI apps.
- [Background tasks](llms-full.txt#background-tasks): fire-and-forget work after response.
- [Hosted services & scheduling](llms-full.txt#hosted-services--scheduling): `@hosted_service`, `@interval` / `@cron` / `@run_once`, `ScheduleModule`, `SchedulerRegistry`, `ManualClock`.
- [Pagination](llms-full.txt#pagination): `OffsetPagination` / `CursorPagination[C]`, `Page[T]` / `CursorPage[T]`, envelopes, `Link` headers, signed cursors.
- [Sessions](llms-full.txt#sessions): signed-cookie sessions with `session: Session` injection and a pluggable store.
- [Stores & caching](llms-full.txt#stores--caching): `Store` / `MemoryStore`, `Cache.get_or_set`, `@cache_response`, `@idempotent`, locks.
- [Typed configuration](llms-full.txt#configuration): `ConfigModule.for_root(Settings)`, layered sources, profiles, `Secret[T]`, `load_settings`.
