Metadata-Version: 2.4
Name: lauren
Version: 1.11.0
Summary: An ASGI web framework with dependency injection, metadata-driven routing, and modular architecture.
Author: Platform Engineering Team
Maintainer: Platform Engineering Team
License-Expression: MIT
Project-URL: Homepage, https://lauren-py.dev
Project-URL: Documentation, https://lauren-py.dev/docs
Project-URL: Repository, https://github.com/lauren-framework/lauren-framework
Project-URL: Issues, https://github.com/lauren-framework/lauren-framework/issues
Project-URL: Changelog, https://github.com/lauren-framework/lauren-framework/releases
Keywords: asgi,web,framework,async,di,dependency-injection,nestjs,axum,fastapi,openapi,routing
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: anyio>=4.0
Provides-Extra: pydantic
Requires-Dist: pydantic>=2.0; extra == "pydantic"
Provides-Extra: msgspec
Requires-Dist: msgspec>=0.18; extra == "msgspec"
Provides-Extra: yaml
Requires-Dist: pyyaml>=6.0; extra == "yaml"
Provides-Extra: full
Requires-Dist: pydantic>=2.0; extra == "full"
Requires-Dist: msgspec>=0.18; extra == "full"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: pytest-mock; extra == "dev"
Requires-Dist: httpx; extra == "dev"
Requires-Dist: hypothesis>=6.0; extra == "dev"
Requires-Dist: uvicorn>=0.29; extra == "dev"
Requires-Dist: sqlalchemy>=2.0; extra == "dev"
Requires-Dist: sqlmodel>=0.0.14; extra == "dev"
Requires-Dist: pyjwt>=2.0; extra == "dev"
Requires-Dist: cryptography>=40.0; extra == "dev"
Requires-Dist: pyotp>=2.0; extra == "dev"
Requires-Dist: passlib[bcrypt]; extra == "dev"
Requires-Dist: fakeredis>=2.0; extra == "dev"
Requires-Dist: pillow>=10.0; extra == "dev"
Requires-Dist: openpyxl>=3.0; extra == "dev"
Requires-Dist: prometheus-client>=0.17; extra == "dev"
Requires-Dist: opentelemetry-api>=1.0; extra == "dev"
Requires-Dist: opentelemetry-sdk>=1.0; extra == "dev"
Provides-Extra: test
Requires-Dist: pytest>=8.0; extra == "test"
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
Requires-Dist: httpx>=0.27; extra == "test"
Requires-Dist: hypothesis>=6.0; extra == "test"
Requires-Dist: openapi-spec-validator>=0.7; extra == "test"
Requires-Dist: pytest-cov>=5.0; extra == "test"
Provides-Extra: docs
Requires-Dist: mkdocs>=1.6; extra == "docs"
Requires-Dist: mkdocs-material>=9.5; extra == "docs"
Requires-Dist: mkdocstrings[python]>=0.27; extra == "docs"
Requires-Dist: pymdown-extensions>=10.7; extra == "docs"
Requires-Dist: griffe>=1.0; extra == "docs"
Dynamic: license-file

<p align="center">
  <img src="https://raw.githubusercontent.com/lauren-framework/lauren-assets/refs/heads/main/framework/lauren-logo-only.png" width=40%></img>
</p>
<div align="center">
  <h1><i>lauren</i></h1>
</div>
<p align="center">
    <em>lauren framework: a metadata-first Python web framework. NestJS-style modules, Axum-style extractors, FastAPI-class ergonomics &mdash; resolved into an immutable execution graph at startup.</em>
</p>
<p align="center">
<a href="https://github.com/lauren-framework/lauren-framework/actions/workflows/tests.yml?query=branch%3Amain+event%3Apush">
    <img src="https://github.com/lauren-framework/lauren-framework/actions/workflows/tests.yml/badge.svg?branch=main&event=push" alt="Test">
</a>
<a href="https://github.com/lauren-framework/lauren-framework/actions/workflows/lint.yml?query=branch%3Amain+event%3Apush">
    <img src="https://github.com/lauren-framework/lauren-framework/actions/workflows/lint.yml/badge.svg?branch=main&event=push" alt="Lint">
</a>
<a href="https://github.com/lauren-framework/lauren-framework/actions/workflows/codeql.yml?query=branch%3Amain">
    <img src="https://github.com/lauren-framework/lauren-framework/actions/workflows/codeql.yml/badge.svg?branch=main" alt="CodeQL">
