diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..d4ed901
Binary files /dev/null and b/.DS_Store differ
diff --git a/docs/en/dev/changelog.md b/docs/en/dev/changelog.md
index 539eaba..7175678 100644
--- a/docs/en/dev/changelog.md
+++ b/docs/en/dev/changelog.md
@@ -18,6 +18,13 @@ cut.
### Changed
+- Docs: split `scripts/generate_reference.py` into focused internal modules
+ under `scripts/_reference/` (page definitions, manual page loading, signature
+ formatting, type and default rendering, table and reference validation, and
+ output/check-mode). `npm run docs:reference` and
+ `npm run docs:reference:check` behavior, generated files, and the docs
+ generator's public surface are unchanged.
+
- CLI: split parser setup, command dispatch, local action handling, and remote
action handling into focused internal modules. Command syntax and behavior are
unchanged. ([#167](https://github.com/DevilsAutumn/quater/issues/167))
@@ -405,4 +412,4 @@ No public API is deprecated in this alpha.
- [Known Limitations](/en/dev/known-limitations): current gaps.
- [Stability](/en/dev/stability): version pinning and upgrade expectations.
-- [Quickstart](/en/dev/quickstart): first working app.
+- [Quickstart](/en/dev/quickstart): first working app.
\ No newline at end of file
diff --git a/scripts/_reference/__init__.py b/scripts/_reference/__init__.py
new file mode 100644
index 0000000..aa1a6a8
--- /dev/null
+++ b/scripts/_reference/__init__.py
@@ -0,0 +1 @@
+"""Internal helpers for scripts/generate_reference.py."""
diff --git a/scripts/_reference/manual.py b/scripts/_reference/manual.py
new file mode 100644
index 0000000..13db7df
--- /dev/null
+++ b/scripts/_reference/manual.py
@@ -0,0 +1,40 @@
+"""Loader for manually-authored reference pages.
+
+If every reference file exists and none carries the generated header, the
+generator hands the manual content back unchanged (after checking each
+public symbol has an anchor on its assigned page).
+"""
+
+from __future__ import annotations
+
+from collections.abc import Mapping
+from pathlib import Path
+
+from _reference.pages import PAGES, ReferencePage
+from _reference.paths import GENERATED_HEADER, REFERENCE_DIR
+from _reference.types import symbol_anchor
+
+
+def read_manual_reference(
+ public_api: tuple[str, ...],
+ pages_by_symbol: Mapping[str, ReferencePage],
+) -> dict[Path, str] | None:
+ paths = {REFERENCE_DIR / "index.md", *(page.path for page in PAGES)}
+ outputs: dict[Path, str] = {}
+ for path in paths:
+ if not path.exists():
+ return None
+ content = path.read_text(encoding="utf-8")
+ if content.startswith(GENERATED_HEADER):
+ return None
+ outputs[path] = content
+
+ for name in public_api:
+ page = pages_by_symbol[name]
+ content = outputs[page.path]
+ anchor = symbol_anchor(name)
+ if anchor not in content:
+ raise SystemExit(
+ f"Manual reference page {page.path} does not document {name!r}"
+ )
+ return outputs
diff --git a/scripts/_reference/output.py b/scripts/_reference/output.py
new file mode 100644
index 0000000..507e13a
--- /dev/null
+++ b/scripts/_reference/output.py
@@ -0,0 +1,38 @@
+"""Write generated reference files or diff them in --check mode."""
+
+from __future__ import annotations
+
+import difflib
+import sys
+from collections.abc import Mapping
+from pathlib import Path
+
+from _reference.paths import REFERENCE_DIR
+
+
+def write_outputs(outputs: Mapping[Path, str]) -> None:
+ REFERENCE_DIR.mkdir(parents=True, exist_ok=True)
+ for path, content in sorted(outputs.items()):
+ path.write_text(content, encoding="utf-8")
+
+
+def check_outputs(outputs: Mapping[Path, str]) -> int:
+ stale = False
+ for path, expected in sorted(outputs.items()):
+ if not path.exists():
+ print(f"Missing generated reference file: {path}", file=sys.stderr)
+ stale = True
+ continue
+ actual = path.read_text(encoding="utf-8")
+ if actual == expected:
+ continue
+ stale = True
+ diff = difflib.unified_diff(
+ actual.splitlines(),
+ expected.splitlines(),
+ fromfile=str(path),
+ tofile=f"{path} (generated)",
+ lineterm="",
+ )
+ print("\n".join(diff), file=sys.stderr)
+ return 1 if stale else 0
diff --git a/scripts/_reference/pages.py b/scripts/_reference/pages.py
new file mode 100644
index 0000000..21fa5fe
--- /dev/null
+++ b/scripts/_reference/pages.py
@@ -0,0 +1,164 @@
+"""Reference page definitions and public-API validation."""
+
+from __future__ import annotations
+
+import ast
+import re
+from collections.abc import Iterable
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any
+
+from _reference.paths import PACKAGE_INIT, REFERENCE_DIR
+from _reference.signatures import object_for
+
+MIN_PUBLIC_DOCSTRING_WORDS = 8
+PLACEHOLDER_DOCSTRING_WORDS = frozenset({"todo", "tbd", "fixme", "placeholder"})
+
+
+@dataclass(frozen=True, slots=True)
+class ReferencePage:
+ slug: str
+ title: str
+ description: str
+ symbols: tuple[str, ...]
+
+ @property
+ def path(self) -> Path:
+ return REFERENCE_DIR / f"{self.slug}.md"
+
+
+PAGES: tuple[ReferencePage, ...] = (
+ ReferencePage(
+ slug="application",
+ title="Application",
+ description="App objects, route groups, and configuration.",
+ symbols=("Quater", "RouteGroup", "AppConfig", "CORSConfig", "__version__"),
+ ),
+ ReferencePage(
+ slug="resources",
+ title="Resources",
+ description="Request-scoped resources injected into handlers.",
+ symbols=("Resource",),
+ ),
+ ReferencePage(
+ slug="request",
+ title="Request",
+ description="Request data and state passed through handlers.",
+ symbols=("Request", "State", "FormData", "UploadFile"),
+ ),
+ ReferencePage(
+ slug="parameters",
+ title="Parameters",
+ description="Handler parameter markers for request data binding.",
+ symbols=("Path", "Query", "Body", "Form", "File", "Header", "Cookie"),
+ ),
+ ReferencePage(
+ slug="responses",
+ title="Responses",
+ description="Return values and explicit response classes.",
+ symbols=(
+ "Response",
+ "JSONResponse",
+ "TextResponse",
+ "HTMLResponse",
+ "BytesResponse",
+ "StreamResponse",
+ "RedirectResponse",
+ "EmptyResponse",
+ ),
+ ),
+ ReferencePage(
+ slug="auth",
+ title="Auth and Security",
+ description=(
+ "Auth hooks, approval hooks, framework errors, and signed cookies."
+ ),
+ symbols=(
+ "AuthConfig",
+ "AuthContext",
+ "ApprovalRequest",
+ "ActionApproval",
+ "HTTPError",
+ "ImproperlyConfigured",
+ "SignedCookieSigner",
+ ),
+ ),
+ ReferencePage(
+ slug="observability",
+ title="Observability",
+ description="Access-log and MCP audit event types.",
+ symbols=("AccessLogEvent", "AccessLogHook", "ToolAuditEvent"),
+ ),
+ ReferencePage(
+ slug="testing",
+ title="Testing",
+ description="In-process HTTP, MCP, and CLI test clients.",
+ symbols=("TestClient", "TestResponse", "MCPTestClient", "CliTestClient"),
+ ),
+)
+
+
+def page_symbols(slug: str) -> tuple[str, ...]:
+ for page in PAGES:
+ if page.slug == slug:
+ return page.symbols
+ raise KeyError(slug)
+
+
+def page_map(pages: Iterable[ReferencePage]) -> dict[str, ReferencePage]:
+ mapped: dict[str, ReferencePage] = {}
+ for page in pages:
+ for symbol in page.symbols:
+ if symbol in mapped:
+ raise SystemExit(f"Duplicate reference symbol: {symbol}")
+ mapped[symbol] = page
+ return mapped
+
+
+def read_public_api() -> tuple[str, ...]:
+ module = ast.parse(PACKAGE_INIT.read_text(encoding="utf-8"))
+ for node in module.body:
+ if not isinstance(node, ast.Assign):
+ continue
+ if not any(
+ isinstance(target, ast.Name) and target.id == "__all__"
+ for target in node.targets
+ ):
+ continue
+ value = ast.literal_eval(node.value)
+ if not isinstance(value, list) or not all(
+ isinstance(item, str) for item in value
+ ):
+ raise SystemExit("__all__ must be a list of strings")
+ return tuple(value)
+ raise SystemExit("Could not find quater.__all__")
+
+
+def validate_public_docstrings(package: Any, public_api: tuple[str, ...]) -> None:
+ missing: list[str] = []
+ for name in public_api:
+ obj = object_for(package, name)
+ kind_name = str(getattr(obj, "kind", ""))
+ if not kind_name.endswith(("CLASS", "FUNCTION")):
+ continue
+ docstring = getattr(obj, "docstring", None)
+ value = getattr(docstring, "value", None)
+ if not meaningful_docstring(value):
+ missing.append(name)
+
+ if missing:
+ raise SystemExit(
+ "Public classes/functions need meaningful docstrings: "
+ + ", ".join(sorted(missing))
+ )
+
+
+def meaningful_docstring(value: object) -> bool:
+ if not isinstance(value, str):
+ return False
+ words = re.findall(r"[A-Za-z0-9_]+", value)
+ if len(words) < MIN_PUBLIC_DOCSTRING_WORDS:
+ return False
+ lowered = {word.lower() for word in words}
+ return not bool(lowered & PLACEHOLDER_DOCSTRING_WORDS)
diff --git a/scripts/_reference/paths.py b/scripts/_reference/paths.py
new file mode 100644
index 0000000..6d22739
--- /dev/null
+++ b/scripts/_reference/paths.py
@@ -0,0 +1,13 @@
+"""Filesystem paths and the shared generated-file header."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+REPO_ROOT = Path(__file__).resolve().parents[2]
+SOURCE_ROOT = REPO_ROOT / "src"
+PACKAGE_INIT = SOURCE_ROOT / "quater" / "__init__.py"
+REFERENCE_DIR = REPO_ROOT / "docs" / "en" / "dev" / "reference"
+GENERATED_HEADER = (
+ ""
+)
diff --git a/scripts/_reference/render.py b/scripts/_reference/render.py
new file mode 100644
index 0000000..eb3860f
--- /dev/null
+++ b/scripts/_reference/render.py
@@ -0,0 +1,1269 @@
+"""Per-page markdown renderers and the top-level orchestration helper."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping, Sequence
+from pathlib import Path
+from typing import Any
+
+from _reference.manual import read_manual_reference
+from _reference.pages import PAGES, ReferencePage, page_symbols
+from _reference.paths import GENERATED_HEADER, REFERENCE_DIR
+from _reference.signatures import (
+ attribute_value,
+ callable_signature,
+ class_signature,
+ code_block,
+ method_signature,
+ signature_block,
+)
+from _reference.tables import (
+ CLI_CALL_OPTIONS,
+ CLI_TEST_CLIENT_OPTIONS,
+ DELETE_COOKIE_OPTIONS,
+ GROUP_ROUTE_OPTIONS,
+ HTTP_ERROR_OPTIONS,
+ MCP_TEST_CLIENT_OPTIONS,
+ MCP_TOOLS_CALL_OPTIONS,
+ PARAMETER_DOCS,
+ PARAMETER_OPTIONS,
+ QUATER_OPTIONS,
+ REQUEST_CONSTRUCTOR_OPTIONS,
+ RESPONSE_DOCS,
+ RESPONSE_OPTIONS,
+ ROUTE_GROUP_OPTIONS,
+ ROUTE_OPTIONS,
+ SET_COOKIE_OPTIONS,
+ SIGNED_COOKIE_SIGNER_OPTIONS,
+ TEST_CLIENT_OPTIONS,
+ TEST_CLIENT_REQUEST_OPTIONS,
+ TEST_RESPONSE_OPTIONS,
+ field_table,
+ type_section,
+ validated_function_option_table,
+ validated_option_table,
+)
+from _reference.types import context_row, import_link, symbol_heading
+
+
+def new_page(title: str) -> list[str]:
+ return [GENERATED_HEADER, "", f"# {title}", ""]
+
+
+def symbol_intro(
+ lines: list[str],
+ package: Any,
+ symbol: str,
+ summary: str,
+ details: Sequence[str],
+) -> None:
+ lines.extend(
+ [symbol_heading(symbol), "", source_line(package, symbol), "", summary, ""]
+ )
+ if details:
+ lines.extend([*details, ""])
+
+
+def source_line(package: Any, symbol: str) -> str:
+ if symbol == "__version__":
+ return "Public import: `from quater import __version__`."
+ return f"Public import: `from quater import {symbol}`."
+
+
+def finish(lines: list[str]) -> str:
+ return "\n".join(lines).rstrip() + "\n"
+
+
+def render_reference(
+ package: Any,
+ public_api: tuple[str, ...],
+ pages_by_symbol: Mapping[str, ReferencePage],
+) -> dict[Path, str]:
+ manual_outputs = read_manual_reference(public_api, pages_by_symbol)
+ if manual_outputs is not None:
+ return manual_outputs
+
+ return {
+ REFERENCE_DIR / "index.md": render_index(public_api, pages_by_symbol),
+ REFERENCE_DIR / "application.md": render_application(package),
+ REFERENCE_DIR / "resources.md": render_resources(package),
+ REFERENCE_DIR / "request.md": render_request(package),
+ REFERENCE_DIR / "parameters.md": render_parameters(package),
+ REFERENCE_DIR / "responses.md": render_responses(package),
+ REFERENCE_DIR / "auth.md": render_auth(package),
+ REFERENCE_DIR / "observability.md": render_observability(package),
+ REFERENCE_DIR / "testing.md": render_testing(package),
+ }
+
+
+def render_index(
+ public_api: tuple[str, ...],
+ pages_by_symbol: Mapping[str, ReferencePage],
+) -> str:
+ lines = new_page("Reference")
+ lines.extend(
+ [
+ "These pages document the public objects you can import from `quater`.",
+ "They are meant for quick lookup after you understand the concept.",
+ "",
+ "If you are still learning the framework, start with the",
+ "[Quickstart](/en/dev/quickstart) and then come back here for",
+ "exact names and signatures.",
+ "",
+ "For import guarantees, read [Stability](/en/dev/stability).",
+ "",
+ "## Pages",
+ "",
+ "| Page | Use it for |",
+ "| --- | --- |",
+ ]
+ )
+ for page in PAGES:
+ lines.append(f"| [{page.title}](./{page.slug}) | {page.description} |")
+
+ lines.extend(
+ [
+ "",
+ "## Public Imports",
+ "",
+ "Use top-level imports for normal app code. These are the documented",
+ "symbols Quater expects application code to use.",
+ "",
+ ]
+ )
+ for page in PAGES:
+ names = [name for name in public_api if pages_by_symbol[name] == page]
+ imports = ", ".join(import_link(name, page) for name in names)
+ lines.append(f"- **{page.title}:** {imports}")
+ return finish(lines)
+
+
+def render_application(package: Any) -> str:
+ lines = new_page("Application Reference")
+ lines.extend(
+ [
+ "Use these objects to create an app, group routes by feature, and",
+ "configure Quater's built-in docs and safety defaults.",
+ "",
+ "For the route model, read [Public API](/en/dev/api). For production",
+ "settings, read [Security](/en/dev/security).",
+ "",
+ "```python",
+ "from quater import AppConfig, CORSConfig, Quater, Resource, RouteGroup",
+ "```",
+ "",
+ ]
+ )
+ symbol_intro(
+ lines,
+ package,
+ "Quater",
+ "The application object.",
+ [
+ "`Quater` owns the route registry, middleware, lifespan hooks, and",
+ "server adapters. A route can stay HTTP-only, or it can opt into MCP",
+ "and CLI surfaces with `tool=True` and `cli=True`.",
+ ],
+ )
+ lines.extend(signature_block(class_signature(package, "Quater")))
+ lines.extend(
+ validated_option_table(
+ package, "Quater", None, "Constructor options", QUATER_OPTIONS
+ )
+ )
+ lines.extend(
+ [
+ "### App state",
+ "",
+ "`app.state` is a [`State`](./request#symbol-state) container for",
+ "resources that belong to the app instance. It is available from",
+ "handlers as `request.app.state`.",
+ "",
+ "### Route decorators",
+ "",
+ "Use decorators for normal route registration. `get`, `post`, `put`,",
+ "`patch`, and `delete` use the same options as `route`.",
+ "",
+ ]
+ )
+ lines.extend(signature_block(method_signature(package, "Quater", "route")))
+ lines.extend(
+ validated_option_table(
+ package, "Quater", "route", "Route options", ROUTE_OPTIONS
+ )
+ )
+ lines.extend(
+ [
+ "Descriptions matter for `tool=True` and `cli=True` routes. They are the",
+ "text an agent or operator sees before deciding whether to call the",
+ "operation.",
+ "",
+ "### Common app methods",
+ "",
+ "| Method | Use it for |",
+ "| --- | --- |",
+ "| `include(group)` | Include a "
+ "[`RouteGroup`](#symbol-routegroup) in the app. |",
+ "| `add_route(...)` | Register a route without decorator syntax. |",
+ "| `before_request(...)` | Register global before-request middleware. |",
+ "| `after_response(...)` | Register global after-response middleware. |",
+ "| `around_request(...)` | Wrap the request handler pipeline. |",
+ "| `exception_handler(...)` | Register a global exception handler. |",
+ "| `on_startup(...)` / `on_shutdown(...)` | Register lifespan hooks. |",
+ "| `startup()` / `shutdown()` | Run lifespan hooks manually in tests. |",
+ "| `handle(request)` | Handle an in-process request. |",
+ "| `validate_production()` | Fail fast on unsafe production config. |",
+ "",
+ ]
+ )
+ symbol_intro(
+ lines,
+ package,
+ "RouteGroup",
+ "A compile-time group for related routes.",
+ [
+ "A group lets you share a prefix, tags, auth, middleware, and exception",
+ "handlers across a feature area. Included groups are flattened into",
+ "normal routes before matching, so grouping does not add a router layer",
+ "to the hot path.",
+ ],
+ )
+ lines.extend(signature_block(class_signature(package, "RouteGroup")))
+ lines.extend(
+ validated_option_table(
+ package,
+ "RouteGroup",
+ None,
+ "Constructor options",
+ ROUTE_GROUP_OPTIONS,
+ )
+ )
+ lines.extend(
+ [
+ "Use `app.include(group)` after all routes are declared. Quater locks the",
+ "group after inclusion so routes cannot silently disappear later.",
+ "",
+ "Route groups expose the same route decorators as the app.",
+ "",
+ ]
+ )
+ lines.extend(signature_block(method_signature(package, "RouteGroup", "route")))
+ lines.extend(
+ validated_option_table(
+ package,
+ "RouteGroup",
+ "route",
+ "Group route options",
+ GROUP_ROUTE_OPTIONS,
+ )
+ )
+ lines.extend(
+ [
+ "The route options mean the same thing on",
+ "[`RouteGroup`](#symbol-routegroup) as they do on",
+ "[`Quater`](#symbol-quater). Group-level auth, resources, metadata, and",
+ "middleware are merged into the final route before the app compiles",
+ "routes.",
+ "",
+ ]
+ )
+ symbol_intro(
+ lines,
+ package,
+ "AppConfig",
+ "Immutable application configuration.",
+ [
+ "Most apps pass simple keyword overrides to",
+ "[`Quater(...)`](#symbol-quater). Use `AppConfig` when you want to",
+ "build configuration once and pass it around explicitly.",
+ ],
+ )
+ lines.extend(signature_block(class_signature(package, "AppConfig")))
+ lines.extend(field_table(package, "AppConfig"))
+ symbol_intro(
+ lines,
+ package,
+ "CORSConfig",
+ "Browser CORS policy.",
+ [
+ "CORS controls which browser origins may read responses. It is not an",
+ "authentication system; use `auth=...`, `mcp_auth`, and `cli_auth` for",
+ "access control.",
+ ],
+ )
+ lines.extend(signature_block(class_signature(package, "CORSConfig")))
+ lines.extend(field_table(package, "CORSConfig"))
+ symbol_intro(
+ lines,
+ package,
+ "__version__",
+ "Installed Quater version.",
+ ["Use this for diagnostics or support output."],
+ )
+ lines.extend(code_block(f"__version__ = {attribute_value(package, '__version__')}"))
+ lines.extend(
+ type_section(
+ (
+ (
+ "SecurityMode",
+ "Literal config value: `strict`, `relaxed`, or `off`.",
+ ),
+ (
+ "MaxBodySize",
+ 'Either bytes as `int` or a string such as `"2mb"`.',
+ ),
+ (
+ "Authenticate",
+ "Async auth hook. See "
+ "[Auth and Security](./auth#symbol-authrequest).",
+ ),
+ (
+ "ActionApproval",
+ "Async approval hook. See "
+ "[ActionApproval](./auth#symbol-actionapproval).",
+ ),
+ (
+ "AuditHook",
+ "Async MCP audit hook receiving "
+ "[ToolAuditEvent](./observability#symbol-toolauditevent).",
+ ),
+ (
+ "AccessLogHook",
+ "Async access-log hook. See "
+ "[Observability](./observability#symbol-accessloghook).",
+ ),
+ (
+ "BeforeMiddleware",
+ "Runs before the handler. It can short-circuit by returning "
+ "a response.",
+ ),
+ (
+ "AfterMiddleware",
+ "Runs after the handler and can adjust the response.",
+ ),
+ (
+ "AroundMiddleware",
+ "Wraps the handler pipeline for timing, tracing, or similar "
+ "cross-cutting behavior.",
+ ),
+ (
+ "ExceptionHandlerEntry",
+ "Internal wrapper for exception handlers passed through "
+ "decorators.",
+ ),
+ )
+ )
+ )
+ return finish(lines)
+
+
+def render_resources(package: Any) -> str:
+ lines = new_page("Resources Reference")
+ lines.extend(
+ [
+ "Use these objects when a handler needs an app-owned value, such as a",
+ "database session, cache handle, tenant object, or request-scoped service.",
+ "",
+ "For the guide, read [Resources and Injection](/en/dev/resources).",
+ "",
+ "```python",
+ "from quater import Resource",
+ "```",
+ "",
+ ]
+ )
+ symbol_intro(
+ lines,
+ package,
+ "Resource",
+ "A request-scoped injectable value.",
+ [
+ "`Resource` wraps a provider callable and lets a route inject the",
+ "provider result into a named handler parameter.",
+ ],
+ )
+ lines.extend(signature_block(class_signature(package, "Resource")))
+ lines.extend(
+ [
+ "### Constructor options",
+ "",
+ "| Name | Type | Meaning |",
+ "| --- | --- | --- |",
+ "| `provider` | [`ResourceProvider[T]`](#type-resourceprovider) | "
+ "Callable that creates the value. |",
+ "| `scope` | [`ResourceScope`](#type-resourcescope) | "
+ "Resource lifetime. Currently only `request` is supported. |",
+ "| `name` | `str \\| None` | Optional name used in resource error "
+ "messages. |",
+ "",
+ "### Provider forms",
+ "",
+ "The provider may accept no arguments:",
+ "",
+ "```python",
+ "async def settings() -> Settings:",
+ " return Settings.from_env()",
+ "```",
+ "",
+ "Or it may accept the current [`Request`](./request#symbol-request):",
+ "",
+ "```python",
+ "async def current_tenant(request: Request) -> Tenant:",
+ " return await request.app.state.tenants.load(",
+ ' request.headers.get("x-tenant-id")',
+ " )",
+ "```",
+ "",
+ "The provider can return a plain value, an awaitable value, a sync or "
+ "async",
+ "context manager, or yield one value from a sync or async generator.",
+ "`Resource` is generic: `Resource(provider)` carries the provider's "
+ "resolved value type, so `await request.resolve(resource)` returns "
+ "that value type.",
+ "",
+ "```python",
+ "async def db_session(request: Request) -> AsyncIterator[DatabaseSession]:",
+ " async with request.app.state.database.session() as session:",
+ " yield session",
+ "```",
+ "",
+ "Quater closes context-manager and generator resources after the handler",
+ "finishes. Cleanup also runs when the handler raises.",
+ "",
+ "### Route usage",
+ "",
+ "```python",
+ "db = Resource(db_session)",
+ "",
+ "",
+ '@app.get("/orders/{order_id}", inject={"session": db})',
+ "async def get_order(",
+ " order_id: str,",
+ " session: DatabaseSession,",
+ ") -> dict[str, object]:",
+ " ...",
+ "```",
+ "",
+ "Injected parameters are not included in OpenAPI request parameters, MCP",
+ "input schemas, or CLI action schemas.",
+ "",
+ "## Types",
+ "",
+ "| Type | Meaning |",
+ "| --- | --- |",
+ '| `ResourceProvider` | '
+ "Callable used by [`Resource`](#symbol-resource). It may return a "
+ "plain value, awaitable value, sync or async context manager, or a "
+ "sync or async generator that yields one value. |",
+ '| `ResourceMap` | Mapping of '
+ "handler parameter name to [`Resource`](#symbol-resource). This is "
+ "the type accepted by `inject`. |",
+ '| `ResourceScope` | Literal '
+ "resource lifetime. Currently `request`. |",
+ ]
+ )
+ return finish(lines)
+
+
+def render_request(package: Any) -> str:
+ lines = new_page("Request Reference")
+ lines.extend(
+ [
+ "`Request` is the object to ask for when a handler needs headers,",
+ "cookies, body access, auth context, or call-source information.",
+ "",
+ "For simple path/query/body parameters, let Quater bind the function",
+ "arguments directly instead.",
+ "",
+ "```python",
+ "from quater import Request, State",
+ "```",
+ "",
+ ]
+ )
+ symbol_intro(
+ lines,
+ package,
+ "Request",
+ "Transport-neutral request data.",
+ [
+ "The same request object is used after RSGI, ASGI, WSGI, MCP, and CLI",
+ "calls have been normalized into Quater's internal request flow.",
+ ],
+ )
+ lines.extend(signature_block(class_signature(package, "Request")))
+ lines.extend(
+ validated_option_table(
+ package,
+ "Request",
+ None,
+ "Constructor parameters",
+ REQUEST_CONSTRUCTOR_OPTIONS,
+ )
+ )
+ lines.extend(
+ [
+ "In normal app code, Quater creates the request and passes it to your",
+ "handler. The table above matches the constructor signature. The sections",
+ "below explain the objects you usually read from inside handlers.",
+ "",
+ "## Reading Request Data",
+ "",
+ "Most handlers use `request.headers`, `request.query`, `request.cookies`,",
+ "`request.auth`, `request.context`, `await request.body()`, or",
+ "`await request.json()`.",
+ "",
+ "Helper objects such as [`Headers`](#headers),",
+ "[`QueryParams`](#queryparams), and [`Cookies`](#cookies) are not",
+ "top-level public imports. Treat them as read-only request views.",
+ "",
+ ]
+ )
+ symbol_intro(
+ lines,
+ package,
+ "State",
+ "Attribute storage for application and request-local state.",
+ [
+ "`app.state` is shared by the application instance. Use it for",
+ "resources created at startup, such as database pools or clients.",
+ "`request.state` is created fresh for each request and is useful for",
+ "middleware that needs to pass values to handlers.",
+ ],
+ )
+ lines.extend(signature_block("State()"))
+ lines.extend(
+ [
+ "Keep per-request data on `request.state`, not `app.state`. If you",
+ "store shared objects on `app.state`, make sure those objects are safe",
+ "for your concurrency and deployment model.",
+ "",
+ "```python",
+ "@app.on_startup",
+ "async def startup() -> None:",
+ " app.state.db = await open_database_pool()",
+ "",
+ '@app.get("/users/{id}")',
+ "async def get_user(id: str, request: Request) -> dict[str, object]:",
+ " assert request.app is not None",
+ " user = await request.app.state.db.fetch_user(id)",
+ ' return {"id": user.id}',
+ "```",
+ "",
+ "## Call Context",
+ "",
+ "`RequestContext` is the small object stored at `request.context`. You do",
+ "not need to create it in normal app code, but it is useful when a handler",
+ "can be reached from more than one surface.",
+ "",
+ "`request.context` tells you how the handler was reached:",
+ "",
+ '- `source="api"` for normal HTTP routes.',
+ '- `source="mcp"` for MCP protocol and tool calls.',
+ '- `source="cli"` for Quater CLI actions.',
+ '- `entrypoint="server"` for hosted calls.',
+ '- `entrypoint="local"` for local CLI calls.',
+ "",
+ "```python",
+ "async def whoami(request: Request) -> dict[str, object]:",
+ " return {",
+ ' "source": request.context.source,',
+ ' "entrypoint": request.context.entrypoint,',
+ ' "subject": request.auth.subject if request.auth else None,',
+ " }",
+ "```",
+ "",
+ "Context fields:",
+ "",
+ "| Field | Type | Meaning |",
+ "| --- | --- | --- |",
+ context_row(
+ "source",
+ '"api" | "mcp" | "cli"',
+ "Which surface reached the handler.",
+ ),
+ context_row(
+ "entrypoint",
+ '"server" | "local"',
+ "Hosted request or local CLI call.",
+ ),
+ "| `request_id` | `str \\| None` | Correlation id assigned by Quater. |",
+ "| `tool_name` | `str \\| None` | MCP tool name for tool calls. |",
+ "| `action_name` | `str \\| None` | CLI action name for action calls. |",
+ "",
+ "## Header, Query, and Cookie Views",
+ "",
+ "These objects behave like small read-only mappings. You can use common",
+ "mapping methods such as `get()`, `in`, iteration, and `[...]` lookup.",
+ "",
+ "### Headers",
+ "",
+ "`request.headers` is case-insensitive. Header names are normalized for",
+ "lookup, so these are equivalent:",
+ "",
+ "```python",
+ 'request.headers.get("authorization")',
+ 'request.headers.get("Authorization")',
+ "```",
+ "",
+ "Use `get_all(name)` when a header may appear more than once. Use `.raw`",
+ "when you need the normalized `(name, value)` pairs.",
+ "",
+ "```python",
+ 'authorization = request.headers.get("authorization")',
+ 'set_cookie_headers = request.headers.get_all("set-cookie")',
+ "raw_headers = request.headers.raw",
+ "```",
+ "",
+ "### QueryParams",
+ "",
+ "`request.query` is a parsed query-string mapping. Normal lookup returns",
+ "the last value for a repeated key, which matches normal dictionary",
+ "behavior. Use `get_all(name)` when repeated query parameters matter.",
+ "",
+ "```python",
+ "# /search?tag=python&tag=api",
+ 'first_value = request.query.get("tag")',
+ 'all_values = request.query.get_all("tag")',
+ "```",
+ "",
+ "Use `.raw` when you need all `(name, value)` pairs in order.",
+ "",
+ "### Cookies",
+ "",
+ "`request.cookies` is a parsed mapping of cookie names to cookie values.",
+ "Use `.get()` when a cookie is optional.",
+ "",
+ "```python",
+ 'session_id = request.cookies.get("session")',
+ "```",
+ "",
+ "### AuthContext",
+ "",
+ "`request.auth` is either `None` or the",
+ "[`AuthContext`](./auth#symbol-authcontext) returned by the route",
+ "auth hook. Always check it before reading `subject`.",
+ "",
+ "```python",
+ "subject = request.auth.subject if request.auth else None",
+ "```",
+ "",
+ ]
+ )
+ lines.extend(
+ type_section(
+ (
+ (
+ "State",
+ "Attribute container exposed as `app.state` and `request.state`.",
+ ),
+ (
+ "Quater",
+ "Application object available as `request.app` after a "
+ "request enters an app.",
+ ),
+ (
+ "HeaderItems",
+ "`(name, value)` header pairs. Names and values may be "
+ "`str` or `bytes`.",
+ ),
+ (
+ "RequestBody",
+ "`bytes`, an async body reader, or `None`. App handlers "
+ "usually use `await request.body()`.",
+ ),
+ (
+ "AuthContext",
+ "Auth result returned by a route auth hook. See "
+ "[AuthContext](./auth#symbol-authcontext).",
+ ),
+ (
+ "RequestContext",
+ "Call-source context explained in [Call Context](#call-context).",
+ ),
+ (
+ "Headers",
+ "Read-only, case-insensitive header view available as "
+ "`request.headers`.",
+ ),
+ (
+ "QueryParams",
+ "Read-only query-string view available as `request.query`.",
+ ),
+ (
+ "Cookies",
+ "Read-only cookie mapping available as `request.cookies`.",
+ ),
+ )
+ )
+ )
+ return finish(lines)
+
+
+def render_parameters(package: Any) -> str:
+ lines = new_page("Parameter Reference")
+ lines.extend(
+ [
+ "Use parameter markers when handler arguments need explicit request",
+ "locations, aliases, defaults, or generated schema descriptions.",
+ "",
+ "For the binding model, read [Public API](/en/dev/api#parameters).",
+ "For raw request access, read [Request](./request).",
+ "",
+ "```python",
+ "from quater import Body, Cookie, File, Form, Header, Path, Query",
+ "```",
+ "",
+ "Markers can be used as defaults or inside `typing.Annotated`. The",
+ "default form is shorter. `Annotated` keeps the Python default separate.",
+ "`Query`, `Header`, `Cookie`, and `Form` bind scalar values only:",
+ "`str`, `int`, `float`, or `bool`. Use `Body` for structured JSON",
+ "input and `File` for multipart file uploads.",
+ "",
+ "```python",
+ "from typing import Annotated",
+ "",
+ "from quater import Query",
+ "",
+ "async def search(",
+ ' q: str = Query(description="Search text"),',
+ ' page: Annotated[int, Query(alias="p")] = 1,',
+ ") -> dict[str, object]:",
+ ' return {"q": q, "page": page}',
+ "```",
+ "",
+ ]
+ )
+ for symbol in page_symbols("parameters"):
+ summary, details = PARAMETER_DOCS[symbol]
+ symbol_intro(lines, package, symbol, summary, details)
+ lines.extend(signature_block(callable_signature(package, symbol)))
+ lines.extend(
+ validated_function_option_table(
+ package,
+ symbol,
+ "Parameters",
+ PARAMETER_OPTIONS[symbol],
+ )
+ )
+ lines.extend(
+ [
+ "## Action and Tool Names",
+ "",
+ "Aliases describe the HTTP wire name. MCP tools and CLI actions keep the",
+ "Python handler parameter name as the action argument name, except for",
+ "`Body(alias=...)`, which renames the body action argument. That keeps",
+ '`Header(alias="X-Request-ID")` readable in OpenAPI without forcing',
+ "agents to send a JSON key named `X-Request-ID`.",
+ "",
+ ]
+ )
+ return finish(lines)
+
+
+def render_responses(package: Any) -> str:
+ lines = new_page("Responses Reference")
+ lines.extend(
+ [
+ "Most handlers do not need to create response objects. Return plain Python",
+ "values when that is enough; use explicit response classes when you need",
+ "status codes, headers, streaming, redirects, or a specific content type.",
+ "",
+ "```python",
+ "from quater import JSONResponse, RedirectResponse, StreamResponse",
+ "```",
+ "",
+ "## Automatic Return Values",
+ "",
+ "| Handler returns | Quater sends |",
+ "| --- | --- |",
+ "| `dict`, `list`, dataclass, `msgspec.Struct` | JSON response |",
+ "| `str` | UTF-8 text response |",
+ "| `bytes`, `bytearray`, `memoryview` | Byte response |",
+ "| `None` | Empty `204` response |",
+ "| [`Response`](#symbol-response) instance | Sent as-is |",
+ "",
+ "## Response Classes",
+ "",
+ ]
+ )
+ for symbol in page_symbols("responses"):
+ symbol_intro(lines, package, symbol, RESPONSE_DOCS[symbol], [])
+ lines.extend(signature_block(class_signature(package, symbol)))
+ lines.extend(
+ validated_option_table(
+ package,
+ symbol,
+ None,
+ "Parameters",
+ RESPONSE_OPTIONS[symbol],
+ )
+ )
+ lines.extend(
+ [
+ "## Cookie helpers",
+ "",
+ "Both helpers are available on every response class.",
+ "",
+ "### `set_cookie`",
+ "",
+ ]
+ )
+ lines.extend(signature_block(method_signature(package, "Response", "set_cookie")))
+ lines.extend(
+ validated_option_table(
+ package,
+ "Response",
+ "set_cookie",
+ "Parameters",
+ SET_COOKIE_OPTIONS,
+ )
+ )
+ lines.extend(
+ [
+ "### `delete_cookie`",
+ "",
+ ]
+ )
+ lines.extend(
+ signature_block(method_signature(package, "Response", "delete_cookie"))
+ )
+ lines.extend(
+ validated_option_table(
+ package,
+ "Response",
+ "delete_cookie",
+ "Parameters",
+ DELETE_COOKIE_OPTIONS,
+ )
+ )
+ lines.extend(
+ type_section(
+ (
+ (
+ "HeaderItems",
+ "`(name, value)` header pairs. See "
+ "[Request headers](./request#headers).",
+ ),
+ (
+ "ResponseBody",
+ "Bytes-like value accepted by "
+ "[`BytesResponse`](#symbol-bytesresponse).",
+ ),
+ ("AsyncIterable[bytes]", "Async stream of response chunks."),
+ )
+ )
+ )
+ return finish(lines)
+
+
+def render_auth(package: Any) -> str:
+ lines = new_page("Auth and Security Reference")
+ lines.extend(
+ [
+ "Quater keeps the three access surfaces explicit:",
+ "",
+ "- normal routes use `auth=...` on the route or route group.",
+ "- MCP tools require `mcp_auth` when any tool is exposed.",
+ "- CLI actions require `cli_auth` when any action is exposed.",
+ "",
+ "The auth hook shape is the same across those surfaces. For the full",
+ "security model, read [Security](/en/dev/security).",
+ "",
+ "```python",
+ "from quater import (",
+ " AuthConfig,",
+ " AuthContext,",
+ " HTTPError,",
+ " ImproperlyConfigured,",
+ " SignedCookieSigner,",
+ ")",
+ "```",
+ "",
+ ]
+ )
+ symbol_intro(
+ lines,
+ package,
+ "AuthConfig",
+ "One authenticator bound to one or more request surfaces.",
+ [
+ "Pass a list of `AuthConfig` objects to `Quater(auth=...)`. Each surface",
+ "(`api`, `mcp`, `cli`) may be covered by at most one `AuthConfig`.",
+ ],
+ )
+ lines.extend(signature_block(class_signature(package, "AuthConfig")))
+ lines.extend(field_table(package, "AuthConfig"))
+ symbol_intro(
+ lines,
+ package,
+ "AuthContext",
+ "Authenticated subject returned by an auth hook.",
+ [
+ "Return `None` from an auth hook when the request is not authenticated.",
+ "Return `AuthContext` when it is authenticated.",
+ ],
+ )
+ lines.extend(signature_block(class_signature(package, "AuthContext")))
+ lines.extend(field_table(package, "AuthContext"))
+ symbol_intro(
+ lines,
+ package,
+ "ApprovalRequest",
+ "Input passed to the approval hook for protected tools and actions.",
+ [
+ "Use this when a route has `needs_approval=True`. Approval is separate",
+ "from auth: auth identifies the caller, approval confirms a sensitive",
+ "operation should run.",
+ ],
+ )
+ lines.extend(signature_block(class_signature(package, "ApprovalRequest")))
+ lines.extend(field_table(package, "ApprovalRequest"))
+ symbol_intro(
+ lines,
+ package,
+ "ActionApproval",
+ "Callable type for approval hooks.",
+ [
+ "Return `True` to allow the protected operation.",
+ "Return `False` to deny it.",
+ ],
+ )
+ lines.extend(
+ code_block(f"ActionApproval = {attribute_value(package, 'ActionApproval')}")
+ )
+ symbol_intro(
+ lines,
+ package,
+ "HTTPError",
+ "Exception that becomes an HTTP-style error response.",
+ ["Raise it when app code needs to stop with a specific status and detail."],
+ )
+ lines.extend(signature_block(class_signature(package, "HTTPError")))
+ lines.extend(
+ validated_option_table(
+ package,
+ "HTTPError",
+ None,
+ "Constructor parameters",
+ HTTP_ERROR_OPTIONS,
+ )
+ )
+ symbol_intro(
+ lines,
+ package,
+ "ImproperlyConfigured",
+ "Exception raised for invalid framework configuration.",
+ [
+ "Catch this when app setup should fail loudly before serving traffic.",
+ "`ConfigurationError` remains as a backward-compatible subclass.",
+ ],
+ )
+ lines.extend(code_block('raise ImproperlyConfigured("bad setup")'))
+ symbol_intro(
+ lines,
+ package,
+ "SignedCookieSigner",
+ "HMAC signer for small cookie values.",
+ [
+ "Use fallback secrets during key rotation. Verification uses constant-time",
+ "signature comparison.",
+ ],
+ )
+ lines.extend(signature_block(class_signature(package, "SignedCookieSigner")))
+ lines.extend(
+ validated_option_table(
+ package,
+ "SignedCookieSigner",
+ None,
+ "Constructor parameters",
+ SIGNED_COOKIE_SIGNER_OPTIONS,
+ )
+ )
+ lines.extend(
+ [
+ "Common methods:",
+ "",
+ "| Method | Use it for |",
+ "| --- | --- |",
+ "| `sign(value)` | Return a signed string safe to store in a cookie. |",
+ "| `verify(signed_value)` | Return the original value, or `None`. |",
+ "",
+ ]
+ )
+ lines.extend(
+ type_section(
+ (
+ (
+ "RequestContext",
+ "Call-source context. See [Request](./request#call-context).",
+ ),
+ (
+ "Authenticator",
+ "Async callable that receives a "
+ "[`Request`](./request#symbol-request) and returns an "
+ "[`AuthContext`](#symbol-authcontext) or `None`.",
+ ),
+ ("SecretValue", "`str` or `bytes` cookie signing secret."),
+ )
+ )
+ )
+ return finish(lines)
+
+
+def render_observability(package: Any) -> str:
+ lines = new_page("Observability Reference")
+ lines.extend(
+ [
+ "These types are used by access logging and MCP tool auditing. They are",
+ "small by design so apps can send them to logs, metrics, or tracing",
+ "systems without depending on Quater internals.",
+ "",
+ "```python",
+ "from quater import AccessLogEvent, AccessLogHook, ToolAuditEvent",
+ "```",
+ "",
+ ]
+ )
+ symbol_intro(
+ lines,
+ package,
+ "AccessLogEvent",
+ "Structured event emitted after a request is handled.",
+ ["Configure `access_logger=` on `Quater(...)` to receive these events."],
+ )
+ lines.extend(signature_block(class_signature(package, "AccessLogEvent")))
+ lines.extend(field_table(package, "AccessLogEvent"))
+ lines.extend(
+ [
+ "`AccessLogEvent.to_dict()` returns a plain dictionary for loggers that",
+ "expect JSON-like data.",
+ "",
+ ]
+ )
+ symbol_intro(
+ lines,
+ package,
+ "AccessLogHook",
+ "Callable type for access-log hooks.",
+ ["The hook is async so it can write to async logging or telemetry clients."],
+ )
+ lines.extend(
+ code_block(f"AccessLogHook = {attribute_value(package, 'AccessLogHook')}")
+ )
+ symbol_intro(
+ lines,
+ package,
+ "ToolAuditEvent",
+ "Structured event emitted for MCP tool calls.",
+ [
+ "Tool arguments are redacted before the event reaches your audit hook.",
+ "Use this for visibility, not for authorization.",
+ ],
+ )
+ lines.extend(signature_block(class_signature(package, "ToolAuditEvent")))
+ lines.extend(field_table(package, "ToolAuditEvent"))
+ return finish(lines)
+
+
+def render_testing(package: Any) -> str:
+ lines = new_page("Testing Reference")
+ lines.extend(
+ [
+ "Use the in-process clients to test Quater apps without starting Granian",
+ "or opening a socket. This keeps tests fast and makes auth, cookies,",
+ "lifespan hooks, MCP tools, and response bodies straightforward to assert.",
+ "",
+ "For examples, read the [Testing guide](/en/dev/testing).",
+ "",
+ "```python",
+ "from quater import CliTestClient, MCPTestClient, TestClient, TestResponse",
+ "```",
+ "",
+ ]
+ )
+ symbol_intro(
+ lines,
+ package,
+ "TestClient",
+ "Async in-process client for HTTP-style tests.",
+ ["Use it with `async with` when your app has startup or shutdown hooks."],
+ )
+ lines.extend(signature_block(class_signature(package, "TestClient")))
+ lines.extend(
+ validated_option_table(
+ package,
+ "TestClient",
+ None,
+ "Constructor parameters",
+ TEST_CLIENT_OPTIONS,
+ )
+ )
+ lines.extend(
+ [
+ "Common methods:",
+ "",
+ "| Method | Use it for |",
+ "| --- | --- |",
+ "| `request(method, path, ...)` | Send any method. |",
+ "| `get`, `post`, `put`, `patch`, `delete` | Convenience methods. |",
+ "| `set_cookie(name, value)` | Store a cookie for later requests. |",
+ "| `clear_cookies()` | Clear stored cookies. |",
+ "| `startup()` / `shutdown()` | Run lifespan manually. |",
+ "",
+ ]
+ )
+ lines.extend(signature_block(method_signature(package, "TestClient", "request")))
+ lines.extend(
+ validated_option_table(
+ package,
+ "TestClient",
+ "request",
+ "`request()` parameters",
+ TEST_CLIENT_REQUEST_OPTIONS,
+ )
+ )
+ symbol_intro(
+ lines,
+ package,
+ "TestResponse",
+ "Response returned by [`TestClient`](#symbol-testclient).",
+ ["It stores the collected body, headers, status code, and JSON helpers."],
+ )
+ lines.extend(signature_block(class_signature(package, "TestResponse")))
+ lines.extend(
+ validated_option_table(
+ package,
+ "TestResponse",
+ None,
+ "Constructor parameters",
+ TEST_RESPONSE_OPTIONS,
+ )
+ )
+ lines.extend(
+ [
+ "| Property or method | What it returns |",
+ "| --- | --- |",
+ "| `status_code` | Integer response status. |",
+ "| `headers` | Parsed response headers. |",
+ "| `body` | Raw response bytes. |",
+ "| `text` | UTF-8 decoded body. |",
+ "| `is_success` | `True` for `2xx` and `3xx` responses. |",
+ "| `json()` | Parsed JSON body. |",
+ "",
+ ]
+ )
+ symbol_intro(
+ lines,
+ package,
+ "MCPTestClient",
+ "Small JSON-RPC helper bound to a [`TestClient`](#symbol-testclient).",
+ [
+ "Use `client.mcp` in tests. It sends MCP requests through the same app",
+ "pipeline as a real MCP client.",
+ ],
+ )
+ lines.extend(signature_block(class_signature(package, "MCPTestClient")))
+ lines.extend(
+ validated_option_table(
+ package,
+ "MCPTestClient",
+ None,
+ "Constructor parameters",
+ MCP_TEST_CLIENT_OPTIONS,
+ )
+ )
+ lines.extend(
+ [
+ "Common methods:",
+ "",
+ "| Method | Use it for |",
+ "| --- | --- |",
+ "| `initialize(...)` | Send MCP `initialize`. |",
+ "| `tools_list(...)` | List exposed tools. |",
+ "| `tools_call(name, arguments, ...)` | Call an exposed tool. |",
+ "| `request(payload, ...)` | Send a custom JSON-RPC payload. |",
+ "",
+ ]
+ )
+ lines.extend(
+ signature_block(method_signature(package, "MCPTestClient", "tools_call"))
+ )
+ lines.extend(
+ validated_option_table(
+ package,
+ "MCPTestClient",
+ "tools_call",
+ "`tools_call()` parameters",
+ MCP_TOOLS_CALL_OPTIONS,
+ )
+ )
+ symbol_intro(
+ lines,
+ package,
+ "CliTestClient",
+ "Remote-action helper bound to a [`TestClient`](#symbol-testclient).",
+ [
+ "Use `client.cli` in tests. It calls actions and reads the action",
+ "manifest through the same remote-action endpoints as the Quater CLI.",
+ ],
+ )
+ lines.extend(signature_block(class_signature(package, "CliTestClient")))
+ lines.extend(
+ validated_option_table(
+ package,
+ "CliTestClient",
+ None,
+ "Constructor parameters",
+ CLI_TEST_CLIENT_OPTIONS,
+ )
+ )
+ lines.extend(
+ [
+ "Common methods:",
+ "",
+ "| Method | Use it for |",
+ "| --- | --- |",
+ "| `call(action, arguments, ...)` | Call an exposed CLI action. |",
+ "| `manifest(...)` | Read the action manifest. |",
+ "",
+ "Both methods return the raw [`TestResponse`](#symbol-testresponse). A",
+ "successful action body is the `{ok, status_code, body}` envelope, and a",
+ "`dry_run=True` call returns the preflight payload instead of running the",
+ "handler.",
+ "",
+ ]
+ )
+ lines.extend(signature_block(method_signature(package, "CliTestClient", "call")))
+ lines.extend(
+ validated_option_table(
+ package,
+ "CliTestClient",
+ "call",
+ "`call()` parameters",
+ CLI_CALL_OPTIONS,
+ )
+ )
+ lines.extend(
+ type_section(
+ (
+ (
+ "HeaderItems",
+ "`(name, value)` header pairs. See "
+ "[Request headers](./request#headers).",
+ ),
+ (
+ "QueryParams",
+ "Mapping or sequence accepted by "
+ "[`TestClient`](#symbol-testclient).",
+ ),
+ (
+ "RequestContent",
+ "`bytes`, `bytearray`, `memoryview`, or `str` request body "
+ "content.",
+ ),
+ ("JSONRPCID", "MCP JSON-RPC request id, either `str` or `int`."),
+ )
+ )
+ )
+ return finish(lines)
diff --git a/scripts/_reference/signatures.py b/scripts/_reference/signatures.py
new file mode 100644
index 0000000..aea7424
--- /dev/null
+++ b/scripts/_reference/signatures.py
@@ -0,0 +1,185 @@
+"""Signature and type formatting helpers.
+
+Handles griffe object resolution, signature/type string cleanup, and
+the wrapping rules for callables shown in the reference pages.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+
+def object_for(package: Any, symbol: str) -> Any:
+ return resolve(package[symbol])
+
+
+def resolve(obj: Any) -> Any:
+ return getattr(obj, "target", obj)
+
+
+def annotation(obj: Any) -> str:
+ value = getattr(obj, "annotation", None)
+ if value is None:
+ return "object"
+ return str(value)
+
+
+def init_annotations(obj: Any) -> dict[str, str]:
+ init = getattr(obj, "members", {}).get("__init__")
+ if init is None:
+ return {}
+ target = resolve(init)
+ parameters = getattr(target, "parameters", ())
+ annotations: dict[str, str] = {}
+ for parameter in parameters:
+ name = getattr(parameter, "name", "")
+ value = getattr(parameter, "annotation", None)
+ if not name or name == "self" or value is None:
+ continue
+ annotations[name] = str(value)
+ return annotations
+
+
+def attribute_value(package: Any, symbol: str) -> str:
+ value = getattr(object_for(package, symbol), "value", None)
+ if value is None:
+ return clean_signature(annotation(object_for(package, symbol)))
+ return clean_signature(str(value))
+
+
+def function_signature(obj: Any) -> str | None:
+ signature_method = getattr(obj, "signature", None)
+ if signature_method is None:
+ return None
+ return format_signature(clean_signature(str(signature_method())))
+
+
+def class_signature(package: Any, symbol: str) -> str:
+ obj = object_for(package, symbol)
+ init = getattr(obj, "members", {}).get("__init__")
+ if init is None:
+ return symbol
+ signature = function_signature(resolve(init))
+ if signature is None:
+ return symbol
+ signature = signature.replace("__init__(", f"{symbol}(", 1)
+ signature = signature.removesuffix(" -> None")
+ return signature
+
+
+def method_signature(package: Any, symbol: str, method: str) -> str:
+ obj = object_for(package, symbol)
+ member = getattr(obj, "members", {}).get(method)
+ if member is None:
+ raise SystemExit(f"Could not find {symbol}.{method}")
+ signature = function_signature(resolve(member))
+ if signature is None:
+ raise SystemExit(f"Could not read signature for {symbol}.{method}")
+ return signature
+
+
+def callable_signature(package: Any, symbol: str) -> str:
+ signature = function_signature(resolve(object_for(package, symbol)))
+ if signature is None:
+ raise SystemExit(f"Could not read signature for {symbol}")
+ return signature
+
+
+def code_block(code: str) -> list[str]:
+ return ["```python", code, "```", ""]
+
+
+def signature_block(signature: str) -> list[str]:
+ return code_block(format_signature(signature))
+
+
+def format_signature(signature: str, *, max_width: int = 88) -> str:
+ if len(signature) <= max_width:
+ return signature
+
+ open_index = signature.find("(")
+ close_index = signature.rfind(")")
+ if open_index == -1 or close_index == -1 or close_index < open_index:
+ return signature
+
+ head = signature[:open_index]
+ args = split_top_level_commas(signature[open_index + 1 : close_index])
+ tail = signature[close_index + 1 :]
+ if not args:
+ return signature
+
+ lines = [f"{head}("]
+ lines.extend(f" {argument}," for argument in args)
+ lines.append(f"){tail}")
+ return "\n".join(lines)
+
+
+def clean_signature(value: str) -> str:
+ replacements = {
+ "mcp_docs_path: str | None | _Unset = _UNSET": (
+ "mcp_docs_path: str | None = '/mcp/docs'"
+ ),
+ "docs_path: str | None | _Unset = _UNSET": ("docs_path: str | None = '/docs'"),
+ "openapi_path: str | None | _Unset = _UNSET": (
+ "openapi_path: str | None = '/openapi.json'"
+ ),
+ "request_id_header: str | None | _Unset = _UNSET": (
+ "request_id_header: str | None = 'x-request-id'"
+ ),
+ "_empty_str_map()": "...",
+ "_empty_metadata()": "...",
+ "_MCP_PROTOCOL_VERSION": "'2025-11-25'",
+ "Callable[['AccessLogEvent'], Awaitable[None]]": (
+ "Callable[[AccessLogEvent], Awaitable[None]]"
+ ),
+ "_DEFAULT_METHODS": (
+ "('DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT')"
+ ),
+ }
+ for old, new in replacements.items():
+ value = value.replace(old, new)
+ return value
+
+
+def clean_type(value: str) -> str:
+ replacements = {
+ "str | None | _Unset": "str | None",
+ }
+ for old, new in replacements.items():
+ value = value.replace(old, new)
+ return value
+
+
+def split_top_level_commas(value: str) -> list[str]:
+ parts: list[str] = []
+ start = 0
+ depth = 0
+ quote: str | None = None
+ escaped = False
+ for index, char in enumerate(value):
+ if quote is not None:
+ if escaped:
+ escaped = False
+ elif char == "\\":
+ escaped = True
+ elif char == quote:
+ quote = None
+ continue
+ if char in {"'", '"'}:
+ quote = char
+ continue
+ if char in "([{":
+ depth += 1
+ continue
+ if char in ")]}":
+ depth -= 1
+ continue
+ if char == "," and depth == 0:
+ part = value[start:index].strip()
+ if part:
+ parts.append(part)
+ start = index + 1
+ tail = value[start:].strip()
+ if tail:
+ parts.append(tail)
+ return parts
diff --git a/scripts/_reference/tables.py b/scripts/_reference/tables.py
new file mode 100644
index 0000000..12c3f62
--- /dev/null
+++ b/scripts/_reference/tables.py
@@ -0,0 +1,708 @@
+"""Table content constants and table-building / validation helpers."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping, Sequence
+from typing import Any
+
+from _reference.signatures import annotation, clean_type, object_for, resolve
+from _reference.types import (
+ is_private_member,
+ table_text,
+ type_cell,
+ type_name_cell,
+)
+
+FIELD_DOCS: Mapping[str, Mapping[str, str]] = {
+ "AppConfig": {
+ "debug": "Return detailed error responses while developing.",
+ "security": "`strict`, `relaxed`, or `off` security defaults.",
+ "allowed_hosts": "Host names the app should accept.",
+ "trusted_proxies": "Proxy IPs or CIDR ranges trusted for forwarded headers.",
+ "max_body_size": "Maximum request body size in bytes.",
+ "max_form_parts": "Maximum number of form fields and file parts.",
+ "max_form_field_size": "Maximum size for one string form field.",
+ "max_file_size": "Maximum size for one uploaded file.",
+ "upload_spool_size": "Per-file size before upload data rolls to disk.",
+ "max_tool_response_size": "Maximum MCP tool response body size.",
+ "max_action_response_size": "Maximum CLI action response body size.",
+ "cors": "Optional CORS policy.",
+ "content_security_policy": "Optional Content-Security-Policy header value.",
+ "docs_path": "Swagger UI path. Set to `None` to disable it.",
+ "openapi_path": "OpenAPI JSON path. Set to `None` to disable it.",
+ "mcp_docs_path": "Human-readable MCP docs path. Set to `None` to disable it.",
+ "mcp_allowed_origins": (
+ "Origins allowed to call the MCP endpoint from browsers."
+ ),
+ "request_id_header": "Header used for incoming and outgoing request ids.",
+ },
+ "CORSConfig": {
+ "allowed_origins": "Origins allowed to read browser responses.",
+ "allowed_methods": "Methods allowed during browser preflight checks.",
+ "allowed_headers": (
+ "Request headers allowed during preflight. Empty reflects sanitized "
+ "requested headers."
+ ),
+ "expose_headers": "Response headers browsers may expose to client code.",
+ "allow_credentials": "Whether browsers may include credentials.",
+ "max_age": "How long browsers may cache a preflight result.",
+ },
+ "AuthConfig": {
+ "authenticator": "Async callable invoked for requests on covered surfaces.",
+ "surfaces": (
+ "Surfaces this authenticator covers (`api`, `mcp`, and/or `cli`)."
+ ),
+ "name": "Optional display name used in error and log messages.",
+ },
+ "AuthContext": {
+ "subject": "Stable id for the authenticated user, service, or agent.",
+ "metadata": "Small extra values your app wants to carry with the request.",
+ },
+ "ApprovalRequest": {
+ "action": "Tool or CLI action name.",
+ "arguments_hash": "Hash of the action name and canonical bound arguments.",
+ "token": "Approval token supplied by the caller.",
+ "auth": "Authenticated subject, when the action request was authenticated.",
+ "context": "Quater context for the tool or CLI call.",
+ },
+ "AccessLogEvent": {
+ "request_id": "Request id used for correlation.",
+ "method": "HTTP method handled by Quater.",
+ "path": "Path handled by Quater.",
+ "status_code": "Final response status code.",
+ "duration_ms": "Time spent handling the request.",
+ "source": "`api`, `mcp`, or `cli`.",
+ "entrypoint": "`server` for hosted calls, `local` for local CLI.",
+ "scheme": "Request scheme.",
+ "client": "Client address when available.",
+ "tool_name": "MCP tool name when the request came from a tool call.",
+ "action_name": "CLI action name when the request came from an action call.",
+ },
+ "ToolAuditEvent": {
+ "tool_name": "MCP tool that was called.",
+ "subject": "Authenticated subject, when present.",
+ "success": "Whether the tool call completed successfully.",
+ "duration_ms": "Tool call duration.",
+ "arguments": "Redacted argument map passed to the audit hook.",
+ },
+}
+
+REQUEST_CONSTRUCTOR_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("method", "str", "HTTP method used to create the normalized request."),
+ ("path", "str", "Request path without the query string."),
+ ("scheme", "str", "`http` or `https`. Defaults to `http`."),
+ (
+ "headers",
+ "HeaderItems | Mapping[str, str]",
+ "Incoming request headers. Exposed later as `request.headers`.",
+ ),
+ (
+ "query_string",
+ "str | bytes",
+ "Raw query string. Exposed later as parsed `request.query`.",
+ ),
+ ("body", "RequestBody", "Raw bytes, async body reader, or `None`."),
+ (
+ "auth",
+ "AuthContext | None",
+ "Initial auth context. Most apps let route auth set this.",
+ ),
+ ("client", "str | None", "Client address when available."),
+ (
+ "context",
+ "RequestContext | None",
+ "Call-source context. Quater creates a default when omitted.",
+ ),
+ (
+ "app",
+ "Quater | None",
+ "Application handling the request. Quater sets this at the app boundary.",
+ ),
+ ("max_body_size", "int | None", "Optional body-size limit for this request."),
+ ("max_form_parts", "int | None", "Optional form part count limit."),
+ (
+ "max_form_field_size",
+ "int | None",
+ "Optional per-field form size limit.",
+ ),
+ ("max_file_size", "int | None", "Optional per-file upload size limit."),
+ ("upload_spool_size", "int | None", "Optional upload spool threshold."),
+)
+
+RESPONSE_DOCS: Mapping[str, str] = {
+ "Response": "Use this when you already have bytes and want full control.",
+ "JSONResponse": "Use this when you need explicit status or headers for JSON.",
+ "TextResponse": "Use this for plain text.",
+ "HTMLResponse": "Use this for HTML.",
+ "BytesResponse": "Use this for raw bytes.",
+ "StreamResponse": "Use this for async byte streams.",
+ "RedirectResponse": "Use this for redirects.",
+ "EmptyResponse": "Use this for responses with no body.",
+}
+
+PARAMETER_DOCS: Mapping[str, tuple[str, tuple[str, ...]]] = {
+ "Path": (
+ "Bind a value from a route path segment.",
+ (
+ "`Path` is useful when the Python parameter name differs from the",
+ "name in the route path, or when you want descriptions in OpenAPI",
+ "and action schemas.",
+ ),
+ ),
+ "Query": (
+ "Bind a value from the query string.",
+ (
+ "`Query` makes query parameters explicit and lets you set aliases,",
+ "defaults, and descriptions without changing handler logic.",
+ ),
+ ),
+ "Body": (
+ "Bind the JSON request body.",
+ (
+ "`Body` documents the body parameter and feeds the same schema into",
+ "OpenAPI, MCP tools, and CLI actions.",
+ ),
+ ),
+ "Form": (
+ "Bind a scalar field from a submitted form.",
+ (
+ "`Form` reads fields from `application/x-www-form-urlencoded` or",
+ "`multipart/form-data` requests. It is useful for browser forms,",
+ "OAuth-style token endpoints, and compatibility with existing clients.",
+ ),
+ ),
+ "File": (
+ "Bind uploaded files from multipart form data.",
+ (
+ "`File` reads uploaded files from `multipart/form-data`. HTTP routes",
+ "can receive files, but MCP tools and CLI actions cannot expose file",
+ "parameters in this release.",
+ ),
+ ),
+ "Header": (
+ "Bind a value from an HTTP request header.",
+ (
+ "`Header` reads case-insensitive HTTP headers. When no alias is",
+ "provided, underscores in the Python parameter name become hyphens.",
+ ),
+ ),
+ "Cookie": (
+ "Bind a value from a request cookie.",
+ (
+ "`Cookie` reads the parsed `Cookie` header and passes the selected",
+ "cookie value to the handler.",
+ ),
+ ),
+}
+
+PARAMETER_OPTIONS: Mapping[str, tuple[tuple[str, str, str], ...]] = {
+ "Path": (
+ ("default", "object", "Path parameters are always required. Leave unset."),
+ ("alias", "str | None", "Route path variable name when it differs."),
+ ("description", "str | None", "Human description used in generated schemas."),
+ ),
+ "Query": (
+ ("default", "object", "Default value. Omit it to make the parameter required."),
+ ("alias", "str | None", "Query-string name when it differs."),
+ ("description", "str | None", "Human description used in generated schemas."),
+ ),
+ "Body": (
+ ("default", "object", "Default body value. Omit it to make the body required."),
+ (
+ "alias",
+ "str | None",
+ "MCP and CLI argument name. HTTP still reads the full body.",
+ ),
+ ("description", "str | None", "Human description used in generated schemas."),
+ ),
+ "Form": (
+ ("default", "object", "Default value. Omit it to make the field required."),
+ ("alias", "str | None", "Form field name when it differs."),
+ ("description", "str | None", "Human description used in generated schemas."),
+ ),
+ "File": (
+ ("default", "object", "Default value. Omit it to make the file required."),
+ ("alias", "str | None", "Multipart field name when it differs."),
+ ("description", "str | None", "Human description used in generated schemas."),
+ ),
+ "Header": (
+ ("default", "object", "Default value. Omit it to make the header required."),
+ ("alias", "str | None", "HTTP header name, such as `X-Request-ID`."),
+ ("description", "str | None", "Human description used in generated schemas."),
+ (
+ "convert_underscores",
+ "bool",
+ "Convert `user_agent` to `user-agent` when no alias is set.",
+ ),
+ ),
+ "Cookie": (
+ ("default", "object", "Default value. Omit it to make the cookie required."),
+ ("alias", "str | None", "Cookie name when it differs."),
+ ("description", "str | None", "Human description used in generated schemas."),
+ ),
+}
+
+QUATER_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("name", "str | None", "Optional app name used by generated metadata."),
+ (
+ "config",
+ "AppConfig | None",
+ "Base [AppConfig](#symbol-appconfig) to start from.",
+ ),
+ ("debug", "bool | None", "Override `config.debug`. Use only while developing."),
+ ("security", "SecurityMode | None", "Override the security mode."),
+ ("allowed_hosts", "Iterable[str] | None", "Hosts accepted by host checks."),
+ (
+ "trusted_proxies",
+ "Iterable[str] | None",
+ "Proxy IPs trusted for forwarded headers.",
+ ),
+ ("max_body_size", "MaxBodySize | None", "Maximum request body size."),
+ ("max_form_parts", "int | None", "Maximum form field and file count."),
+ (
+ "max_form_field_size",
+ "MaxBodySize | None",
+ "Maximum size for one string form field.",
+ ),
+ ("max_file_size", "MaxBodySize | None", "Maximum size for one uploaded file."),
+ (
+ "upload_spool_size",
+ "MaxBodySize | None",
+ "Per-file size before upload data rolls to disk.",
+ ),
+ (
+ "max_tool_response_size",
+ "MaxBodySize | None",
+ "Maximum MCP tool response body size.",
+ ),
+ (
+ "max_action_response_size",
+ "MaxBodySize | None",
+ "Maximum CLI action response body size.",
+ ),
+ ("cors", "CORSConfig | None", "Optional [CORSConfig](#symbol-corsconfig)."),
+ ("content_security_policy", "str | None", "Optional CSP response header."),
+ ("mcp_docs_path", "str | None", "MCP docs path. `None` disables it."),
+ ("mcp_allowed_origins", "Iterable[str] | None", "Browser origins allowed for MCP."),
+ (
+ "auth",
+ "Iterable[AuthConfig] | None",
+ "Per-surface [`AuthConfig`](./auth#symbol-authconfig) list. See "
+ "[Auth](./auth).",
+ ),
+ (
+ "mcp_audit",
+ "AuditHook | None",
+ "Receives redacted [MCP audit events](./observability#symbol-toolauditevent).",
+ ),
+ (
+ "action_approval",
+ "ActionApproval | None",
+ "Required for protected tools/actions. See "
+ "[ActionApproval](./auth#symbol-actionapproval).",
+ ),
+ (
+ "access_logger",
+ "AccessLogHook | None",
+ "Receives "
+ "[structured access log events](./observability#symbol-accesslogevent).",
+ ),
+ ("docs_path", "str | None", "Swagger UI path. `None` disables it."),
+ ("openapi_path", "str | None", "OpenAPI JSON path. `None` disables it."),
+ ("request_id_header", "str | None", "Correlation header name. `None` disables it."),
+)
+
+ROUTE_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("method", "str", "HTTP method. Only passed to `route()` or `add_route()`."),
+ ("path", "str", "Route path, such as `/orders/{order_id}`."),
+ (
+ "name",
+ "str | None",
+ "Operation name used in docs and exposed metadata. "
+ "Defaults to the handler name.",
+ ),
+ (
+ "description",
+ "str | None",
+ "Human text used by MCP tools, CLI actions, and docs.",
+ ),
+ ("tool", "bool", "Expose this route as an MCP tool."),
+ ("cli", "bool", "Expose this route as a Quater CLI action."),
+ ("needs_approval", "bool", "Require approval before MCP or CLI execution."),
+ ("auth", "Authenticate | None", "Route-level auth hook. See [Auth](./auth)."),
+ (
+ "inject",
+ "ResourceMap | None",
+ "Handler resources created by Quater. See [Resources](/en/dev/resources).",
+ ),
+ (
+ "metadata",
+ "dict[str, Any] | None",
+ "Extra metadata used by docs and extensions.",
+ ),
+ ("before", "Iterable[BeforeMiddleware]", "Route before-request middleware."),
+ ("after", "Iterable[AfterMiddleware]", "Route after-response middleware."),
+ ("around", "Iterable[AroundMiddleware]", "Route wrapper middleware."),
+ (
+ "exception_handlers",
+ "Iterable[ExceptionHandlerEntry]",
+ "Route-specific exception handlers.",
+ ),
+)
+
+GROUP_ROUTE_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("method", "str", "HTTP method. Only passed to `route()` or `add_route()`."),
+ ("path", "str", "Group-relative route path, such as `/{order_id}`."),
+ (
+ "name",
+ "str | None",
+ "Operation name used in docs and exposed metadata. "
+ "Defaults to the handler name.",
+ ),
+ (
+ "description",
+ "str | None",
+ "Human text used by MCP tools, CLI actions, and docs.",
+ ),
+ ("tool", "bool", "Expose this route as an MCP tool."),
+ ("cli", "bool", "Expose this route as a Quater CLI action."),
+ ("needs_approval", "bool", "Require approval before MCP or CLI execution."),
+ ("auth", "Authenticate | None", "Route-level auth hook. See [Auth](./auth)."),
+ (
+ "inject",
+ "ResourceMap | None",
+ "Handler resources created by Quater. See [Resources](/en/dev/resources).",
+ ),
+ (
+ "metadata",
+ "Mapping[str, Any] | None",
+ "Extra metadata inherited into the final route.",
+ ),
+ ("before", "Iterable[BeforeMiddleware]", "Route before-request middleware."),
+ ("after", "Iterable[AfterMiddleware]", "Route after-response middleware."),
+ ("around", "Iterable[AroundMiddleware]", "Route wrapper middleware."),
+ (
+ "exception_handlers",
+ "Iterable[ExceptionHandlerEntry]",
+ "Route-specific exception handlers.",
+ ),
+)
+
+ROUTE_GROUP_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("prefix", "str", "Path prefix applied to child routes."),
+ ("tags", "Iterable[str]", "OpenAPI tags inherited by child routes."),
+ (
+ "auth",
+ "Authenticate | None",
+ "Auth hook inherited by child routes. See [Auth](./auth).",
+ ),
+ (
+ "inject",
+ "ResourceMap | None",
+ "Resources inherited by child routes. See [Resources](/en/dev/resources).",
+ ),
+ ("metadata", "Mapping[str, Any] | None", "Metadata inherited by child routes."),
+ ("before", "Iterable[BeforeMiddleware]", "Before middleware inherited by routes."),
+ ("after", "Iterable[AfterMiddleware]", "After middleware inherited by routes."),
+ ("around", "Iterable[AroundMiddleware]", "Around middleware inherited by routes."),
+ (
+ "exception_handlers",
+ "Iterable[ExceptionHandlerEntry]",
+ "Exception handlers inherited by routes.",
+ ),
+)
+
+RESPONSE_OPTIONS: Mapping[str, tuple[tuple[str, str, str], ...]] = {
+ "Response": (
+ ("body", "bytes", "Raw response body."),
+ ("status_code", "int", "HTTP status code."),
+ ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
+ ("content_type", "str | None", "Sets `content-type` when not already set."),
+ ),
+ "JSONResponse": (
+ ("content", "object", "Value serialized as JSON with msgspec."),
+ ("status_code", "int", "HTTP status code."),
+ ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
+ ),
+ "TextResponse": (
+ ("content", "str", "Text encoded as UTF-8."),
+ ("status_code", "int", "HTTP status code."),
+ ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
+ ("content_type", "str", "Text content type."),
+ ),
+ "HTMLResponse": (
+ ("content", "str", "HTML encoded as UTF-8."),
+ ("status_code", "int", "HTTP status code."),
+ ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
+ ),
+ "BytesResponse": (
+ ("content", "ResponseBody", "Bytes-like body value."),
+ ("status_code", "int", "HTTP status code."),
+ ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
+ ("content_type", "str", "Byte response content type."),
+ ),
+ "StreamResponse": (
+ ("body_iterator", "AsyncIterable[bytes]", "Async iterator yielding bytes."),
+ ("status_code", "int", "HTTP status code."),
+ ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
+ ("content_type", "str", "Stream content type."),
+ ),
+ "RedirectResponse": (
+ ("location", "str", "Redirect target."),
+ ("status_code", "int", "Redirect status code."),
+ ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
+ ),
+ "EmptyResponse": (
+ ("status_code", "int", "HTTP status code."),
+ ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
+ ),
+}
+
+SET_COOKIE_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("key", "str", "Cookie name. Must be a valid RFC 6265 token."),
+ ("value", "str", "Cookie value. Must be a valid RFC 6265 cookie-octet string."),
+ ("max_age", "int | None", "Max age in seconds."),
+ ("expires", "int | None", "Expiry as a Unix timestamp."),
+ ("path", "str | None", "Cookie path. Defaults to `/`."),
+ ("domain", "str | None", "Cookie domain."),
+ ("secure", "bool", "Set the `Secure` flag."),
+ ("httponly", "bool", "Set the `HttpOnly` flag."),
+ (
+ "samesite",
+ "Literal['lax', 'strict', 'none'] | None",
+ 'SameSite policy. `"none"` requires `secure=True`. Defaults to `"lax"`.',
+ ),
+)
+
+DELETE_COOKIE_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("key", "str", "Cookie name to delete."),
+ ("path", "str | None", "Must match the path used when the cookie was set."),
+ ("domain", "str | None", "Must match the domain used when the cookie was set."),
+ ("secure", "bool", "Required for `__Secure-` and `__Host-` prefixed cookies."),
+ ("httponly", "bool", "Set the `HttpOnly` flag."),
+ (
+ "samesite",
+ "Literal['lax', 'strict', 'none'] | None",
+ "Required when the deletion response is sent cross-site."
+ ' `"none"` requires `secure=True`.',
+ ),
+)
+
+HTTP_ERROR_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("detail", "str | None", "Error message returned to the client."),
+ ("status_code", "int | None", "HTTP status code for the error response."),
+)
+
+SIGNED_COOKIE_SIGNER_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("secret", "SecretValue", "Current signing secret."),
+ (
+ "fallback_secrets",
+ "Iterable[SecretValue]",
+ "Old secrets accepted during rotation.",
+ ),
+ ("salt", "str", "Purpose-specific salt for cookie signatures."),
+)
+
+TEST_CLIENT_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("app", "object", "Quater app under test."),
+ ("host", "str", "Host header used for requests."),
+ ("scheme", "Literal['http', 'https']", "Request scheme."),
+ ("client", "str", "Client address attached to requests."),
+ ("headers", "HeaderItems | Mapping[str, str] | None", "Default headers."),
+ ("cookies", "Mapping[str, str] | None", "Initial cookie jar."),
+)
+
+TEST_CLIENT_REQUEST_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("method", "str", "HTTP method."),
+ ("path", "str", "Request path, optionally with a query string."),
+ ("params", "QueryParams | None", "Extra query parameters."),
+ ("headers", "HeaderItems | Mapping[str, str] | None", "Request headers."),
+ ("cookies", "Mapping[str, str] | None", "Request cookies."),
+ ("json", "object", "JSON body to encode."),
+ ("content", "RequestContent | None", "Raw request body content."),
+ (
+ "data",
+ "FormDataInput | None",
+ "Form fields for URL-encoded or multipart requests.",
+ ),
+ ("files", "FilesInput | None", "Uploaded files for multipart requests."),
+)
+
+TEST_RESPONSE_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("status_code", "int", "Response status code."),
+ ("headers", "HeaderItems", "Collected response headers."),
+ ("body", "bytes", "Collected response body."),
+)
+
+MCP_TEST_CLIENT_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("client", "TestClient", "HTTP test client used for MCP requests."),
+)
+
+MCP_TOOLS_CALL_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("name", "str", "Tool name to call."),
+ ("arguments", "Mapping[str, object] | None", "Tool arguments."),
+ ("request_id", "JSONRPCID", "JSON-RPC request id."),
+ ("token", "str | None", "Bearer token used for MCP auth."),
+ ("origin", "str | None", "Origin header used for MCP origin checks."),
+ ("approval_token", "str | None", "Approval token for protected tools."),
+ ("meta", "Mapping[str, object] | None", "Optional MCP `_meta` payload."),
+ ("protocol_version", "str", "MCP protocol version header."),
+ ("headers", "HeaderItems | Mapping[str, str] | None", "Extra request headers."),
+)
+
+CLI_TEST_CLIENT_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("client", "TestClient", "HTTP test client used for CLI action requests."),
+)
+
+CLI_CALL_OPTIONS: tuple[tuple[str, str, str], ...] = (
+ ("action", "str", "Action name to call."),
+ ("arguments", "Mapping[str, object] | None", "Action arguments."),
+ ("token", "str | None", "Bearer token used for CLI auth."),
+ (
+ "dry_run",
+ "bool",
+ "Return the preflight payload instead of running the handler.",
+ ),
+ ("approval_token", "str | None", "Approval token for protected actions."),
+ ("headers", "HeaderItems | Mapping[str, str] | None", "Extra request headers."),
+)
+
+
+def parameter_table(
+ package: Any,
+ symbol: str,
+ method: str | None,
+) -> tuple[tuple[str, str], ...]:
+ obj = object_for(package, symbol)
+ if method is None:
+ member = getattr(obj, "members", {}).get("__init__")
+ else:
+ member = getattr(obj, "members", {}).get(method)
+ if member is None:
+ target = f"{symbol}.{method}" if method is not None else f"{symbol}.__init__"
+ raise SystemExit(f"Could not find {target}")
+
+ resolved = resolve(member)
+ parameters = getattr(resolved, "parameters", ())
+ rows: list[tuple[str, str]] = []
+ for parameter in parameters:
+ name = getattr(parameter, "name", "")
+ if not name or name == "self":
+ continue
+ annotation_value = getattr(parameter, "annotation", None)
+ type_name = "object" if annotation_value is None else str(annotation_value)
+ rows.append((name, clean_type(type_name)))
+ return tuple(rows)
+
+
+def function_parameter_table(package: Any, symbol: str) -> tuple[tuple[str, str], ...]:
+ obj = resolve(object_for(package, symbol))
+ parameters = getattr(obj, "parameters", ())
+ rows: list[tuple[str, str]] = []
+ for parameter in parameters:
+ name = getattr(parameter, "name", "")
+ if not name:
+ continue
+ annotation_value = getattr(parameter, "annotation", None)
+ type_name = "object" if annotation_value is None else str(annotation_value)
+ rows.append((name, clean_type(type_name)))
+ return tuple(rows)
+
+
+def field_table(package: Any, symbol: str) -> list[str]:
+ from _reference.signatures import init_annotations
+
+ obj = object_for(package, symbol)
+ annotations = init_annotations(obj)
+ rows: list[str] = []
+ actual_fields: set[str] = set()
+ for name, member in getattr(obj, "members", {}).items():
+ if is_private_member(name) or name in {"__slots__", "__test__"}:
+ continue
+ target = resolve(member)
+ kind = str(getattr(target, "kind", ""))
+ if not kind.endswith("ATTRIBUTE"):
+ continue
+ actual_fields.add(name)
+ type_name = annotation(target)
+ if type_name == "object":
+ type_name = annotations.get(name, type_name)
+ description = FIELD_DOCS.get(symbol, {}).get(name)
+ if description is None:
+ continue
+ rows.append(
+ f"| `{name}` | {type_cell(type_name)} | {table_text(description)} |"
+ )
+
+ documented_fields = set(FIELD_DOCS.get(symbol, {}))
+ if actual_fields != documented_fields:
+ missing = sorted(actual_fields - documented_fields)
+ extra = sorted(documented_fields - actual_fields)
+ raise SystemExit(
+ f"{symbol} field docs mismatch; missing={missing}, extra={extra}"
+ )
+
+ if not rows:
+ return []
+
+ return [
+ "Fields:",
+ "",
+ "| Field | Type | Meaning |",
+ "| --- | --- | --- |",
+ *rows,
+ "",
+ ]
+
+
+def option_table(
+ title: str,
+ rows: Sequence[tuple[str, str, str]],
+) -> list[str]:
+ lines = [f"### {title}", "", "| Name | Type | Meaning |", "| --- | --- | --- |"]
+ for name, type_name, description in rows:
+ lines.append(
+ f"| `{name}` | {type_cell(type_name)} | {table_text(description)} |"
+ )
+ lines.append("")
+ return lines
+
+
+def validated_option_table(
+ package: Any,
+ symbol: str,
+ method: str | None,
+ title: str,
+ rows: Sequence[tuple[str, str, str]],
+) -> list[str]:
+ actual = parameter_table(package, symbol, method)
+ expected = tuple((name, type_name) for name, type_name, _ in rows)
+ if actual != expected:
+ target = f"{symbol}.{method}" if method is not None else symbol
+ raise SystemExit(
+ f"{target} reference table mismatch; actual={actual}, expected={expected}"
+ )
+ return option_table(title, rows)
+
+
+def validated_function_option_table(
+ package: Any,
+ symbol: str,
+ title: str,
+ rows: Sequence[tuple[str, str, str]],
+) -> list[str]:
+ actual = function_parameter_table(package, symbol)
+ expected = tuple((name, type_name) for name, type_name, _ in rows)
+ if actual != expected:
+ raise SystemExit(
+ f"{symbol} reference table mismatch; actual={actual}, expected={expected}"
+ )
+ return option_table(title, rows)
+
+
+def type_section(rows: Sequence[tuple[str, str]]) -> list[str]:
+ lines = ["## Type Names Used Here", "", "| Name | Meaning |", "| --- | --- |"]
+ for name, description in rows:
+ lines.append(f"| {type_name_cell(name)} | {table_text(description)} |")
+ lines.append("")
+ return lines
diff --git a/scripts/_reference/types.py b/scripts/_reference/types.py
new file mode 100644
index 0000000..ecab178
--- /dev/null
+++ b/scripts/_reference/types.py
@@ -0,0 +1,121 @@
+"""Type name rendering, links, and small string helpers."""
+
+from __future__ import annotations
+
+import re
+from collections.abc import Iterable
+
+from _reference.pages import PAGES, ReferencePage
+from _reference.signatures import clean_type
+
+
+def slug(value: str) -> str:
+ normalized = re.sub(r"[^a-z0-9]+", "-", value.lower())
+ return normalized.strip("-")
+
+
+def symbol_anchor(value: str) -> str:
+ if value == "__version__":
+ return "symbol-version"
+ return f"symbol-{slug(value)}"
+
+
+def symbol_heading(value: str) -> str:
+ return f"## {value} {{#{symbol_anchor(value)}}}"
+
+
+def is_private_member(name: str) -> bool:
+ return name.startswith("_")
+
+
+def import_link(name: str, page: ReferencePage) -> str:
+ target = f"./{page.slug}#{symbol_anchor(name)}"
+ return f"[`{name}`]({target})"
+
+
+def type_cell(value: str) -> str:
+ text = clean_type(value)
+ if not any(type_reference(match.group(0)) for match in type_words(text)):
+ return f"`{table_code(text)}`"
+
+ escaped = table_text(text)
+ return re.sub(
+ r"\b[A-Za-z_][A-Za-z0-9_]*\b",
+ lambda match: type_token(match.group(0)),
+ escaped,
+ )
+
+
+def type_name_cell(name: str) -> str:
+ anchor = f''
+ link = public_type_reference(name)
+ if link is not None:
+ return f"{anchor}[`{name}`]({link})"
+ return f"{anchor}`{name}`"
+
+
+def type_token(name: str) -> str:
+ link = type_reference(name)
+ if link is None:
+ return name
+ return f"[`{name}`]({link})"
+
+
+def type_words(value: str) -> Iterable[re.Match[str]]:
+ return re.finditer(r"\b[A-Za-z_][A-Za-z0-9_]*\b", value)
+
+
+def type_reference(name: str) -> str | None:
+ public_link = public_type_reference(name)
+ if public_link is not None:
+ return public_link
+
+ internal_links = {
+ "HeaderItems": "/en/dev/reference/request#type-headeritems",
+ "RequestBody": "/en/dev/reference/request#type-requestbody",
+ "RequestContext": "/en/dev/reference/request#call-context",
+ "Headers": "/en/dev/reference/request#headers",
+ "QueryParams": "/en/dev/reference/request#queryparams",
+ "Cookies": "/en/dev/reference/request#cookies",
+ "SecurityMode": "/en/dev/reference/application#type-securitymode",
+ "MaxBodySize": "/en/dev/reference/application#type-maxbodysize",
+ "Authenticate": "/en/dev/reference/auth#type-authenticate",
+ "AuditHook": "/en/dev/reference/application#type-audithook",
+ "BeforeMiddleware": "/en/dev/reference/application#type-beforemiddleware",
+ "AfterMiddleware": "/en/dev/reference/application#type-aftermiddleware",
+ "AroundMiddleware": "/en/dev/reference/application#type-aroundmiddleware",
+ "ExceptionHandlerEntry": (
+ "/en/dev/reference/application#type-exceptionhandlerentry"
+ ),
+ "ResourceMap": "/en/dev/reference/resources#type-resourcemap",
+ "ResourceProvider": "/en/dev/reference/resources#type-resourceprovider",
+ "ResourceScope": "/en/dev/reference/resources#type-resourcescope",
+ "ResponseBody": "/en/dev/reference/responses#type-responsebody",
+ "RequestContent": "/en/dev/reference/testing#type-requestcontent",
+ "JSONRPCID": "/en/dev/reference/testing#type-jsonrpcid",
+ "SecretValue": "/en/dev/reference/auth#type-secretvalue",
+ }
+ return internal_links.get(name)
+
+
+def public_type_reference(name: str) -> str | None:
+ for page in PAGES:
+ if name in page.symbols and name != "__version__":
+ return f"/en/dev/reference/{page.slug}#{symbol_anchor(name)}"
+ return None
+
+
+def type_anchor(name: str) -> str:
+ return f"type-{slug(name)}"
+
+
+def table_code(value: str) -> str:
+ return value.replace("|", "\\|")
+
+
+def table_text(value: str) -> str:
+ return value.replace("|", "\\|")
+
+
+def context_row(name: str, type_name: str, description: str) -> str:
+ return f"| `{name}` | {type_cell(type_name)} | {table_text(description)} |"
diff --git a/scripts/generate_reference.py b/scripts/generate_reference.py
index 304b676..be9c7fb 100644
--- a/scripts/generate_reference.py
+++ b/scripts/generate_reference.py
@@ -1,675 +1,35 @@
-"""Generate the VitePress API reference from Quater's public Python API."""
+"""Generate the VitePress API reference from Quater's public Python API.
+
+The heavy lifting lives in the internal ``_reference`` package next to this
+script. This file only wires up the CLI, calls the pipeline, and re-exports
+the signature helpers exercised by ``tests/unit/test_generate_reference.py``.
+"""
from __future__ import annotations
import argparse
-import ast
-import difflib
-import re
import sys
-from collections.abc import Iterable, Mapping, Sequence
-from dataclasses import dataclass
+from collections.abc import Sequence
from pathlib import Path
-from typing import Any
-
-import griffe
-
-REPO_ROOT = Path(__file__).resolve().parents[1]
-SOURCE_ROOT = REPO_ROOT / "src"
-PACKAGE_INIT = SOURCE_ROOT / "quater" / "__init__.py"
-REFERENCE_DIR = REPO_ROOT / "docs" / "en" / "dev" / "reference"
-GENERATED_HEADER = (
- ""
-)
-MIN_PUBLIC_DOCSTRING_WORDS = 8
-PLACEHOLDER_DOCSTRING_WORDS = frozenset({"todo", "tbd", "fixme", "placeholder"})
-
-
-@dataclass(frozen=True, slots=True)
-class ReferencePage:
- slug: str
- title: str
- description: str
- symbols: tuple[str, ...]
-
- @property
- def path(self) -> Path:
- return REFERENCE_DIR / f"{self.slug}.md"
-
-
-PAGES: tuple[ReferencePage, ...] = (
- ReferencePage(
- slug="application",
- title="Application",
- description="App objects, route groups, and configuration.",
- symbols=("Quater", "RouteGroup", "AppConfig", "CORSConfig", "__version__"),
- ),
- ReferencePage(
- slug="resources",
- title="Resources",
- description="Request-scoped resources injected into handlers.",
- symbols=("Resource",),
- ),
- ReferencePage(
- slug="request",
- title="Request",
- description="Request data and state passed through handlers.",
- symbols=("Request", "State", "FormData", "UploadFile"),
- ),
- ReferencePage(
- slug="parameters",
- title="Parameters",
- description="Handler parameter markers for request data binding.",
- symbols=("Path", "Query", "Body", "Form", "File", "Header", "Cookie"),
- ),
- ReferencePage(
- slug="responses",
- title="Responses",
- description="Return values and explicit response classes.",
- symbols=(
- "Response",
- "JSONResponse",
- "TextResponse",
- "HTMLResponse",
- "BytesResponse",
- "StreamResponse",
- "RedirectResponse",
- "EmptyResponse",
- ),
- ),
- ReferencePage(
- slug="auth",
- title="Auth and Security",
- description=(
- "Auth hooks, approval hooks, framework errors, and signed cookies."
- ),
- symbols=(
- "AuthConfig",
- "AuthContext",
- "ApprovalRequest",
- "ActionApproval",
- "HTTPError",
- "ImproperlyConfigured",
- "SignedCookieSigner",
- ),
- ),
- ReferencePage(
- slug="observability",
- title="Observability",
- description="Access-log and MCP audit event types.",
- symbols=("AccessLogEvent", "AccessLogHook", "ToolAuditEvent"),
- ),
- ReferencePage(
- slug="testing",
- title="Testing",
- description="In-process HTTP, MCP, and CLI test clients.",
- symbols=("TestClient", "TestResponse", "MCPTestClient", "CliTestClient"),
- ),
-)
-
-
-def page_symbols(slug: str) -> tuple[str, ...]:
- for page in PAGES:
- if page.slug == slug:
- return page.symbols
- raise KeyError(slug)
-
-
-FIELD_DOCS: Mapping[str, Mapping[str, str]] = {
- "AppConfig": {
- "debug": "Return detailed error responses while developing.",
- "security": "`strict`, `relaxed`, or `off` security defaults.",
- "allowed_hosts": "Host names the app should accept.",
- "trusted_proxies": "Proxy IPs or CIDR ranges trusted for forwarded headers.",
- "max_body_size": "Maximum request body size in bytes.",
- "max_form_parts": "Maximum number of form fields and file parts.",
- "max_form_field_size": "Maximum size for one string form field.",
- "max_file_size": "Maximum size for one uploaded file.",
- "upload_spool_size": "Per-file size before upload data rolls to disk.",
- "max_tool_response_size": "Maximum MCP tool response body size.",
- "max_action_response_size": "Maximum CLI action response body size.",
- "cors": "Optional CORS policy.",
- "content_security_policy": "Optional Content-Security-Policy header value.",
- "docs_path": "Swagger UI path. Set to `None` to disable it.",
- "openapi_path": "OpenAPI JSON path. Set to `None` to disable it.",
- "mcp_docs_path": "Human-readable MCP docs path. Set to `None` to disable it.",
- "mcp_allowed_origins": (
- "Origins allowed to call the MCP endpoint from browsers."
- ),
- "request_id_header": "Header used for incoming and outgoing request ids.",
- },
- "CORSConfig": {
- "allowed_origins": "Origins allowed to read browser responses.",
- "allowed_methods": "Methods allowed during browser preflight checks.",
- "allowed_headers": (
- "Request headers allowed during preflight. Empty reflects sanitized "
- "requested headers."
- ),
- "expose_headers": "Response headers browsers may expose to client code.",
- "allow_credentials": "Whether browsers may include credentials.",
- "max_age": "How long browsers may cache a preflight result.",
- },
- "AuthRequest": {
- "method": "HTTP method for the route or protocol request.",
- "path": "Path being accessed.",
- "headers": "Normalized request headers.",
- "context": "Quater context describing the source of the call.",
- },
- "AuthContext": {
- "subject": "Stable id for the authenticated user, service, or agent.",
- "metadata": "Small extra values your app wants to carry with the request.",
- },
- "ApprovalRequest": {
- "action": "Tool or CLI action name.",
- "arguments_hash": "Hash of the action name and canonical bound arguments.",
- "token": "Approval token supplied by the caller.",
- "auth": "Authenticated subject, when the action request was authenticated.",
- "context": "Quater context for the tool or CLI call.",
- },
- "AccessLogEvent": {
- "request_id": "Request id used for correlation.",
- "method": "HTTP method handled by Quater.",
- "path": "Path handled by Quater.",
- "status_code": "Final response status code.",
- "duration_ms": "Time spent handling the request.",
- "source": "`api`, `mcp`, or `cli`.",
- "entrypoint": "`server` for hosted calls, `local` for local CLI.",
- "scheme": "Request scheme.",
- "client": "Client address when available.",
- "tool_name": "MCP tool name when the request came from a tool call.",
- "action_name": "CLI action name when the request came from an action call.",
- },
- "ToolAuditEvent": {
- "tool_name": "MCP tool that was called.",
- "subject": "Authenticated subject, when present.",
- "success": "Whether the tool call completed successfully.",
- "duration_ms": "Tool call duration.",
- "arguments": "Redacted argument map passed to the audit hook.",
- },
-}
-
-REQUEST_CONSTRUCTOR_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("method", "str", "HTTP method used to create the normalized request."),
- ("path", "str", "Request path without the query string."),
- ("scheme", "str", "`http` or `https`. Defaults to `http`."),
- (
- "headers",
- "HeaderItems | Mapping[str, str]",
- "Incoming request headers. Exposed later as `request.headers`.",
- ),
- (
- "query_string",
- "str | bytes",
- "Raw query string. Exposed later as parsed `request.query`.",
- ),
- ("body", "RequestBody", "Raw bytes, async body reader, or `None`."),
- (
- "auth",
- "AuthContext | None",
- "Initial auth context. Most apps let route auth set this.",
- ),
- ("client", "str | None", "Client address when available."),
- (
- "context",
- "RequestContext | None",
- "Call-source context. Quater creates a default when omitted.",
- ),
- (
- "app",
- "Quater | None",
- "Application handling the request. Quater sets this at the app boundary.",
- ),
- ("max_body_size", "int | None", "Optional body-size limit for this request."),
- ("max_form_parts", "int | None", "Optional form part count limit."),
- (
- "max_form_field_size",
- "int | None",
- "Optional per-field form size limit.",
- ),
- ("max_file_size", "int | None", "Optional per-file upload size limit."),
- ("upload_spool_size", "int | None", "Optional upload spool threshold."),
-)
-
-RESPONSE_DOCS: Mapping[str, str] = {
- "Response": "Use this when you already have bytes and want full control.",
- "JSONResponse": "Use this when you need explicit status or headers for JSON.",
- "TextResponse": "Use this for plain text.",
- "HTMLResponse": "Use this for HTML.",
- "BytesResponse": "Use this for raw bytes.",
- "StreamResponse": "Use this for async byte streams.",
- "RedirectResponse": "Use this for redirects.",
- "EmptyResponse": "Use this for responses with no body.",
-}
-
-PARAMETER_DOCS: Mapping[str, tuple[str, tuple[str, ...]]] = {
- "Path": (
- "Bind a value from a route path segment.",
- (
- "`Path` is useful when the Python parameter name differs from the",
- "name in the route path, or when you want descriptions in OpenAPI",
- "and action schemas.",
- ),
- ),
- "Query": (
- "Bind a value from the query string.",
- (
- "`Query` makes query parameters explicit and lets you set aliases,",
- "defaults, and descriptions without changing handler logic.",
- ),
- ),
- "Body": (
- "Bind the JSON request body.",
- (
- "`Body` documents the body parameter and feeds the same schema into",
- "OpenAPI, MCP tools, and CLI actions.",
- ),
- ),
- "Form": (
- "Bind a scalar field from a submitted form.",
- (
- "`Form` reads fields from `application/x-www-form-urlencoded` or",
- "`multipart/form-data` requests. It is useful for browser forms,",
- "OAuth-style token endpoints, and compatibility with existing clients.",
- ),
- ),
- "File": (
- "Bind uploaded files from multipart form data.",
- (
- "`File` reads uploaded files from `multipart/form-data`. HTTP routes",
- "can receive files, but MCP tools and CLI actions cannot expose file",
- "parameters in this release.",
- ),
- ),
- "Header": (
- "Bind a value from an HTTP request header.",
- (
- "`Header` reads case-insensitive HTTP headers. When no alias is",
- "provided, underscores in the Python parameter name become hyphens.",
- ),
- ),
- "Cookie": (
- "Bind a value from a request cookie.",
- (
- "`Cookie` reads the parsed `Cookie` header and passes the selected",
- "cookie value to the handler.",
- ),
- ),
-}
-
-PARAMETER_OPTIONS: Mapping[str, tuple[tuple[str, str, str], ...]] = {
- "Path": (
- ("default", "object", "Path parameters are always required. Leave unset."),
- ("alias", "str | None", "Route path variable name when it differs."),
- ("description", "str | None", "Human description used in generated schemas."),
- ),
- "Query": (
- ("default", "object", "Default value. Omit it to make the parameter required."),
- ("alias", "str | None", "Query-string name when it differs."),
- ("description", "str | None", "Human description used in generated schemas."),
- ),
- "Body": (
- ("default", "object", "Default body value. Omit it to make the body required."),
- (
- "alias",
- "str | None",
- "MCP and CLI argument name. HTTP still reads the full body.",
- ),
- ("description", "str | None", "Human description used in generated schemas."),
- ),
- "Form": (
- ("default", "object", "Default value. Omit it to make the field required."),
- ("alias", "str | None", "Form field name when it differs."),
- ("description", "str | None", "Human description used in generated schemas."),
- ),
- "File": (
- ("default", "object", "Default value. Omit it to make the file required."),
- ("alias", "str | None", "Multipart field name when it differs."),
- ("description", "str | None", "Human description used in generated schemas."),
- ),
- "Header": (
- ("default", "object", "Default value. Omit it to make the header required."),
- ("alias", "str | None", "HTTP header name, such as `X-Request-ID`."),
- ("description", "str | None", "Human description used in generated schemas."),
- (
- "convert_underscores",
- "bool",
- "Convert `user_agent` to `user-agent` when no alias is set.",
- ),
- ),
- "Cookie": (
- ("default", "object", "Default value. Omit it to make the cookie required."),
- ("alias", "str | None", "Cookie name when it differs."),
- ("description", "str | None", "Human description used in generated schemas."),
- ),
-}
-
-QUATER_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("name", "str | None", "Optional app name used by generated metadata."),
- (
- "config",
- "AppConfig | None",
- "Base [AppConfig](#symbol-appconfig) to start from.",
- ),
- ("debug", "bool | None", "Override `config.debug`. Use only while developing."),
- ("security", "SecurityMode | None", "Override the security mode."),
- ("allowed_hosts", "Iterable[str] | None", "Hosts accepted by host checks."),
- (
- "trusted_proxies",
- "Iterable[str] | None",
- "Proxy IPs trusted for forwarded headers.",
- ),
- ("max_body_size", "MaxBodySize | None", "Maximum request body size."),
- ("max_form_parts", "int | None", "Maximum form field and file count."),
- (
- "max_form_field_size",
- "MaxBodySize | None",
- "Maximum size for one string form field.",
- ),
- ("max_file_size", "MaxBodySize | None", "Maximum size for one uploaded file."),
- (
- "upload_spool_size",
- "MaxBodySize | None",
- "Per-file size before upload data rolls to disk.",
- ),
- (
- "max_tool_response_size",
- "MaxBodySize | None",
- "Maximum MCP tool response body size.",
- ),
- (
- "max_action_response_size",
- "MaxBodySize | None",
- "Maximum CLI action response body size.",
- ),
- ("cors", "CORSConfig | None", "Optional [CORSConfig](#symbol-corsconfig)."),
- ("content_security_policy", "str | None", "Optional CSP response header."),
- ("mcp_docs_path", "str | None", "MCP docs path. `None` disables it."),
- ("mcp_allowed_origins", "Iterable[str] | None", "Browser origins allowed for MCP."),
- (
- "mcp_auth",
- "Authenticate | None",
- "Required when any route has `tool=True`. See [Auth](./auth).",
- ),
- (
- "mcp_audit",
- "AuditHook | None",
- "Receives redacted [MCP audit events](./observability#symbol-toolauditevent).",
- ),
- (
- "cli_auth",
- "Authenticate | None",
- "Required when any route has `cli=True`. See [Auth](./auth).",
- ),
- (
- "action_approval",
- "ActionApproval | None",
- "Required for protected tools/actions. See "
- "[ActionApproval](./auth#symbol-actionapproval).",
- ),
- (
- "access_logger",
- "AccessLogHook | None",
- "Receives "
- "[structured access log events](./observability#symbol-accesslogevent).",
- ),
- ("docs_path", "str | None", "Swagger UI path. `None` disables it."),
- ("openapi_path", "str | None", "OpenAPI JSON path. `None` disables it."),
- ("request_id_header", "str | None", "Correlation header name. `None` disables it."),
-)
-
-ROUTE_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("method", "str", "HTTP method. Only passed to `route()` or `add_route()`."),
- ("path", "str", "Route path, such as `/orders/{order_id}`."),
- (
- "name",
- "str | None",
- "Operation name used in docs and exposed metadata. "
- "Defaults to the handler name.",
- ),
- (
- "description",
- "str | None",
- "Human text used by MCP tools, CLI actions, and docs.",
- ),
- ("tool", "bool", "Expose this route as an MCP tool."),
- ("cli", "bool", "Expose this route as a Quater CLI action."),
- ("needs_approval", "bool", "Require approval before MCP or CLI execution."),
- ("auth", "Authenticate | None", "Route-level auth hook. See [Auth](./auth)."),
- (
- "inject",
- "ResourceMap | None",
- "Handler resources created by Quater. See [Resources](/en/dev/resources).",
- ),
- (
- "metadata",
- "dict[str, Any] | None",
- "Extra metadata used by docs and extensions.",
- ),
- ("before", "Iterable[BeforeMiddleware]", "Route before-request middleware."),
- ("after", "Iterable[AfterMiddleware]", "Route after-response middleware."),
- ("around", "Iterable[AroundMiddleware]", "Route wrapper middleware."),
- (
- "exception_handlers",
- "Iterable[ExceptionHandlerEntry]",
- "Route-specific exception handlers.",
- ),
-)
-
-GROUP_ROUTE_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("method", "str", "HTTP method. Only passed to `route()` or `add_route()`."),
- ("path", "str", "Group-relative route path, such as `/{order_id}`."),
- (
- "name",
- "str | None",
- "Operation name used in docs and exposed metadata. "
- "Defaults to the handler name.",
- ),
- (
- "description",
- "str | None",
- "Human text used by MCP tools, CLI actions, and docs.",
- ),
- ("tool", "bool", "Expose this route as an MCP tool."),
- ("cli", "bool", "Expose this route as a Quater CLI action."),
- ("needs_approval", "bool", "Require approval before MCP or CLI execution."),
- ("auth", "Authenticate | None", "Route-level auth hook. See [Auth](./auth)."),
- (
- "inject",
- "ResourceMap | None",
- "Handler resources created by Quater. See [Resources](/en/dev/resources).",
- ),
- (
- "metadata",
- "Mapping[str, Any] | None",
- "Extra metadata inherited into the final route.",
- ),
- ("before", "Iterable[BeforeMiddleware]", "Route before-request middleware."),
- ("after", "Iterable[AfterMiddleware]", "Route after-response middleware."),
- ("around", "Iterable[AroundMiddleware]", "Route wrapper middleware."),
- (
- "exception_handlers",
- "Iterable[ExceptionHandlerEntry]",
- "Route-specific exception handlers.",
- ),
-)
-
-ROUTE_GROUP_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("prefix", "str", "Path prefix applied to child routes."),
- ("tags", "Iterable[str]", "OpenAPI tags inherited by child routes."),
- (
- "auth",
- "Authenticate | None",
- "Auth hook inherited by child routes. See [Auth](./auth).",
- ),
- (
- "inject",
- "ResourceMap | None",
- "Resources inherited by child routes. See [Resources](/en/dev/resources).",
- ),
- ("metadata", "Mapping[str, Any] | None", "Metadata inherited by child routes."),
- ("before", "Iterable[BeforeMiddleware]", "Before middleware inherited by routes."),
- ("after", "Iterable[AfterMiddleware]", "After middleware inherited by routes."),
- ("around", "Iterable[AroundMiddleware]", "Around middleware inherited by routes."),
- (
- "exception_handlers",
- "Iterable[ExceptionHandlerEntry]",
- "Exception handlers inherited by routes.",
- ),
-)
-
-RESPONSE_OPTIONS: Mapping[str, tuple[tuple[str, str, str], ...]] = {
- "Response": (
- ("body", "bytes", "Raw response body."),
- ("status_code", "int", "HTTP status code."),
- ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
- ("content_type", "str | None", "Sets `content-type` when not already set."),
- ),
- "JSONResponse": (
- ("content", "object", "Value serialized as JSON with msgspec."),
- ("status_code", "int", "HTTP status code."),
- ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
- ),
- "TextResponse": (
- ("content", "str", "Text encoded as UTF-8."),
- ("status_code", "int", "HTTP status code."),
- ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
- ("content_type", "str", "Text content type."),
- ),
- "HTMLResponse": (
- ("content", "str", "HTML encoded as UTF-8."),
- ("status_code", "int", "HTTP status code."),
- ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
- ),
- "BytesResponse": (
- ("content", "ResponseBody", "Bytes-like body value."),
- ("status_code", "int", "HTTP status code."),
- ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
- ("content_type", "str", "Byte response content type."),
- ),
- "StreamResponse": (
- ("body_iterator", "AsyncIterable[bytes]", "Async iterator yielding bytes."),
- ("status_code", "int", "HTTP status code."),
- ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
- ("content_type", "str", "Stream content type."),
- ),
- "RedirectResponse": (
- ("location", "str", "Redirect target."),
- ("status_code", "int", "Redirect status code."),
- ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
- ),
- "EmptyResponse": (
- ("status_code", "int", "HTTP status code."),
- ("headers", "HeaderItems | Mapping[str, str] | None", "Response headers."),
- ),
-}
-
-SET_COOKIE_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("key", "str", "Cookie name. Must be a valid RFC 6265 token."),
- ("value", "str", "Cookie value. Must be a valid RFC 6265 cookie-octet string."),
- ("max_age", "int | None", "Max age in seconds."),
- ("expires", "int | None", "Expiry as a Unix timestamp."),
- ("path", "str | None", "Cookie path. Defaults to `/`."),
- ("domain", "str | None", "Cookie domain."),
- ("secure", "bool", "Set the `Secure` flag."),
- ("httponly", "bool", "Set the `HttpOnly` flag."),
- (
- "samesite",
- "Literal['lax', 'strict', 'none'] | None",
- 'SameSite policy. `"none"` requires `secure=True`. Defaults to `"lax"`.',
- ),
-)
-
-DELETE_COOKIE_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("key", "str", "Cookie name to delete."),
- ("path", "str | None", "Must match the path used when the cookie was set."),
- ("domain", "str | None", "Must match the domain used when the cookie was set."),
- ("secure", "bool", "Required for `__Secure-` and `__Host-` prefixed cookies."),
- ("httponly", "bool", "Set the `HttpOnly` flag."),
- (
- "samesite",
- "Literal['lax', 'strict', 'none'] | None",
- "Required when the deletion response is sent cross-site."
- ' `"none"` requires `secure=True`.',
- ),
-)
-
-HTTP_ERROR_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("detail", "str | None", "Error message returned to the client."),
- ("status_code", "int | None", "HTTP status code for the error response."),
-)
-
-SIGNED_COOKIE_SIGNER_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("secret", "SecretValue", "Current signing secret."),
- (
- "fallback_secrets",
- "Iterable[SecretValue]",
- "Old secrets accepted during rotation.",
- ),
- ("salt", "str", "Purpose-specific salt for cookie signatures."),
-)
-TEST_CLIENT_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("app", "object", "Quater app under test."),
- ("host", "str", "Host header used for requests."),
- ("scheme", "Literal['http', 'https']", "Request scheme."),
- ("client", "str", "Client address attached to requests."),
- ("headers", "HeaderItems | Mapping[str, str] | None", "Default headers."),
- ("cookies", "Mapping[str, str] | None", "Initial cookie jar."),
-)
-
-TEST_CLIENT_REQUEST_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("method", "str", "HTTP method."),
- ("path", "str", "Request path, optionally with a query string."),
- ("params", "QueryParams | None", "Extra query parameters."),
- ("headers", "HeaderItems | Mapping[str, str] | None", "Request headers."),
- ("cookies", "Mapping[str, str] | None", "Request cookies."),
- ("json", "object", "JSON body to encode."),
- ("content", "RequestContent | None", "Raw request body content."),
- (
- "data",
- "FormDataInput | None",
- "Form fields for URL-encoded or multipart requests.",
- ),
- ("files", "FilesInput | None", "Uploaded files for multipart requests."),
-)
-
-TEST_RESPONSE_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("status_code", "int", "Response status code."),
- ("headers", "HeaderItems", "Collected response headers."),
- ("body", "bytes", "Collected response body."),
-)
-
-MCP_TEST_CLIENT_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("client", "TestClient", "HTTP test client used for MCP requests."),
-)
-
-MCP_TOOLS_CALL_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("name", "str", "Tool name to call."),
- ("arguments", "Mapping[str, object] | None", "Tool arguments."),
- ("request_id", "JSONRPCID", "JSON-RPC request id."),
- ("token", "str | None", "Bearer token used for MCP auth."),
- ("origin", "str | None", "Origin header used for MCP origin checks."),
- ("approval_token", "str | None", "Approval token for protected tools."),
- ("meta", "Mapping[str, object] | None", "Optional MCP `_meta` payload."),
- ("protocol_version", "str", "MCP protocol version header."),
- ("headers", "HeaderItems | Mapping[str, str] | None", "Extra request headers."),
-)
+_SCRIPTS_DIR = str(Path(__file__).resolve().parent)
+if _SCRIPTS_DIR not in sys.path:
+ sys.path.insert(0, _SCRIPTS_DIR)
-CLI_TEST_CLIENT_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("client", "TestClient", "HTTP test client used for CLI action requests."),
+import griffe # noqa: E402
+from _reference.output import check_outputs, write_outputs # noqa: E402
+from _reference.pages import ( # noqa: E402
+ PAGES,
+ page_map,
+ read_public_api,
+ validate_public_docstrings,
)
-
-CLI_CALL_OPTIONS: tuple[tuple[str, str, str], ...] = (
- ("action", "str", "Action name to call."),
- ("arguments", "Mapping[str, object] | None", "Action arguments."),
- ("token", "str | None", "Bearer token used for CLI auth."),
- (
- "dry_run",
- "bool",
- "Return the preflight payload instead of running the handler.",
- ),
- ("approval_token", "str | None", "Approval token for protected actions."),
- ("headers", "HeaderItems | Mapping[str, str] | None", "Extra request headers."),
+from _reference.paths import SOURCE_ROOT # noqa: E402
+from _reference.render import render_reference # noqa: E402
+from _reference.signatures import ( # noqa: E402,F401
+ clean_signature,
+ format_signature,
+ split_top_level_commas,
)
@@ -701,1762 +61,5 @@ def main(argv: Sequence[str] | None = None) -> int:
return 0
-def read_public_api() -> tuple[str, ...]:
- module = ast.parse(PACKAGE_INIT.read_text(encoding="utf-8"))
- for node in module.body:
- if not isinstance(node, ast.Assign):
- continue
- if not any(
- isinstance(target, ast.Name) and target.id == "__all__"
- for target in node.targets
- ):
- continue
- value = ast.literal_eval(node.value)
- if not isinstance(value, list) or not all(
- isinstance(item, str) for item in value
- ):
- raise SystemExit("__all__ must be a list of strings")
- return tuple(value)
- raise SystemExit("Could not find quater.__all__")
-
-
-def page_map(pages: Iterable[ReferencePage]) -> dict[str, ReferencePage]:
- mapped: dict[str, ReferencePage] = {}
- for page in pages:
- for symbol in page.symbols:
- if symbol in mapped:
- raise SystemExit(f"Duplicate reference symbol: {symbol}")
- mapped[symbol] = page
- return mapped
-
-
-def validate_public_docstrings(package: Any, public_api: tuple[str, ...]) -> None:
- missing: list[str] = []
- for name in public_api:
- obj = object_for(package, name)
- kind_name = str(getattr(obj, "kind", ""))
- if not kind_name.endswith(("CLASS", "FUNCTION")):
- continue
- docstring = getattr(obj, "docstring", None)
- value = getattr(docstring, "value", None)
- if not meaningful_docstring(value):
- missing.append(name)
-
- if missing:
- raise SystemExit(
- "Public classes/functions need meaningful docstrings: "
- + ", ".join(sorted(missing))
- )
-
-
-def meaningful_docstring(value: object) -> bool:
- if not isinstance(value, str):
- return False
- words = re.findall(r"[A-Za-z0-9_]+", value)
- if len(words) < MIN_PUBLIC_DOCSTRING_WORDS:
- return False
- lowered = {word.lower() for word in words}
- return not bool(lowered & PLACEHOLDER_DOCSTRING_WORDS)
-
-
-def render_reference(
- package: Any,
- public_api: tuple[str, ...],
- pages_by_symbol: Mapping[str, ReferencePage],
-) -> dict[Path, str]:
- manual_outputs = read_manual_reference(public_api, pages_by_symbol)
- if manual_outputs is not None:
- return manual_outputs
-
- return {
- REFERENCE_DIR / "index.md": render_index(public_api, pages_by_symbol),
- REFERENCE_DIR / "application.md": render_application(package),
- REFERENCE_DIR / "resources.md": render_resources(package),
- REFERENCE_DIR / "request.md": render_request(package),
- REFERENCE_DIR / "parameters.md": render_parameters(package),
- REFERENCE_DIR / "responses.md": render_responses(package),
- REFERENCE_DIR / "auth.md": render_auth(package),
- REFERENCE_DIR / "observability.md": render_observability(package),
- REFERENCE_DIR / "testing.md": render_testing(package),
- }
-
-
-def read_manual_reference(
- public_api: tuple[str, ...],
- pages_by_symbol: Mapping[str, ReferencePage],
-) -> dict[Path, str] | None:
- paths = {REFERENCE_DIR / "index.md", *(page.path for page in PAGES)}
- outputs: dict[Path, str] = {}
- for path in paths:
- if not path.exists():
- return None
- content = path.read_text(encoding="utf-8")
- if content.startswith(GENERATED_HEADER):
- return None
- outputs[path] = content
-
- for name in public_api:
- page = pages_by_symbol[name]
- content = outputs[page.path]
- anchor = symbol_anchor(name)
- if anchor not in content:
- raise SystemExit(
- f"Manual reference page {page.path} does not document {name!r}"
- )
- return outputs
-
-
-def render_index(
- public_api: tuple[str, ...],
- pages_by_symbol: Mapping[str, ReferencePage],
-) -> str:
- lines = new_page("Reference")
- lines.extend(
- [
- "These pages document the public objects you can import from `quater`.",
- "They are meant for quick lookup after you understand the concept.",
- "",
- "If you are still learning the framework, start with the",
- "[Quickstart](/en/dev/quickstart) and then come back here for",
- "exact names and signatures.",
- "",
- "For import guarantees, read [Stability](/en/dev/stability).",
- "",
- "## Pages",
- "",
- "| Page | Use it for |",
- "| --- | --- |",
- ]
- )
- for page in PAGES:
- lines.append(f"| [{page.title}](./{page.slug}) | {page.description} |")
-
- lines.extend(
- [
- "",
- "## Public Imports",
- "",
- "Use top-level imports for normal app code. These are the documented",
- "symbols Quater expects application code to use.",
- "",
- ]
- )
- for page in PAGES:
- names = [name for name in public_api if pages_by_symbol[name] == page]
- imports = ", ".join(import_link(name, page) for name in names)
- lines.append(f"- **{page.title}:** {imports}")
- return finish(lines)
-
-
-def render_application(package: Any) -> str:
- lines = new_page("Application Reference")
- lines.extend(
- [
- "Use these objects to create an app, group routes by feature, and",
- "configure Quater's built-in docs and safety defaults.",
- "",
- "For the route model, read [Public API](/en/dev/api). For production",
- "settings, read [Security](/en/dev/security).",
- "",
- "```python",
- "from quater import AppConfig, CORSConfig, Quater, Resource, RouteGroup",
- "```",
- "",
- ]
- )
- symbol_intro(
- lines,
- package,
- "Quater",
- "The application object.",
- [
- "`Quater` owns the route registry, middleware, lifespan hooks, and",
- "server adapters. A route can stay HTTP-only, or it can opt into MCP",
- "and CLI surfaces with `tool=True` and `cli=True`.",
- ],
- )
- lines.extend(signature_block(class_signature(package, "Quater")))
- lines.extend(
- validated_option_table(
- package, "Quater", None, "Constructor options", QUATER_OPTIONS
- )
- )
- lines.extend(
- [
- "### App state",
- "",
- "`app.state` is a [`State`](./request#symbol-state) container for",
- "resources that belong to the app instance. It is available from",
- "handlers as `request.app.state`.",
- "",
- "### Route decorators",
- "",
- "Use decorators for normal route registration. `get`, `post`, `put`,",
- "`patch`, and `delete` use the same options as `route`.",
- "",
- ]
- )
- lines.extend(signature_block(method_signature(package, "Quater", "route")))
- lines.extend(
- validated_option_table(
- package, "Quater", "route", "Route options", ROUTE_OPTIONS
- )
- )
- lines.extend(
- [
- "Descriptions matter for `tool=True` and `cli=True` routes. They are the",
- "text an agent or operator sees before deciding whether to call the",
- "operation.",
- "",
- "### Common app methods",
- "",
- "| Method | Use it for |",
- "| --- | --- |",
- "| `include(group)` | Include a "
- "[`RouteGroup`](#symbol-routegroup) in the app. |",
- "| `add_route(...)` | Register a route without decorator syntax. |",
- "| `before_request(...)` | Register global before-request middleware. |",
- "| `after_response(...)` | Register global after-response middleware. |",
- "| `around_request(...)` | Wrap the request handler pipeline. |",
- "| `exception_handler(...)` | Register a global exception handler. |",
- "| `on_startup(...)` / `on_shutdown(...)` | Register lifespan hooks. |",
- "| `startup()` / `shutdown()` | Run lifespan hooks manually in tests. |",
- "| `handle(request)` | Handle an in-process request. |",
- "| `validate_production()` | Fail fast on unsafe production config. |",
- "",
- ]
- )
- symbol_intro(
- lines,
- package,
- "RouteGroup",
- "A compile-time group for related routes.",
- [
- "A group lets you share a prefix, tags, auth, middleware, and exception",
- "handlers across a feature area. Included groups are flattened into",
- "normal routes before matching, so grouping does not add a router layer",
- "to the hot path.",
- ],
- )
- lines.extend(signature_block(class_signature(package, "RouteGroup")))
- lines.extend(
- validated_option_table(
- package,
- "RouteGroup",
- None,
- "Constructor options",
- ROUTE_GROUP_OPTIONS,
- )
- )
- lines.extend(
- [
- "Use `app.include(group)` after all routes are declared. Quater locks the",
- "group after inclusion so routes cannot silently disappear later.",
- "",
- "Route groups expose the same route decorators as the app.",
- "",
- ]
- )
- lines.extend(signature_block(method_signature(package, "RouteGroup", "route")))
- lines.extend(
- validated_option_table(
- package,
- "RouteGroup",
- "route",
- "Group route options",
- GROUP_ROUTE_OPTIONS,
- )
- )
- lines.extend(
- [
- "The route options mean the same thing on",
- "[`RouteGroup`](#symbol-routegroup) as they do on",
- "[`Quater`](#symbol-quater). Group-level auth, resources, metadata, and",
- "middleware are merged into the final route before the app compiles",
- "routes.",
- "",
- ]
- )
- symbol_intro(
- lines,
- package,
- "AppConfig",
- "Immutable application configuration.",
- [
- "Most apps pass simple keyword overrides to",
- "[`Quater(...)`](#symbol-quater). Use `AppConfig` when you want to",
- "build configuration once and pass it around explicitly.",
- ],
- )
- lines.extend(signature_block(class_signature(package, "AppConfig")))
- lines.extend(field_table(package, "AppConfig"))
- symbol_intro(
- lines,
- package,
- "CORSConfig",
- "Browser CORS policy.",
- [
- "CORS controls which browser origins may read responses. It is not an",
- "authentication system; use `auth=...`, `mcp_auth`, and `cli_auth` for",
- "access control.",
- ],
- )
- lines.extend(signature_block(class_signature(package, "CORSConfig")))
- lines.extend(field_table(package, "CORSConfig"))
- symbol_intro(
- lines,
- package,
- "__version__",
- "Installed Quater version.",
- ["Use this for diagnostics or support output."],
- )
- lines.extend(code_block(f"__version__ = {attribute_value(package, '__version__')}"))
- lines.extend(
- type_section(
- (
- (
- "SecurityMode",
- "Literal config value: `strict`, `relaxed`, or `off`.",
- ),
- (
- "MaxBodySize",
- 'Either bytes as `int` or a string such as `"2mb"`.',
- ),
- (
- "Authenticate",
- "Async auth hook. See "
- "[Auth and Security](./auth#symbol-authrequest).",
- ),
- (
- "ActionApproval",
- "Async approval hook. See "
- "[ActionApproval](./auth#symbol-actionapproval).",
- ),
- (
- "AuditHook",
- "Async MCP audit hook receiving "
- "[ToolAuditEvent](./observability#symbol-toolauditevent).",
- ),
- (
- "AccessLogHook",
- "Async access-log hook. See "
- "[Observability](./observability#symbol-accessloghook).",
- ),
- (
- "BeforeMiddleware",
- "Runs before the handler. It can short-circuit by returning "
- "a response.",
- ),
- (
- "AfterMiddleware",
- "Runs after the handler and can adjust the response.",
- ),
- (
- "AroundMiddleware",
- "Wraps the handler pipeline for timing, tracing, or similar "
- "cross-cutting behavior.",
- ),
- (
- "ExceptionHandlerEntry",
- "Internal wrapper for exception handlers passed through "
- "decorators.",
- ),
- )
- )
- )
- return finish(lines)
-
-
-def render_resources(package: Any) -> str:
- lines = new_page("Resources Reference")
- lines.extend(
- [
- "Use these objects when a handler needs an app-owned value, such as a",
- "database session, cache handle, tenant object, or request-scoped service.",
- "",
- "For the guide, read [Resources and Injection](/en/dev/resources).",
- "",
- "```python",
- "from quater import Resource",
- "```",
- "",
- ]
- )
- symbol_intro(
- lines,
- package,
- "Resource",
- "A request-scoped injectable value.",
- [
- "`Resource` wraps a provider callable and lets a route inject the",
- "provider result into a named handler parameter.",
- ],
- )
- lines.extend(signature_block(class_signature(package, "Resource")))
- lines.extend(
- [
- "### Constructor options",
- "",
- "| Name | Type | Meaning |",
- "| --- | --- | --- |",
- "| `provider` | [`ResourceProvider[T]`](#type-resourceprovider) | "
- "Callable that creates the value. |",
- "| `scope` | [`ResourceScope`](#type-resourcescope) | "
- "Resource lifetime. Currently only `request` is supported. |",
- "| `name` | `str \\| None` | Optional name used in resource error "
- "messages. |",
- "",
- "### Provider forms",
- "",
- "The provider may accept no arguments:",
- "",
- "```python",
- "async def settings() -> Settings:",
- " return Settings.from_env()",
- "```",
- "",
- "Or it may accept the current [`Request`](./request#symbol-request):",
- "",
- "```python",
- "async def current_tenant(request: Request) -> Tenant:",
- " return await request.app.state.tenants.load(",
- ' request.headers.get("x-tenant-id")',
- " )",
- "```",
- "",
- "The provider can return a plain value, an awaitable value, a sync or "
- "async",
- "context manager, or yield one value from a sync or async generator.",
- "`Resource` is generic: `Resource(provider)` carries the provider's "
- "resolved value type, so `await request.resolve(resource)` returns "
- "that value type.",
- "",
- "```python",
- "async def db_session(request: Request) -> AsyncIterator[DatabaseSession]:",
- " async with request.app.state.database.session() as session:",
- " yield session",
- "```",
- "",
- "Quater closes context-manager and generator resources after the handler",
- "finishes. Cleanup also runs when the handler raises.",
- "",
- "### Route usage",
- "",
- "```python",
- "db = Resource(db_session)",
- "",
- "",
- '@app.get("/orders/{order_id}", inject={"session": db})',
- "async def get_order(",
- " order_id: str,",
- " session: DatabaseSession,",
- ") -> dict[str, object]:",
- " ...",
- "```",
- "",
- "Injected parameters are not included in OpenAPI request parameters, MCP",
- "input schemas, or CLI action schemas.",
- "",
- "## Types",
- "",
- "| Type | Meaning |",
- "| --- | --- |",
- '| `ResourceProvider` | '
- "Callable used by [`Resource`](#symbol-resource). It may return a "
- "plain value, awaitable value, sync or async context manager, or a "
- "sync or async generator that yields one value. |",
- '| `ResourceMap` | Mapping of '
- "handler parameter name to [`Resource`](#symbol-resource). This is "
- "the type accepted by `inject`. |",
- '| `ResourceScope` | Literal '
- "resource lifetime. Currently `request`. |",
- ]
- )
- return finish(lines)
-
-
-def render_request(package: Any) -> str:
- lines = new_page("Request Reference")
- lines.extend(
- [
- "`Request` is the object to ask for when a handler needs headers,",
- "cookies, body access, auth context, or call-source information.",
- "",
- "For simple path/query/body parameters, let Quater bind the function",
- "arguments directly instead.",
- "",
- "```python",
- "from quater import Request, State",
- "```",
- "",
- ]
- )
- symbol_intro(
- lines,
- package,
- "Request",
- "Transport-neutral request data.",
- [
- "The same request object is used after RSGI, ASGI, WSGI, MCP, and CLI",
- "calls have been normalized into Quater's internal request flow.",
- ],
- )
- lines.extend(signature_block(class_signature(package, "Request")))
- lines.extend(
- validated_option_table(
- package,
- "Request",
- None,
- "Constructor parameters",
- REQUEST_CONSTRUCTOR_OPTIONS,
- )
- )
- lines.extend(
- [
- "In normal app code, Quater creates the request and passes it to your",
- "handler. The table above matches the constructor signature. The sections",
- "below explain the objects you usually read from inside handlers.",
- "",
- "## Reading Request Data",
- "",
- "Most handlers use `request.headers`, `request.query`, `request.cookies`,",
- "`request.auth`, `request.context`, `await request.body()`, or",
- "`await request.json()`.",
- "",
- "Helper objects such as [`Headers`](#headers),",
- "[`QueryParams`](#queryparams), and [`Cookies`](#cookies) are not",
- "top-level public imports. Treat them as read-only request views.",
- "",
- ]
- )
- symbol_intro(
- lines,
- package,
- "State",
- "Attribute storage for application and request-local state.",
- [
- "`app.state` is shared by the application instance. Use it for",
- "resources created at startup, such as database pools or clients.",
- "`request.state` is created fresh for each request and is useful for",
- "middleware that needs to pass values to handlers.",
- ],
- )
- lines.extend(signature_block("State()"))
- lines.extend(
- [
- "Keep per-request data on `request.state`, not `app.state`. If you",
- "store shared objects on `app.state`, make sure those objects are safe",
- "for your concurrency and deployment model.",
- "",
- "```python",
- "@app.on_startup",
- "async def startup() -> None:",
- " app.state.db = await open_database_pool()",
- "",
- '@app.get("/users/{id}")',
- "async def get_user(id: str, request: Request) -> dict[str, object]:",
- " assert request.app is not None",
- " user = await request.app.state.db.fetch_user(id)",
- ' return {"id": user.id}',
- "```",
- "",
- "## Call Context",
- "",
- "`RequestContext` is the small object stored at `request.context`. You do",
- "not need to create it in normal app code, but it is useful when a handler",
- "can be reached from more than one surface.",
- "",
- "`request.context` tells you how the handler was reached:",
- "",
- '- `source="api"` for normal HTTP routes.',
- '- `source="mcp"` for MCP protocol and tool calls.',
- '- `source="cli"` for Quater CLI actions.',
- '- `entrypoint="server"` for hosted calls.',
- '- `entrypoint="local"` for local CLI calls.',
- "",
- "```python",
- "async def whoami(request: Request) -> dict[str, object]:",
- " return {",
- ' "source": request.context.source,',
- ' "entrypoint": request.context.entrypoint,',
- ' "subject": request.auth.subject if request.auth else None,',
- " }",
- "```",
- "",
- "Context fields:",
- "",
- "| Field | Type | Meaning |",
- "| --- | --- | --- |",
- context_row(
- "source",
- '"api" | "mcp" | "cli"',
- "Which surface reached the handler.",
- ),
- context_row(
- "entrypoint",
- '"server" | "local"',
- "Hosted request or local CLI call.",
- ),
- "| `request_id` | `str \\| None` | Correlation id assigned by Quater. |",
- "| `tool_name` | `str \\| None` | MCP tool name for tool calls. |",
- "| `action_name` | `str \\| None` | CLI action name for action calls. |",
- "",
- "## Header, Query, and Cookie Views",
- "",
- "These objects behave like small read-only mappings. You can use common",
- "mapping methods such as `get()`, `in`, iteration, and `[...]` lookup.",
- "",
- "### Headers",
- "",
- "`request.headers` is case-insensitive. Header names are normalized for",
- "lookup, so these are equivalent:",
- "",
- "```python",
- 'request.headers.get("authorization")',
- 'request.headers.get("Authorization")',
- "```",
- "",
- "Use `get_all(name)` when a header may appear more than once. Use `.raw`",
- "when you need the normalized `(name, value)` pairs.",
- "",
- "```python",
- 'authorization = request.headers.get("authorization")',
- 'set_cookie_headers = request.headers.get_all("set-cookie")',
- "raw_headers = request.headers.raw",
- "```",
- "",
- "### QueryParams",
- "",
- "`request.query` is a parsed query-string mapping. Normal lookup returns",
- "the last value for a repeated key, which matches normal dictionary",
- "behavior. Use `get_all(name)` when repeated query parameters matter.",
- "",
- "```python",
- "# /search?tag=python&tag=api",
- 'first_value = request.query.get("tag")',
- 'all_values = request.query.get_all("tag")',
- "```",
- "",
- "Use `.raw` when you need all `(name, value)` pairs in order.",
- "",
- "### Cookies",
- "",
- "`request.cookies` is a parsed mapping of cookie names to cookie values.",
- "Use `.get()` when a cookie is optional.",
- "",
- "```python",
- 'session_id = request.cookies.get("session")',
- "```",
- "",
- "### AuthContext",
- "",
- "`request.auth` is either `None` or the",
- "[`AuthContext`](./auth#symbol-authcontext) returned by the route",
- "auth hook. Always check it before reading `subject`.",
- "",
- "```python",
- "subject = request.auth.subject if request.auth else None",
- "```",
- "",
- ]
- )
- lines.extend(
- type_section(
- (
- (
- "State",
- "Attribute container exposed as `app.state` and `request.state`.",
- ),
- (
- "Quater",
- "Application object available as `request.app` after a "
- "request enters an app.",
- ),
- (
- "HeaderItems",
- "`(name, value)` header pairs. Names and values may be "
- "`str` or `bytes`.",
- ),
- (
- "RequestBody",
- "`bytes`, an async body reader, or `None`. App handlers "
- "usually use `await request.body()`.",
- ),
- (
- "AuthContext",
- "Auth result returned by a route auth hook. See "
- "[AuthContext](./auth#symbol-authcontext).",
- ),
- (
- "RequestContext",
- "Call-source context explained in [Call Context](#call-context).",
- ),
- (
- "Headers",
- "Read-only, case-insensitive header view available as "
- "`request.headers`.",
- ),
- (
- "QueryParams",
- "Read-only query-string view available as `request.query`.",
- ),
- (
- "Cookies",
- "Read-only cookie mapping available as `request.cookies`.",
- ),
- )
- )
- )
- return finish(lines)
-
-
-def render_parameters(package: Any) -> str:
- lines = new_page("Parameter Reference")
- lines.extend(
- [
- "Use parameter markers when handler arguments need explicit request",
- "locations, aliases, defaults, or generated schema descriptions.",
- "",
- "For the binding model, read [Public API](/en/dev/api#parameters).",
- "For raw request access, read [Request](./request).",
- "",
- "```python",
- "from quater import Body, Cookie, File, Form, Header, Path, Query",
- "```",
- "",
- "Markers can be used as defaults or inside `typing.Annotated`. The",
- "default form is shorter. `Annotated` keeps the Python default separate.",
- "`Query`, `Header`, `Cookie`, and `Form` bind scalar values only:",
- "`str`, `int`, `float`, or `bool`. Use `Body` for structured JSON",
- "input and `File` for multipart file uploads.",
- "",
- "```python",
- "from typing import Annotated",
- "",
- "from quater import Query",
- "",
- "async def search(",
- ' q: str = Query(description="Search text"),',
- ' page: Annotated[int, Query(alias="p")] = 1,',
- ") -> dict[str, object]:",
- ' return {"q": q, "page": page}',
- "```",
- "",
- ]
- )
- for symbol in page_symbols("parameters"):
- summary, details = PARAMETER_DOCS[symbol]
- symbol_intro(lines, package, symbol, summary, details)
- lines.extend(signature_block(callable_signature(package, symbol)))
- lines.extend(
- validated_function_option_table(
- package,
- symbol,
- "Parameters",
- PARAMETER_OPTIONS[symbol],
- )
- )
- lines.extend(
- [
- "## Action and Tool Names",
- "",
- "Aliases describe the HTTP wire name. MCP tools and CLI actions keep the",
- "Python handler parameter name as the action argument name, except for",
- "`Body(alias=...)`, which renames the body action argument. That keeps",
- '`Header(alias="X-Request-ID")` readable in OpenAPI without forcing',
- "agents to send a JSON key named `X-Request-ID`.",
- "",
- ]
- )
- return finish(lines)
-
-
-def render_responses(package: Any) -> str:
- lines = new_page("Responses Reference")
- lines.extend(
- [
- "Most handlers do not need to create response objects. Return plain Python",
- "values when that is enough; use explicit response classes when you need",
- "status codes, headers, streaming, redirects, or a specific content type.",
- "",
- "```python",
- "from quater import JSONResponse, RedirectResponse, StreamResponse",
- "```",
- "",
- "## Automatic Return Values",
- "",
- "| Handler returns | Quater sends |",
- "| --- | --- |",
- "| `dict`, `list`, dataclass, `msgspec.Struct` | JSON response |",
- "| `str` | UTF-8 text response |",
- "| `bytes`, `bytearray`, `memoryview` | Byte response |",
- "| `None` | Empty `204` response |",
- "| [`Response`](#symbol-response) instance | Sent as-is |",
- "",
- "## Response Classes",
- "",
- ]
- )
- for symbol in page_symbols("responses"):
- symbol_intro(lines, package, symbol, RESPONSE_DOCS[symbol], [])
- lines.extend(signature_block(class_signature(package, symbol)))
- lines.extend(
- validated_option_table(
- package,
- symbol,
- None,
- "Parameters",
- RESPONSE_OPTIONS[symbol],
- )
- )
- lines.extend(
- [
- "## Cookie helpers",
- "",
- "Both helpers are available on every response class.",
- "",
- "### `set_cookie`",
- "",
- ]
- )
- lines.extend(signature_block(method_signature(package, "Response", "set_cookie")))
- lines.extend(
- validated_option_table(
- package,
- "Response",
- "set_cookie",
- "Parameters",
- SET_COOKIE_OPTIONS,
- )
- )
- lines.extend(
- [
- "### `delete_cookie`",
- "",
- ]
- )
- lines.extend(
- signature_block(method_signature(package, "Response", "delete_cookie"))
- )
- lines.extend(
- validated_option_table(
- package,
- "Response",
- "delete_cookie",
- "Parameters",
- DELETE_COOKIE_OPTIONS,
- )
- )
- lines.extend(
- type_section(
- (
- (
- "HeaderItems",
- "`(name, value)` header pairs. See "
- "[Request headers](./request#headers).",
- ),
- (
- "ResponseBody",
- "Bytes-like value accepted by "
- "[`BytesResponse`](#symbol-bytesresponse).",
- ),
- ("AsyncIterable[bytes]", "Async stream of response chunks."),
- )
- )
- )
- return finish(lines)
-
-
-def render_auth(package: Any) -> str:
- lines = new_page("Auth and Security Reference")
- lines.extend(
- [
- "Quater keeps the three access surfaces explicit:",
- "",
- "- normal routes use `auth=...` on the route or route group.",
- "- MCP tools require `mcp_auth` when any tool is exposed.",
- "- CLI actions require `cli_auth` when any action is exposed.",
- "",
- "The auth hook shape is the same across those surfaces. For the full",
- "security model, read [Security](/en/dev/security).",
- "",
- "```python",
- "from quater import (",
- " AuthContext,",
- " AuthRequest,",
- " HTTPError,",
- " ImproperlyConfigured,",
- " SignedCookieSigner,",
- ")",
- "```",
- "",
- ]
- )
- symbol_intro(
- lines,
- package,
- "AuthRequest",
- "Input passed to an auth hook.",
- ["Use it to inspect method, path, headers, and Quater call context."],
- )
- lines.extend(signature_block(class_signature(package, "AuthRequest")))
- lines.extend(field_table(package, "AuthRequest"))
- symbol_intro(
- lines,
- package,
- "AuthContext",
- "Authenticated subject returned by an auth hook.",
- [
- "Return `None` from an auth hook when the request is not authenticated.",
- "Return `AuthContext` when it is authenticated.",
- ],
- )
- lines.extend(signature_block(class_signature(package, "AuthContext")))
- lines.extend(field_table(package, "AuthContext"))
- symbol_intro(
- lines,
- package,
- "ApprovalRequest",
- "Input passed to the approval hook for protected tools and actions.",
- [
- "Use this when a route has `needs_approval=True`. Approval is separate",
- "from auth: auth identifies the caller, approval confirms a sensitive",
- "operation should run.",
- ],
- )
- lines.extend(signature_block(class_signature(package, "ApprovalRequest")))
- lines.extend(field_table(package, "ApprovalRequest"))
- symbol_intro(
- lines,
- package,
- "ActionApproval",
- "Callable type for approval hooks.",
- [
- "Return `True` to allow the protected operation.",
- "Return `False` to deny it.",
- ],
- )
- lines.extend(
- code_block(f"ActionApproval = {attribute_value(package, 'ActionApproval')}")
- )
- symbol_intro(
- lines,
- package,
- "HTTPError",
- "Exception that becomes an HTTP-style error response.",
- ["Raise it when app code needs to stop with a specific status and detail."],
- )
- lines.extend(signature_block(class_signature(package, "HTTPError")))
- lines.extend(
- validated_option_table(
- package,
- "HTTPError",
- None,
- "Constructor parameters",
- HTTP_ERROR_OPTIONS,
- )
- )
- symbol_intro(
- lines,
- package,
- "ImproperlyConfigured",
- "Exception raised for invalid framework configuration.",
- [
- "Catch this when app setup should fail loudly before serving traffic.",
- "`ConfigurationError` remains as a backward-compatible subclass.",
- ],
- )
- lines.extend(code_block('raise ImproperlyConfigured("bad setup")'))
- symbol_intro(
- lines,
- package,
- "SignedCookieSigner",
- "HMAC signer for small cookie values.",
- [
- "Use fallback secrets during key rotation. Verification uses constant-time",
- "signature comparison.",
- ],
- )
- lines.extend(signature_block(class_signature(package, "SignedCookieSigner")))
- lines.extend(
- validated_option_table(
- package,
- "SignedCookieSigner",
- None,
- "Constructor parameters",
- SIGNED_COOKIE_SIGNER_OPTIONS,
- )
- )
- lines.extend(
- [
- "Common methods:",
- "",
- "| Method | Use it for |",
- "| --- | --- |",
- "| `sign(value)` | Return a signed string safe to store in a cookie. |",
- "| `verify(signed_value)` | Return the original value, or `None`. |",
- "",
- ]
- )
- lines.extend(
- type_section(
- (
- (
- "RequestContext",
- "Call-source context. See [Request](./request#call-context).",
- ),
- (
- "Authenticate",
- "Async callable that receives "
- "[`AuthRequest`](#symbol-authrequest) and returns "
- "[`AuthContext`](#symbol-authcontext) or `None`.",
- ),
- ("SecretValue", "`str` or `bytes` cookie signing secret."),
- )
- )
- )
- return finish(lines)
-
-
-def render_observability(package: Any) -> str:
- lines = new_page("Observability Reference")
- lines.extend(
- [
- "These types are used by access logging and MCP tool auditing. They are",
- "small by design so apps can send them to logs, metrics, or tracing",
- "systems without depending on Quater internals.",
- "",
- "```python",
- "from quater import AccessLogEvent, AccessLogHook, ToolAuditEvent",
- "```",
- "",
- ]
- )
- symbol_intro(
- lines,
- package,
- "AccessLogEvent",
- "Structured event emitted after a request is handled.",
- ["Configure `access_logger=` on `Quater(...)` to receive these events."],
- )
- lines.extend(signature_block(class_signature(package, "AccessLogEvent")))
- lines.extend(field_table(package, "AccessLogEvent"))
- lines.extend(
- [
- "`AccessLogEvent.to_dict()` returns a plain dictionary for loggers that",
- "expect JSON-like data.",
- "",
- ]
- )
- symbol_intro(
- lines,
- package,
- "AccessLogHook",
- "Callable type for access-log hooks.",
- ["The hook is async so it can write to async logging or telemetry clients."],
- )
- lines.extend(
- code_block(f"AccessLogHook = {attribute_value(package, 'AccessLogHook')}")
- )
- symbol_intro(
- lines,
- package,
- "ToolAuditEvent",
- "Structured event emitted for MCP tool calls.",
- [
- "Tool arguments are redacted before the event reaches your audit hook.",
- "Use this for visibility, not for authorization.",
- ],
- )
- lines.extend(signature_block(class_signature(package, "ToolAuditEvent")))
- lines.extend(field_table(package, "ToolAuditEvent"))
- return finish(lines)
-
-
-def render_testing(package: Any) -> str:
- lines = new_page("Testing Reference")
- lines.extend(
- [
- "Use the in-process clients to test Quater apps without starting Granian",
- "or opening a socket. This keeps tests fast and makes auth, cookies,",
- "lifespan hooks, MCP tools, and response bodies straightforward to assert.",
- "",
- "For examples, read the [Testing guide](/en/dev/testing).",
- "",
- "```python",
- "from quater import CliTestClient, MCPTestClient, TestClient, TestResponse",
- "```",
- "",
- ]
- )
- symbol_intro(
- lines,
- package,
- "TestClient",
- "Async in-process client for HTTP-style tests.",
- ["Use it with `async with` when your app has startup or shutdown hooks."],
- )
- lines.extend(signature_block(class_signature(package, "TestClient")))
- lines.extend(
- validated_option_table(
- package,
- "TestClient",
- None,
- "Constructor parameters",
- TEST_CLIENT_OPTIONS,
- )
- )
- lines.extend(
- [
- "Common methods:",
- "",
- "| Method | Use it for |",
- "| --- | --- |",
- "| `request(method, path, ...)` | Send any method. |",
- "| `get`, `post`, `put`, `patch`, `delete` | Convenience methods. |",
- "| `set_cookie(name, value)` | Store a cookie for later requests. |",
- "| `clear_cookies()` | Clear stored cookies. |",
- "| `startup()` / `shutdown()` | Run lifespan manually. |",
- "",
- ]
- )
- lines.extend(signature_block(method_signature(package, "TestClient", "request")))
- lines.extend(
- validated_option_table(
- package,
- "TestClient",
- "request",
- "`request()` parameters",
- TEST_CLIENT_REQUEST_OPTIONS,
- )
- )
- symbol_intro(
- lines,
- package,
- "TestResponse",
- "Response returned by [`TestClient`](#symbol-testclient).",
- ["It stores the collected body, headers, status code, and JSON helpers."],
- )
- lines.extend(signature_block(class_signature(package, "TestResponse")))
- lines.extend(
- validated_option_table(
- package,
- "TestResponse",
- None,
- "Constructor parameters",
- TEST_RESPONSE_OPTIONS,
- )
- )
- lines.extend(
- [
- "| Property or method | What it returns |",
- "| --- | --- |",
- "| `status_code` | Integer response status. |",
- "| `headers` | Parsed response headers. |",
- "| `body` | Raw response bytes. |",
- "| `text` | UTF-8 decoded body. |",
- "| `is_success` | `True` for `2xx` and `3xx` responses. |",
- "| `json()` | Parsed JSON body. |",
- "",
- ]
- )
- symbol_intro(
- lines,
- package,
- "MCPTestClient",
- "Small JSON-RPC helper bound to a [`TestClient`](#symbol-testclient).",
- [
- "Use `client.mcp` in tests. It sends MCP requests through the same app",
- "pipeline as a real MCP client.",
- ],
- )
- lines.extend(signature_block(class_signature(package, "MCPTestClient")))
- lines.extend(
- validated_option_table(
- package,
- "MCPTestClient",
- None,
- "Constructor parameters",
- MCP_TEST_CLIENT_OPTIONS,
- )
- )
- lines.extend(
- [
- "Common methods:",
- "",
- "| Method | Use it for |",
- "| --- | --- |",
- "| `initialize(...)` | Send MCP `initialize`. |",
- "| `tools_list(...)` | List exposed tools. |",
- "| `tools_call(name, arguments, ...)` | Call an exposed tool. |",
- "| `request(payload, ...)` | Send a custom JSON-RPC payload. |",
- "",
- ]
- )
- lines.extend(
- signature_block(method_signature(package, "MCPTestClient", "tools_call"))
- )
- lines.extend(
- validated_option_table(
- package,
- "MCPTestClient",
- "tools_call",
- "`tools_call()` parameters",
- MCP_TOOLS_CALL_OPTIONS,
- )
- )
- symbol_intro(
- lines,
- package,
- "CliTestClient",
- "Remote-action helper bound to a [`TestClient`](#symbol-testclient).",
- [
- "Use `client.cli` in tests. It calls actions and reads the action",
- "manifest through the same remote-action endpoints as the Quater CLI.",
- ],
- )
- lines.extend(signature_block(class_signature(package, "CliTestClient")))
- lines.extend(
- validated_option_table(
- package,
- "CliTestClient",
- None,
- "Constructor parameters",
- CLI_TEST_CLIENT_OPTIONS,
- )
- )
- lines.extend(
- [
- "Common methods:",
- "",
- "| Method | Use it for |",
- "| --- | --- |",
- "| `call(action, arguments, ...)` | Call an exposed CLI action. |",
- "| `manifest(...)` | Read the action manifest. |",
- "",
- "Both methods return the raw [`TestResponse`](#symbol-testresponse). A",
- "successful action body is the `{ok, status_code, body}` envelope, and a",
- "`dry_run=True` call returns the preflight payload instead of running the",
- "handler.",
- "",
- ]
- )
- lines.extend(signature_block(method_signature(package, "CliTestClient", "call")))
- lines.extend(
- validated_option_table(
- package,
- "CliTestClient",
- "call",
- "`call()` parameters",
- CLI_CALL_OPTIONS,
- )
- )
- lines.extend(
- type_section(
- (
- (
- "HeaderItems",
- "`(name, value)` header pairs. See "
- "[Request headers](./request#headers).",
- ),
- (
- "QueryParams",
- "Mapping or sequence accepted by "
- "[`TestClient`](#symbol-testclient).",
- ),
- (
- "RequestContent",
- "`bytes`, `bytearray`, `memoryview`, or `str` request body "
- "content.",
- ),
- ("JSONRPCID", "MCP JSON-RPC request id, either `str` or `int`."),
- )
- )
- )
- return finish(lines)
-
-
-def new_page(title: str) -> list[str]:
- return [GENERATED_HEADER, "", f"# {title}", ""]
-
-
-def symbol_intro(
- lines: list[str],
- package: Any,
- symbol: str,
- summary: str,
- details: Sequence[str],
-) -> None:
- lines.extend(
- [symbol_heading(symbol), "", source_line(package, symbol), "", summary, ""]
- )
- if details:
- lines.extend([*details, ""])
-
-
-def source_line(package: Any, symbol: str) -> str:
- if symbol == "__version__":
- return "Public import: `from quater import __version__`."
- return f"Public import: `from quater import {symbol}`."
-
-
-def class_signature(package: Any, symbol: str) -> str:
- obj = object_for(package, symbol)
- init = getattr(obj, "members", {}).get("__init__")
- if init is None:
- return symbol
- signature = function_signature(resolve(init))
- if signature is None:
- return symbol
- signature = signature.replace("__init__(", f"{symbol}(", 1)
- signature = signature.removesuffix(" -> None")
- return signature
-
-
-def method_signature(package: Any, symbol: str, method: str) -> str:
- obj = object_for(package, symbol)
- member = getattr(obj, "members", {}).get(method)
- if member is None:
- raise SystemExit(f"Could not find {symbol}.{method}")
- signature = function_signature(resolve(member))
- if signature is None:
- raise SystemExit(f"Could not read signature for {symbol}.{method}")
- return signature
-
-
-def callable_signature(package: Any, symbol: str) -> str:
- signature = function_signature(resolve(object_for(package, symbol)))
- if signature is None:
- raise SystemExit(f"Could not read signature for {symbol}")
- return signature
-
-
-def parameter_table(
- package: Any,
- symbol: str,
- method: str | None,
-) -> tuple[tuple[str, str], ...]:
- obj = object_for(package, symbol)
- if method is None:
- member = getattr(obj, "members", {}).get("__init__")
- else:
- member = getattr(obj, "members", {}).get(method)
- if member is None:
- target = f"{symbol}.{method}" if method is not None else f"{symbol}.__init__"
- raise SystemExit(f"Could not find {target}")
-
- resolved = resolve(member)
- parameters = getattr(resolved, "parameters", ())
- rows: list[tuple[str, str]] = []
- for parameter in parameters:
- name = getattr(parameter, "name", "")
- if not name or name == "self":
- continue
- annotation_value = getattr(parameter, "annotation", None)
- type_name = "object" if annotation_value is None else str(annotation_value)
- rows.append((name, clean_type(type_name)))
- return tuple(rows)
-
-
-def function_parameter_table(package: Any, symbol: str) -> tuple[tuple[str, str], ...]:
- obj = resolve(object_for(package, symbol))
- parameters = getattr(obj, "parameters", ())
- rows: list[tuple[str, str]] = []
- for parameter in parameters:
- name = getattr(parameter, "name", "")
- if not name:
- continue
- annotation_value = getattr(parameter, "annotation", None)
- type_name = "object" if annotation_value is None else str(annotation_value)
- rows.append((name, clean_type(type_name)))
- return tuple(rows)
-
-
-def function_signature(obj: Any) -> str | None:
- signature_method = getattr(obj, "signature", None)
- if signature_method is None:
- return None
- return format_signature(clean_signature(str(signature_method())))
-
-
-def field_table(package: Any, symbol: str) -> list[str]:
- obj = object_for(package, symbol)
- annotations = init_annotations(obj)
- rows: list[str] = []
- actual_fields: set[str] = set()
- for name, member in getattr(obj, "members", {}).items():
- if is_private_member(name) or name in {"__slots__", "__test__"}:
- continue
- target = resolve(member)
- kind = str(getattr(target, "kind", ""))
- if not kind.endswith("ATTRIBUTE"):
- continue
- actual_fields.add(name)
- type_name = annotation(target)
- if type_name == "object":
- type_name = annotations.get(name, type_name)
- description = FIELD_DOCS.get(symbol, {}).get(name)
- if description is None:
- continue
- rows.append(
- f"| `{name}` | {type_cell(type_name)} | {table_text(description)} |"
- )
-
- documented_fields = set(FIELD_DOCS.get(symbol, {}))
- if actual_fields != documented_fields:
- missing = sorted(actual_fields - documented_fields)
- extra = sorted(documented_fields - actual_fields)
- raise SystemExit(
- f"{symbol} field docs mismatch; missing={missing}, extra={extra}"
- )
-
- if not rows:
- return []
-
- return [
- "Fields:",
- "",
- "| Field | Type | Meaning |",
- "| --- | --- | --- |",
- *rows,
- "",
- ]
-
-
-def option_table(
- title: str,
- rows: Sequence[tuple[str, str, str]],
-) -> list[str]:
- lines = [f"### {title}", "", "| Name | Type | Meaning |", "| --- | --- | --- |"]
- for name, type_name, description in rows:
- lines.append(
- f"| `{name}` | {type_cell(type_name)} | {table_text(description)} |"
- )
- lines.append("")
- return lines
-
-
-def validated_option_table(
- package: Any,
- symbol: str,
- method: str | None,
- title: str,
- rows: Sequence[tuple[str, str, str]],
-) -> list[str]:
- actual = parameter_table(package, symbol, method)
- expected = tuple((name, type_name) for name, type_name, _ in rows)
- if actual != expected:
- target = f"{symbol}.{method}" if method is not None else symbol
- raise SystemExit(
- f"{target} reference table mismatch; actual={actual}, expected={expected}"
- )
- return option_table(title, rows)
-
-
-def validated_function_option_table(
- package: Any,
- symbol: str,
- title: str,
- rows: Sequence[tuple[str, str, str]],
-) -> list[str]:
- actual = function_parameter_table(package, symbol)
- expected = tuple((name, type_name) for name, type_name, _ in rows)
- if actual != expected:
- raise SystemExit(
- f"{symbol} reference table mismatch; actual={actual}, expected={expected}"
- )
- return option_table(title, rows)
-
-
-def type_section(rows: Sequence[tuple[str, str]]) -> list[str]:
- lines = ["## Type Names Used Here", "", "| Name | Meaning |", "| --- | --- |"]
- for name, description in rows:
- lines.append(f"| {type_name_cell(name)} | {table_text(description)} |")
- lines.append("")
- return lines
-
-
-def init_annotations(obj: Any) -> dict[str, str]:
- init = getattr(obj, "members", {}).get("__init__")
- if init is None:
- return {}
- target = resolve(init)
- parameters = getattr(target, "parameters", ())
- annotations: dict[str, str] = {}
- for parameter in parameters:
- name = getattr(parameter, "name", "")
- value = getattr(parameter, "annotation", None)
- if not name or name == "self" or value is None:
- continue
- annotations[name] = str(value)
- return annotations
-
-
-def object_for(package: Any, symbol: str) -> Any:
- return resolve(package[symbol])
-
-
-def resolve(obj: Any) -> Any:
- return getattr(obj, "target", obj)
-
-
-def annotation(obj: Any) -> str:
- value = getattr(obj, "annotation", None)
- if value is None:
- return "object"
- return str(value)
-
-
-def attribute_value(package: Any, symbol: str) -> str:
- value = getattr(object_for(package, symbol), "value", None)
- if value is None:
- return clean_signature(annotation(object_for(package, symbol)))
- return clean_signature(str(value))
-
-
-def code_block(code: str) -> list[str]:
- return ["```python", code, "```", ""]
-
-
-def signature_block(signature: str) -> list[str]:
- return code_block(format_signature(signature))
-
-
-def import_link(name: str, page: ReferencePage) -> str:
- target = f"./{page.slug}#{symbol_anchor(name)}"
- return f"[`{name}`]({target})"
-
-
-def type_cell(value: str) -> str:
- text = clean_type(value)
- if not any(type_reference(match.group(0)) for match in type_words(text)):
- return f"`{table_code(text)}`"
-
- escaped = table_text(text)
- return re.sub(
- r"\b[A-Za-z_][A-Za-z0-9_]*\b",
- lambda match: type_token(match.group(0)),
- escaped,
- )
-
-
-def type_name_cell(name: str) -> str:
- anchor = f''
- link = public_type_reference(name)
- if link is not None:
- return f"{anchor}[`{name}`]({link})"
- return f"{anchor}`{name}`"
-
-
-def type_token(name: str) -> str:
- link = type_reference(name)
- if link is None:
- return name
- return f"[`{name}`]({link})"
-
-
-def type_words(value: str) -> Iterable[re.Match[str]]:
- return re.finditer(r"\b[A-Za-z_][A-Za-z0-9_]*\b", value)
-
-
-def type_reference(name: str) -> str | None:
- public_link = public_type_reference(name)
- if public_link is not None:
- return public_link
-
- internal_links = {
- "HeaderItems": "/en/dev/reference/request#type-headeritems",
- "RequestBody": "/en/dev/reference/request#type-requestbody",
- "RequestContext": "/en/dev/reference/request#call-context",
- "Headers": "/en/dev/reference/request#headers",
- "QueryParams": "/en/dev/reference/request#queryparams",
- "Cookies": "/en/dev/reference/request#cookies",
- "SecurityMode": "/en/dev/reference/application#type-securitymode",
- "MaxBodySize": "/en/dev/reference/application#type-maxbodysize",
- "Authenticate": "/en/dev/reference/auth#type-authenticate",
- "AuditHook": "/en/dev/reference/application#type-audithook",
- "BeforeMiddleware": "/en/dev/reference/application#type-beforemiddleware",
- "AfterMiddleware": "/en/dev/reference/application#type-aftermiddleware",
- "AroundMiddleware": "/en/dev/reference/application#type-aroundmiddleware",
- "ExceptionHandlerEntry": (
- "/en/dev/reference/application#type-exceptionhandlerentry"
- ),
- "ResourceMap": "/en/dev/reference/resources#type-resourcemap",
- "ResourceProvider": "/en/dev/reference/resources#type-resourceprovider",
- "ResourceScope": "/en/dev/reference/resources#type-resourcescope",
- "ResponseBody": "/en/dev/reference/responses#type-responsebody",
- "RequestContent": "/en/dev/reference/testing#type-requestcontent",
- "JSONRPCID": "/en/dev/reference/testing#type-jsonrpcid",
- "SecretValue": "/en/dev/reference/auth#type-secretvalue",
- }
- return internal_links.get(name)
-
-
-def public_type_reference(name: str) -> str | None:
- for page in PAGES:
- if name in page.symbols and name != "__version__":
- return f"/en/dev/reference/{page.slug}#{symbol_anchor(name)}"
- return None
-
-
-def type_anchor(name: str) -> str:
- return f"type-{slug(name)}"
-
-
-def table_code(value: str) -> str:
- return value.replace("|", "\\|")
-
-
-def table_text(value: str) -> str:
- return value.replace("|", "\\|")
-
-
-def context_row(name: str, type_name: str, description: str) -> str:
- return f"| `{name}` | {type_cell(type_name)} | {table_text(description)} |"
-
-
-def format_signature(signature: str, *, max_width: int = 88) -> str:
- if len(signature) <= max_width:
- return signature
-
- open_index = signature.find("(")
- close_index = signature.rfind(")")
- if open_index == -1 or close_index == -1 or close_index < open_index:
- return signature
-
- head = signature[:open_index]
- args = split_top_level_commas(signature[open_index + 1 : close_index])
- tail = signature[close_index + 1 :]
- if not args:
- return signature
-
- lines = [f"{head}("]
- lines.extend(f" {argument}," for argument in args)
- lines.append(f"){tail}")
- return "\n".join(lines)
-
-
-def clean_signature(value: str) -> str:
- replacements = {
- "mcp_docs_path: str | None | _Unset = _UNSET": (
- "mcp_docs_path: str | None = '/mcp/docs'"
- ),
- "docs_path: str | None | _Unset = _UNSET": ("docs_path: str | None = '/docs'"),
- "openapi_path: str | None | _Unset = _UNSET": (
- "openapi_path: str | None = '/openapi.json'"
- ),
- "request_id_header: str | None | _Unset = _UNSET": (
- "request_id_header: str | None = 'x-request-id'"
- ),
- "_empty_str_map()": "...",
- "_empty_metadata()": "...",
- "_MCP_PROTOCOL_VERSION": "'2025-11-25'",
- "Callable[['AccessLogEvent'], Awaitable[None]]": (
- "Callable[[AccessLogEvent], Awaitable[None]]"
- ),
- "_DEFAULT_METHODS": (
- "('DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT')"
- ),
- }
- for old, new in replacements.items():
- value = value.replace(old, new)
- return value
-
-
-def clean_type(value: str) -> str:
- replacements = {
- "str | None | _Unset": "str | None",
- }
- for old, new in replacements.items():
- value = value.replace(old, new)
- return value
-
-
-def split_top_level_commas(value: str) -> list[str]:
- parts: list[str] = []
- start = 0
- depth = 0
- quote: str | None = None
- escaped = False
- for index, char in enumerate(value):
- if quote is not None:
- if escaped:
- escaped = False
- elif char == "\\":
- escaped = True
- elif char == quote:
- quote = None
- continue
- if char in {"'", '"'}:
- quote = char
- continue
- if char in "([{":
- depth += 1
- continue
- if char in ")]}":
- depth -= 1
- continue
- if char == "," and depth == 0:
- part = value[start:index].strip()
- if part:
- parts.append(part)
- start = index + 1
- tail = value[start:].strip()
- if tail:
- parts.append(tail)
- return parts
-
-
-def is_private_member(name: str) -> bool:
- return name.startswith("_")
-
-
-def slug(value: str) -> str:
- normalized = re.sub(r"[^a-z0-9]+", "-", value.lower())
- return normalized.strip("-")
-
-
-def symbol_anchor(value: str) -> str:
- if value == "__version__":
- return "symbol-version"
- return f"symbol-{slug(value)}"
-
-
-def symbol_heading(value: str) -> str:
- return f"## {value} {{#{symbol_anchor(value)}}}"
-
-
-def finish(lines: list[str]) -> str:
- return "\n".join(lines).rstrip() + "\n"
-
-
-def write_outputs(outputs: Mapping[Path, str]) -> None:
- REFERENCE_DIR.mkdir(parents=True, exist_ok=True)
- for path, content in sorted(outputs.items()):
- path.write_text(content, encoding="utf-8")
-
-
-def check_outputs(outputs: Mapping[Path, str]) -> int:
- stale = False
- for path, expected in sorted(outputs.items()):
- if not path.exists():
- print(f"Missing generated reference file: {path}", file=sys.stderr)
- stale = True
- continue
- actual = path.read_text(encoding="utf-8")
- if actual == expected:
- continue
- stale = True
- diff = difflib.unified_diff(
- actual.splitlines(),
- expected.splitlines(),
- fromfile=str(path),
- tofile=f"{path} (generated)",
- lineterm="",
- )
- print("\n".join(diff), file=sys.stderr)
- return 1 if stale else 0
-
-
if __name__ == "__main__":
raise SystemExit(main())