</a>
<a href="https://codecov.io/gh/lauren-framework/lauren-framework">
    <img src="https://img.shields.io/codecov/c/github/lauren-framework/lauren-framework?color=%2334D058&label=coverage" alt="Coverage">
</a>
<a href="https://pypi.org/project/lauren">
    <img src="https://img.shields.io/pypi/v/lauren?color=%2334D058&label=pypi%20package" alt="Package version">
</a>
<a href="https://pypi.org/project/lauren">
    <img src="https://img.shields.io/pypi/pyversions/lauren.svg?color=%2334D058" alt="Supported Python versions">
</a>
<a href="https://pypi.org/project/lauren">
    <img src="https://img.shields.io/pypi/dm/lauren.svg?color=%2334D058&label=downloads" alt="Downloads">
</a>
<a href="https://github.com/lauren-framework/lauren-framework/blob/main/LICENSE">
    <img src="https://img.shields.io/github/license/lauren-framework/lauren-framework.svg?color=%2334D058" alt="License">
</a>
<a href="https://github.com/astral-sh/ruff">
    <img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
</a>
<a href="https://mypy.readthedocs.io/en/stable/">
    <img src="https://img.shields.io/badge/types-mypy-blue.svg" alt="Checked with mypy">
</a>
<a href="https://github.com/j178/prek">
    <img src="https://img.shields.io/badge/pre--commit-prek-FAB040.svg?logo=pre-commit&logoColor=white" alt="prek">
</a>
<a href="https://github.com/lauren-framework/lauren-framework/discussions">
    <img src="https://img.shields.io/github/discussions/lauren-framework/lauren-framework?color=%2334D058&label=discussions" alt="Discussions">
</a>
<a href="https://github.com/lauren-framework/lauren-framework/stargazers">
    <img src="https://img.shields.io/github/stars/lauren-framework/lauren-framework.svg?style=social&label=Star" alt="GitHub Stars">
</a>
</p>

---

**Documentation**: <a href="https://lauren-py.dev" target="_blank">https://lauren-py.dev</a>

**Docs MCP server** (for AI coding agents): `https://lauren-py.dev/api/mcp` &mdash; <a href="https://lauren-py.dev/mcp" target="_blank">setup guide</a>

**Source Code**: <a href="https://github.com/lauren-framework/lauren-framework" target="_blank">https://github.com/lauren-framework/lauren-framework</a>

---

## For AI Agents & Coding Assistants

### Install all skills in one command

```bash
# Claude Code, Cursor, Copilot, Continue, Codex CLI — auto-detected
npx skills add lauren-framework/lauren-framework
```

This copies all 60+ SKILL.md context packs into your agent's global skills
directory (`~/.claude/skills/`, `~/.cursor/skills/`, etc.).  The next time your
agent opens a Lauren project it has pre-loaded expertise on auth, database,
API patterns, observability, security, and more.

| Resource | What it contains |
|---|---|
| [`lauren/llms.txt`](https://raw.githubusercontent.com/lauren-framework/lauren-framework/refs/heads/main/lauren/llms.txt) | Framework overview — start here |
| [`lauren/llms-full.txt`](https://raw.githubusercontent.com/lauren-framework/lauren-framework/refs/heads/main/lauren/llms-full.txt) | Complete reference — all APIs, patterns, common errors |
| [`AGENTS.md`](https://github.com/lauren-framework/lauren-framework/blob/main/AGENTS.md) | Agent rules, by-task lookup, file ownership, common errors, definition of done |
| [`CLAUDE.md`](https://github.com/lauren-framework/lauren-framework/blob/main/CLAUDE.md) | Conventions, commands, golden rules, pattern selection guide |
| [`skills/`](https://github.com/lauren-framework/lauren-framework/tree/main/skills/) | 60+ copy-paste skill guides covering every common task |

### Connect your agent to the live docs (MCP)

The docs site hosts a read-only MCP server so agents can search the current
documentation instead of guessing from training data:

```bash
claude mcp add --transport http lauren-docs https://lauren-py.dev/api/mcp
```

Configurations for Cursor, VS Code, Windsurf, Gemini CLI and stdio-only clients:
[lauren-py.dev/mcp](https://lauren-py.dev/mcp).

Full install guide and skills index: [docs/guides/agent-skills.md](https://github.com/lauren-framework/lauren-framework/blob/main/docs/guides/agent-skills.md)

---

lauren is a modern, high-performance Python web framework for building APIs
that need to **fail at startup, not in production**. It is built on these
core ideas:

* **Metadata-first.** Routes, dependency-injection bindings, module
  boundaries, lifecycle hooks, middleware, and guards are declared with
  decorators that *attach metadata*. They never rewrite your functions.
* **Startup-validated, runtime-pure.** Every misuse — circular DI, missing
  module export, malformed extractor, conflicting routes — is rejected
  inside `LaurenFactory.create(...)`, not on the first request.
* **No reflection on the request path.** The whole graph is compiled into
  immutable structures at startup; serving a request is pure traversal.
* **AI-ready by default.** Public surface is mirrored in
  [`llms.txt` / `llms-full.txt`](https://lauren-py.dev/llms.txt) and exported
  in `__all__` — a CI hook keeps the two in lock-step.

## Features

* **Fast** — Zero per-request reflection:
  routes, DI graph, extractors, and middleware are fully compiled at startup.
* **Implicit extractors** — Path params, query strings, and JSON bodies are
  auto-detected from type annotations. Write `id: int`, `q: str`,
  `body: MyModel` without boilerplate unless you need the explicit form.
  `Query[T]` and `Json[T]` support Pydantic models, `msgspec.Struct`,
  Python `dataclass`, and `TypedDict` types — stdlib models are validated
  natively without pydantic (nested models, `Annotated[int, Ge(13)]`
  constraints), and a 422 lists every failing field as structured `issues`.
* **Pydantic-free discriminated unions** — `Discriminated[Cat | Dog, "kind"]`
  routes tagged-union JSON bodies to the correct variant class using only
  stdlib. Works with `@dataclass`, `TypedDict`, `msgspec.Struct`, and
  `pydantic.BaseModel`. OpenAPI emits `oneOf` + `discriminator.mapping`
  automatically.
* **Three-scope DI** — `SINGLETON`, `REQUEST`, and `TRANSIENT` scopes with
  Protocol bindings, multi-bindings (`list[T]`), and NestJS-style custom
  providers (`use_value` / `use_class` / `use_factory` / `use_existing`).
* **Pipes** — Post-extraction value transforms: validate, coerce, or enrich
  parameters before they reach the handler. Function-based, class-based,
  chainable, and DI-aware.
* **Guards, Middleware & Interceptors** — All three attachment points. Guards
  run first (allow/deny); middleware wraps raw request/response bytes; interceptors
  wrap handler execution for timing, caching, and response transforms.
  `@use_guards` and `@use_interceptors` work on both HTTP `@controller` and
  WebSocket `@ws_controller` classes natively.
* **`@propagate_metadata`** — Copy `@use_guards`, `@use_interceptors`,
  `@use_middlewares`, `@use_encoder`, and `@set_metadata` annotations from a
  source class or function to a target — the `functools.wraps` equivalent for
  Lauren's decorator metadata.
* **`lauren.reflect`** — Full metadata introspection API: static readers
  (`reflect_controller`, `reflect_routes`, `reflect_guards`, …), app-level
  readers (`get_all_routes`, `get_all_ws_gateways`, `get_route_metadata`), and
  rich result types (`ReflectedRoute`, `ReflectedController`, …). All readers
  honour the own-class rule — no metadata inheritance.
* **WebSockets** — First-class `@ws_controller` gateways with typed validated
  frames, discriminated-union dispatch (`Discriminated[A | B, "key"]`),
  `BroadcastGroup` rooms, and native guard/interceptor enforcement via
  `WsConnectionContext`.
* **Server-Sent Events** — `EventStream` with keep-alive heartbeats and
  `Last-Event-ID` resumability for AI token-streaming patterns.
* **Typed streaming** — `StreamingResponse[T]` auto-negotiates between SSE,
  NDJSON, and JSON Lines based on the client's `Accept` header.
* **Response contracts** — `status_code=` and typed results (`Ok[T]`,
  `Created[T]`, `Accepted[T]`, `NoContent`) declare a route's statuses on the
  wire and in OpenAPI. `response_validation="filter"` projects the returned
  value onto the annotated type, so returning a `UserInDB` (or an ORM row) from
  a route annotated `-> UserOut` never leaks `password_hash`; `"strict"` also
  validates it. `@raises(NotFoundError)` documents error responses.
* **Pagination** — `paging: OffsetPagination` (or `PageNumberPagination`,
  `CursorPagination[C]`) and a `Page[T]` / `CursorPage[T]` return value give
  validated parameters with hard maxima, one envelope per app, RFC 8288 `Link`
  headers, opaque HMAC-signed cursors with key rotation, and named OpenAPI
  components.
* **Custom responses & file delivery** — Return your own `Response` subclasses,
  stream downloads with `await Response.file(...)`, or emit XML with
  `Response.xml(...)` while keeping the dispatch pipeline untouched.
* **Background tasks** — `BackgroundTasks` extractor fires work after the
  response is sent. `TaskHandle` exposes `status`, `done`, `cancel()` (before
  the task starts) and `await result()` (from another coroutine). Signals
  notify on start, complete, and failure.
* **Application events** — typed domain events between modules: `@on_event`
  handlers on any provider, discovered and validated at startup; publish with
  the singleton `EventBus` or the request-scoped `EventPublisher` (inline
  handlers share the request's DI scope). Inline, concurrent or
  after-the-response delivery, priorities, `propagate` / `isolate` error
  policies, shutdown draining, and `recording()` / `drain()` for tests.
* **Hosted services & scheduling** — `@hosted_service()` long-running loops
  that start after `@post_construct`, restart with backoff and stop before
  `@pre_destruct`; `@interval` / `@cron` / `@run_once` jobs on singleton
  methods with overlap control, jitter, timeouts, misfire handling, a DI scope
  per run, IANA time zones and a `ManualClock` for tests.
* **Commands & scripts** — `LaurenFactory.create_context(AppModule)` runs the
  server's DI graph and lifecycle without HTTP; `ctx.scope()` gives scripts a
  request scope with deterministic teardown. `@command("db:seed")` turns a
  provider method into a CLI command — arguments derived from its signature,
  validated at startup — run with `python -m lauren exec` and tested with
  `await ctx.run_command(...)`.
* **Sessions** — first-class signed-cookie sessions via
  `LaurenFactory.create(..., sessions=SessionConfig(...))`. A mutable
  `session: Session` is injected into handlers at zero per-request cost; the
  pluggable async `SessionStore` ships with an in-memory backend and a
  stateless `SignedCookieSessionStore`. HMAC-signed, `HttpOnly`/`Secure`/
  `SameSite=Lax` by default, with `regenerate_id()` and `invalidate()`.
  Unsafe configuration is rejected at startup. Pair with
  `content=ContentConfig(strict_content_type=True)` so cross-site `text/plain`
  posts to JSON routes are rejected (415).
* **Stores & caching** — one async key-value `Store` protocol (bounded
  `MemoryStore` in core; Redis and friends plug in) behind sessions, session
  revocation, the `Cache` service (`get_or_set` with single-flight, tag
  invalidation, stale-while-revalidate), `@cache_response` server-side
  response caching with correct-by-default rules, and `@idempotent`
  `Idempotency-Key` handling.
* **Typed configuration** — settings classes (frozen dataclasses, pydantic
  models, `msgspec.Struct`) loaded by `ConfigModule.for_root(...)` from `.env`,
  TOML and the environment, validated inside `LaurenFactory.create` — one
  `ConfigValidationError` lists every missing or invalid key with its source —
  and injected through DI. `Secret[T]` is redacted in logs, JSON and errors.
* **Testing toolkit** — `with TestClient(app)` runs startup and the full
  shutdown on one persistent event loop, keeps a browser-like cookie jar, sends
  forms / uploads / streamed bodies, reads endless streams event by event and
  simulates client aborts. `Test.create_testing_module(AppModule)
  .override_provider(X).use_value(fake).compile()` swaps any provider, guard,
  middleware or module and is validated by the real factory. `AsyncTestClient`
  and an opt-in pytest plugin round it out.
* **Static files** — `StaticFilesModule.for_root("/assets", directory="./public")`
  with ETag caching, `Cache-Control`, path-traversal protection, and dotfiles
  (`.env`, `.git/`) hidden by default (`/.well-known/` still served).
* **Socket.IO** — Engine.IO v4 / Socket.IO v5 adapter via `@socketio_controller`.
* **Stable error contract** — A closed error catalog with stable `code`s and
  the everyday HTTP errors built in (`NotFoundError`, `ConflictError`,
  `TooManyRequestsError`, `ServiceUnavailableError`, …). Errors carry headers
  (`UnauthorizedError(challenge=...)`, `retry_after=`), render as Lauren's
  `{"error": {...}}` envelope or RFC 9457 problem details
  (`error_format="rfc9457"`), and `debug=True` serves development error pages
  with the traceback and route chain.
* **OpenAPI 3.1** — Automatic schema generation from Pydantic models,
  dataclasses, `TypedDict`, and `Discriminated` unions. Swagger UI and ReDoc
  served out of the box.
* **Health & readiness probes** — `HealthModule.for_root()` serves
  `/health/live`, `/health/ready`, `/health/startup` and a full report from the
  public `app.lifecycle_state` and `@health_check` methods on your singletons:
  per-check timeouts, concurrency, single-flight and caching, details hidden by
  default, and `app.begin_draining()` to fail readiness before the socket closes.
* **Lifecycle signals** — `SignalBus` with `StartupBegin`, `StartupComplete`,
  `ShutdownBegin`, `RequestReceived`, `RequestComplete`, `LifecycleStateChanged`,
  and more.
* **Standards-based** — Built on [ASGI](https://asgi.readthedocs.io/) and
  [anyio](https://anyio.readthedocs.io/). Pydantic is optional (`pip install
  "lauren[pydantic]"`).

## Requirements

Python **3.11**, **3.12**, **3.13**, and **3.14** are supported. Hard dependencies:

* [anyio](https://anyio.readthedocs.io/) — async backend and thread-pool offload for sync handlers.

Optional extras:

* `pip install "lauren[pydantic]"` — adds `pydantic>=2.0` for Pydantic-backed validation and `PydanticEncoder`.
* `pip install "lauren[msgspec]"` — adds `msgspec>=0.18` for struct-based serialisation.
* `pip install "lauren[full]"` — installs both.

## Installation

```bash
pip install lauren
# with an ASGI server:
pip install "uvicorn[standard]"
# or granian (faster on CPython):
pip install granian
```

## Quick start

```python
from pydantic import BaseModel
from lauren import LaurenFactory, controller, get, post, module, use_guards
from lauren.types import ExecutionContext


class CreateUser(BaseModel):
    name: str
    age: int


class AdminGuard:
    async def can_activate(self, ctx: ExecutionContext) -> bool:
        return ctx.request.headers.get("x-role") == "admin"


@controller("/users", tags=["users"])
class UserController:
    @get("/{id}")
    async def get_user(self, id: int) -> dict:
        return {"id": id, "found": True}

    @post("/", status_code=201)
    async def create(self, body: CreateUser) -> dict:
        return body.model_dump()

    @get("/admin")
    @use_guards(AdminGuard)
    async def admin_only(self) -> dict:
        return {"access": "granted"}


@module(controllers=[UserController])
class AppModule:
    pass


app = LaurenFactory.create(AppModule, docs_url="/docs")
```

```bash
uvicorn main:app --reload
# → http://127.0.0.1:8000/docs  (Swagger UI)
```

## Dependency injection

```python
from lauren import injectable, post_construct, pre_destruct, Scope

@injectable(scope=Scope.SINGLETON)
class UserRepository:
    @post_construct
    async def warm(self) -> None:
        self.cache: dict[int, str] = {}

    @pre_destruct
    async def flush(self) -> None:
        self.cache.clear()

    async def find(self, user_id: int) -> str | None:
        return self.cache.get(user_id)

    async def upsert(self, user_id: int, name: str) -> None:
        self.cache[user_id] = name


@controller("/users")
class UserController:
    def __init__(self, repo: UserRepository) -> None:
        self.repo = repo

    @get("/{id}")
    async def get_user(self, id: int) -> dict:
        name = await self.repo.find(id)
        if name is None:
            return {"id": id, "found": False}, 404
        return {"id": id, "name": name}


@module(providers=[UserRepository], controllers=[UserController])
class AppModule:
    pass
```

## Modules

Modules are the unit of feature composition. Each module declares what it
provides, what it exports, and what it imports from other modules — similar
to NestJS:

```python
@module(
    imports=[DatabaseModule, AuthModule],
    controllers=[UserController, ProfileController],
    providers=[UserService, EmailService],
    exports=[UserService],
)
class UsersModule:
    pass
```

Circular dependency detection, missing export errors, and scope violations
are all caught at startup — before your first request.

## SSE / streaming

```python
from lauren import EventStream, ServerSentEvent, get

@get("/events")
async def stream(self) -> EventStream:
    async def generate():
        for i in range(10):
            yield ServerSentEvent(data=f"tick {i}", event="tick")
            await asyncio.sleep(1)
        yield ServerSentEvent(data="done", event="close")
    return EventStream(generate(), keep_alive=15.0)
```

For typed, content-negotiated streams (SSE / NDJSON / JSON Lines):

```python
from pydantic import BaseModel
from lauren import StreamingResponse

class Tick(BaseModel):
    seq: int

@get("/ticks")
async def ticks(self) -> StreamingResponse[Tick]:
    async def gen():
        for i in range(100):
            yield Tick(seq=i)
            await asyncio.sleep(0.05)
    return StreamingResponse(gen())
```

## Static files

```python
from lauren.static_files import StaticFilesModule

@module(imports=[StaticFilesModule.for_root("/assets", directory="./public")])
class AppModule:
    pass
```

Or mount any ASGI sub-app:

```python
app = LaurenFactory.create(AppModule)
app.mount("/static", StaticFiles(directory="static"))
```

## Sessions

```python
from lauren import LaurenFactory, SessionConfig, Session, controller, get, post, module

@controller("/account")
class AccountController:
    @get("/visits")
    async def visits(self, session: Session) -> dict:
        session["visits"] = session.get("visits", 0) + 1
        return {"visits": session["visits"]}

    @post("/login")
    async def login(self, session: Session) -> dict:
        session.regenerate_id()        # session-fixation defence
        session["user_id"] = "u-42"
        return {"ok": True}

    @post("/logout")
    async def logout(self, session: Session) -> dict:
        session.invalidate()
        return {"ok": True}

@module(controllers=[AccountController])
class AppModule:
    pass

app = LaurenFactory.create(
    AppModule,
    sessions=SessionConfig(secret="a-long-random-secret"),
)
```

The cookie is HMAC-signed and secure by default (`HttpOnly`, `Secure`,
`SameSite=Lax`). Swap `InMemorySessionStore` for the stateless
`SignedCookieSessionStore` or any shared key-value `Store` (adapted
automatically) in production.
Unsafe configuration fails inside `LaurenFactory.create`, not at runtime.

## Caching

```python
from lauren import LaurenFactory, controller, get, injectable, module, post
from lauren.cache import Cache, CacheModule, cache_response, idempotent

@injectable()
class Products:
    def __init__(self, cache: Cache) -> None:
        self._cache = cache

    async def get(self, pid: int) -> dict:
        return await self._cache.get_or_set(
            f"product:{pid}", lambda: load_product(pid), ttl=60, tags=(f"product:{pid}",)
        )

@controller("/products")
class ProductController:
    def __init__(self, products: Products) -> None:
        self._products = products

    @get("/{pid}")
    @cache_response(ttl=30, vary=("accept-language",))   # server-side; guards still run first
    async def show(self, pid: int) -> dict:
        return await self._products.get(pid)

    @post("/{pid}/orders")
    @idempotent(ttl=24 * 3600)                           # Idempotency-Key retries run once
    async def order(self, pid: int) -> dict:
        return {"ordered": pid}

@module(imports=[CacheModule.for_root()], providers=[Products], controllers=[ProductController])
class AppModule:
    pass
```

Concurrent misses share one computation, `cache.invalidate_tags(...)` evicts in
O(1) without scanning keys, and a cached response never replays a `Set-Cookie`.
Every stateful feature runs on one `Store`: `StoresModule.for_root(default=...)`
plus `SessionConfig(store=...)` share a single backend.

## Configuration

```python
from dataclasses import dataclass

from lauren import LaurenFactory, injectable, module
from lauren.config import ConfigModule, Secret

@dataclass(frozen=True)
class DatabaseSettings:
    url: str
    pool_size: int = 10

@dataclass(frozen=True)
class AppSettings:
    session_secret: Secret[str]       # redacted in repr, logs and JSON
    database: DatabaseSettings        # APP_DATABASE__URL, APP_DATABASE__POOL_SIZE
    debug: bool = False               # APP_DEBUG=yes

@injectable()
class Mailer:
    def __init__(self, settings: AppSettings) -> None:
        self.debug = settings.debug

@module(imports=[ConfigModule.for_root(AppSettings, env_prefix="APP_")], providers=[Mailer])
class AppModule:
    pass

app = LaurenFactory.create(AppModule)   # raises one ConfigValidationError listing every bad key
```

Values come from `.env`, `.env.{profile}` and the environment (the environment
always wins; `os.environ` is never written). Profiles, TOML / YAML / secret-file
sources, feature-scoped settings and `load_settings(...)` for factory kwargs:
see the [Typed Configuration guide](https://lauren-py.dev/guides/configuration/).

## Testing

```python
from lauren.testing import Test, TestClient

app = Test.create_testing_module(AppModule).override_provider(Mailer).use_value(FakeMailer()).compile()

with TestClient(app) as client:                 # startup() on enter, shutdown() on exit
    client.post("/account/login")               # Set-Cookie kept in client.cookies
    assert client.get("/account/visits").json() == {"visits": 1}
    with client.stream("GET", "/events") as r:  # endless SSE, event by event
        first = next(r.iter_sse())
```

One event loop per client keeps startup-created resources (pools, `asyncio`
primitives, background tasks) alive across requests. See the
[testing guide](https://lauren-py.dev/guides/testing/).

## Hosted services & scheduling

```python
from lauren import injectable, module
from lauren.scheduling import ScheduleModule, ServiceContext, cron, hosted_service, interval

@hosted_service(restart="on-failure")
class OutboxRelay:
    async def run(self, ctx: ServiceContext) -> None:
        while await ctx.sleep(1.0):          # False once shutdown asks it to stop
            ...                              # relay one batch

@injectable()
class Maintenance:
    @interval(seconds=60, jitter=5)
    async def warm_cache(self) -> None: ...

    @cron("0 3 * * *", tz="Europe/Paris", timeout=300)
    async def purge_sessions(self) -> None: ...

@module(imports=[ScheduleModule.for_root()], providers=[OutboxRelay, Maintenance])
class AppModule:
    pass
```

Services start after `@post_construct` and stop, in reverse order, before
`@pre_destruct`; jobs fire once the app is ready. Bad cron expressions, missing
`ScheduleModule`, non-singleton providers and duplicate names fail at startup.
See the [Scheduling guide](https://lauren-py.dev/guides/scheduling/).

## Commands

Admin and maintenance tasks are commands, not HTTP endpoints:

```python fragment
from typing import Annotated

from lauren import injectable
from lauren.commands import CommandContext, Opt, command

@injectable()
class SeedCommands:
    def __init__(self, db: Database) -> None:
        self._db = db

    @command("db:seed", help="Insert fixture data.")
    async def seed(
        self,
        ctx: CommandContext,
        *,
        count: Annotated[int, Opt(short="-n", help="rows per table")] = 50,
        reset: bool = False,
    ) -> int:
        if reset and not ctx.confirm("Truncate every table?"):
            return 1
        ctx.echo(f"seeded {await self._db.seed(count)} rows")
        return 0
```

```console
$ python -m lauren exec app.main:AppModule db:seed -n 500 --reset --yes
seeded 500 rows
```

The command runs with the app started (`@post_construct` before,
`@pre_destruct` after) in its own DI scope; the exit code comes from the return
value. `python -m lauren commands` lists commands, `python -m lauren console`
opens an async REPL on the app, and `LaurenFactory.create_context(AppModule)`
does the same for your own scripts. See the
[Commands & Scripts guide](https://lauren-py.dev/guides/commands-and-scripts/).

## Performance

Runtime is **pure traversal of pre-compiled structures** — no
`inspect.signature(...)`, no `get_type_hints(...)`, no `isinstance(...)`
walking on the hot path. The DI graph, route table, extractor bindings, and
middleware pipeline are all resolved once at startup. Each request pays only
the cost of dispatching through the already-compiled graph.

## Benchmarks

Cross-framework numbers are produced by a separate harness: `benches/bench.py`
drives every participant through the same workloads, one fresh process per
measurement, and writes `benches/results/frameworks-inprocess.json`. The figures
below are drawn straight from that committed artifact &mdash; median throughput
per workload, then the p50/p95/p99 round trip per participant &mdash; with error
bars spanning the min&ndash;max range across repeats, so the spread sits next to
the median instead of behind it. The latency error bars are capped at 2.7 ms so one
10.5 ms range cannot flatten the other workloads: that single range stops at the
cap and its true maximum is written above a break marker. The client calls
each application through
`httpx.ASGITransport`: no socket, no server process, no TLS. Read them as the
cost of the framework layer itself, not as a deployed-configuration number. The
full tables, the method and the pinned dependency set are on the
[benchmark page](https://lauren-py.dev/comparisons/performance-benchmarks/).

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/lauren-framework/lauren-assets/refs/heads/main/framework/benchmarks/bench-throughput-dark.svg">
  <img alt="Median throughput by workload, one bar per participant, with error bars spanning the min&ndash;max range across the repeats" src="https://raw.githubusercontent.com/lauren-framework/lauren-assets/refs/heads/main/framework/benchmarks/bench-throughput-light.svg">
</picture>

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/lauren-framework/lauren-assets/refs/heads/main/framework/benchmarks/bench-latency-dark.svg">
  <img alt="p50, p95 and p99 round trip per participant, one panel per workload, with error bars spanning the min&ndash;max range across the repeats; colour marks the participant, and the one range past the 2.7 ms cap is cut off with its true maximum written above the break" src="https://raw.githubusercontent.com/lauren-framework/lauren-assets/refs/heads/main/framework/benchmarks/bench-latency-light.svg">
</picture>

## Optional dependencies

| Package | Purpose |
|---|---|
| `uvicorn` / `hypercorn` / `granian` | ASGI server (none bundled — pick one) |
| `pydantic` / `msgspec` | Model validation and OpenAPI for those model kinds (`lauren[pydantic]`, `lauren[msgspec]`, `lauren[full]`) |
| `orjson` | Faster JSON via `OrjsonEncoder` — opt in with `json_encoder=OrjsonEncoder()` or `json_encoder=auto_encoder()` |
| `msgspec` | Alternative fast JSON encoder via `MsgspecEncoder` |
| `pyyaml` | `YamlSource` for typed configuration (`lauren[yaml]`); TOML, `.env` and the environment need nothing extra |
| `httpx` | Only if you prefer `httpx.ASGITransport` in tests — `lauren.testing.TestClient` needs nothing extra |

Form bodies and multipart uploads are parsed in-tree; no `python-multipart` needed.

## Companion packages

| Package | Purpose |
|---|---|
| [`lauren-middlewares`](https://github.com/lauren-framework/lauren-middlewares) | CORS, rate-limit, GZip, security headers, request-id, trusted hosts, HTTPS redirect, body-size limit, timeout |
| [`lauren-logging`](https://github.com/lauren-framework/lauren-logging) | Structured logging module with processor pipeline, contextvars binding, pluggable backends (stdlib, structlog, file, fan-out) |
| [`lauren-guards`](https://github.com/lauren-framework/lauren-guards) | Auth guards: JWT bearer, API key, basic auth, OAuth2 introspection, session cookie, RBAC/ABAC, CSRF, IP allowlist |

## Deployment

lauren is a standard ASGI application — deploy exactly like FastAPI or Starlette:

```bash
# Uvicorn
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

# Granian (Rust-based, faster on CPython)
granian --interface asgi main:app --host 0.0.0.0 --port 8000

# Hypercorn (HTTP/2 + HTTP/3)
hypercorn main:app --bind 0.0.0.0:8000
```

Import `HealthModule.for_root()` and point the orchestrator's probes at it —
liveness never checks dependencies, readiness fails before startup completes
and while draining:

```yaml
startupProbe:   {httpGet: {path: /health/startup, port: 8000}, periodSeconds: 2, failureThreshold: 30}
livenessProbe:  {httpGet: {path: /health/live, port: 8000}, periodSeconds: 10}
readinessProbe: {httpGet: {path: /health/ready, port: 8000}, periodSeconds: 5}
lifecycle:
  preStop: {exec: {command: ["sleep", "5"]}}
```

See the [health checks guide](https://lauren-py.dev/guides/health-checks/) for draining and `@health_check`.

## Contributing

We welcome contributions of every size, from typo fixes to whole subsystems.
Read first:

1. [`CONTRIBUTING.md`](https://github.com/lauren-framework/lauren-framework/blob/main/CONTRIBUTING.md) — setup, branch & commit conventions, and the quality bar.
2. [`AGENTS.md`](https://github.com/lauren-framework/lauren-framework/blob/main/AGENTS.md) — the design invariants every PR must respect, whether the author is human or an AI agent.

```bash
uv tool install prek      # one-time
prek install              # wires up the git hook
nox                       # lint + tests + typecheck
```

## License

MIT — see [LICENSE](https://github.com/lauren-framework/lauren-framework/blob/main/LICENSE).
