Skip to content

Generator API reference

The generator package exposes two supported entry points:

  • generate(manifest) — the manifest-driven public API that powers okapipy generate.
  • generate_for_mount(api, raw_spec, ...) — the per-mount building block used by generate and exposed for tests and embedded callers.

Both return a virtual filesystem (dict[str, GeneratedFile]). The CLI flushes the dict to disk via write_to_disk; tests inspect it directly.

Project manifest

The user-authored project manifest lives in okapipy.manifest.

manifest

Project manifest: the user-authored input that drives okapipy generate.

The manifest is a small YAML (or JSON) document checked into the consumer's repository and is the only input the generate command needs at the command line — every project-level option (package, client class, response shape, template directories) and every per-spec option (source, rules, strip-prefix, unmatched, language) lives inside it. The CLI default location is ./okapipy.yml.

A manifest carries one or more specs[] entries. Each entry pairs an OpenAPI spec source with the mount namespace under which its parser tree is composed into the generated client; an empty namespace mounts the spec at the root (the historical single-spec behavior). The generator parses every entry independently and then composes the results — multi- spec composition is not a parser concern.

This module is intentionally narrow: it owns the Pydantic models and the loader/validator. The composition logic that turns a list of parsed APIModels into a single generated client lives in okapipy.generator.compose.

DEFAULT_MANIFEST_FILENAME module-attribute

DEFAULT_MANIFEST_FILENAME = 'okapipy.yml'

Default filename the CLI looks for when --manifest is not given.

GenerationManifest

Bases: BaseModel

Project-wide configuration for okapipy generate.

Carries the project-level fields that drive one generated Python package (package, client_class, shape, …) plus the specs[] array describing what trees live inside it. The default file location is ./okapipy.yml; load_manifest(path) reads it from disk.

SpecEntry

Bases: BaseModel

One OpenAPI spec mounted under a namespace in the generated client.

Each entry is parsed independently with its own per-spec settings (rules, strip_prefix, unmatched, lang). The resulting APIModel is composed into the project under namespace; "" mounts at the root (single-spec, historical layout) while a dotted string nests under intermediate namespaces (platform.users shares the platform parent with platform.billing).

load_manifest

load_manifest(path: Path) -> GenerationManifest

Read and validate the project manifest at path.

The file extension drives format selection: .json is parsed as JSON, .yml / .yaml (and anything else) as YAML. Relative path fields on the manifest (source, rules, templates_dir, model_templates_dir, nlp_cache_dir, output) are resolved against the manifest file's parent directory before the model is returned, so the manifest is portable with its consumer repository.

Raises:

Type Description
ManifestNotFoundError

when the file does not exist on disk.

ManifestFormatError

when the file is malformed JSON/YAML or when the document fails schema validation (missing required fields, mount-namespace collisions, URL rules entries, …).

Returns:

Type Description
GenerationManifest

A populated, frozen GenerationManifest. Path fields are absolute.

apply_cli_overrides

apply_cli_overrides(
    manifest: GenerationManifest, *, output: Path | None = None
) -> GenerationManifest

Return a copy of manifest with selected fields replaced by CLI overrides.

Only the narrow set of fields that have a CLI counterpart is honored; every other manifest field requires editing the manifest itself. Today that set is just output.

Public entry points

generate

generate(manifest: GenerationManifest) -> dict[str, GeneratedFile]

Build the virtual FS for the project described by manifest.

Walks manifest.specs[], parses each OpenAPI document with the per-spec inputs declared in the manifest (rules, strip_prefix, unmatched, lang), and composes the result into one generated Python package: one pyproject.toml, one <Client>Base, one vendored runtime, one _generated.json. Each spec entry produces its own sub-tree at base/<mount_path>/... (or at the package root for the empty mount).

Parameters:

Name Type Description Default
manifest GenerationManifest

the project manifest produced by okapipy.manifest.load_manifest.

required

Returns:

Type Description
dict[str, GeneratedFile]

A dict[str, GeneratedFile] keyed on POSIX-style relative paths.

Raises:

Type Description
GenerationError

when a specs[] entry declares a dotted mount namespace (currently unsupported) or when the manifest fails cross-mount collision checks.

generate_for_mount

generate_for_mount(
    api: APIModel,
    raw_spec: dict[str, Any] | str | Path,
    *,
    output_dir: Path | None = None,
    package: str,
    client_class: str,
    project_name: str | None = None,
    project_description: str | None = None,
    project_version: str = "0.1.0",
    python_version: str = "3.13",
    license: str = "Proprietary",
    author: str | None = None,
    repo_url: str | None = None,
    templates_dir: Path | None = None,
    model_templates_dir: Path | None = None,
    shape: Shape = "auto"
) -> dict[str, GeneratedFile]

Build the virtual FS for a single-mount generated client project.

This is the lower-level entry point: it produces the full set of files for one OpenAPI spec mounted at the package root. The high-level manifest-driven entry point is generate(manifest).

Parameters:

Name Type Description Default
api APIModel

parsed APIModel produced by okapipy.parser.api.parse.

required
raw_spec dict[str, Any] | str | Path

original OpenAPI document (path, URL string, or already-loaded dict). Forwarded to datamodel-code-generator for models.py emission.

required
output_dir Path | None

accepted for symmetry with generate(manifest); not used.

None
package str

dotted package path for the generated client (e.g. "acme.commerce").

required
client_class str

PascalCase class name for the sync client; async sibling is Async<client_class>. Base classes carry an additional Base suffix.

required
project_name str | None

PEP 503 distribution name. Defaults to the last segment of package.

None
project_description str | None

free-form PEP 621 description field for pyproject.toml. Defaults to "Generated client for <project_name>".

None
project_version str

initial version string emitted into pyproject.toml.

'0.1.0'
python_version str

pinned Python version for the generated project.

'3.13'
license str

SPDX identifier; drives the LICENSE placeholder.

'Proprietary'
author str | None

copyright holder for the generated LICENSE and PEP 621 authors entry in pyproject.toml. When omitted, the LICENSE falls back to the project name and pyproject.toml omits the authors block.

None
repo_url str | None

optional source-repository URL. When set, drives the [project.urls] table in pyproject.toml (Homepage, Repository, and — for github.com URLs — Issues).

None
templates_dir Path | None

optional directory of user templates. Resolved before the packaged defaults (ChoiceLoader).

None
model_templates_dir Path | None

optional directory of datamodel-code-generator templates. Forwarded as dmcg's custom_template_dir.

None
shape Shape

response-shape policy for the generated client.

  • "auto" (default) — emit a dual-shape client. The constructor accepts a shape: "models" | "dicts" keyword and with_shape(...) returns a sibling switching shape at runtime. Bodies and returns are typed as Foo | dict[str, Any] / Foo | dict[str, Any] | None.
  • "models" — lock the client to typed Pydantic models. The shape= constructor option and with_shape(...) are dropped. Bodies are typed as Foo; returns as Foo | None.
  • "dicts" — lock the client to raw dicts. base/models.py is skipped, model imports are dropped, and bodies / returns are typed as dict[str, Any] / dict[str, Any] | None.
'auto'

Returns:

Type Description
dict[str, GeneratedFile]

A dict[str, GeneratedFile] mapping POSIX-style relative paths to

dict[str, GeneratedFile]

file content + lifecycle policy.

The virtual filesystem

vfs

Virtual filesystem with file-lifecycle metadata, state pruning, and drift detection.

The generator builds a dict[str, GeneratedFile] keyed on POSIX-style paths relative to the output directory. Each GeneratedFile carries the file's content plus its lifecycle policy:

  • one_shot=False (default): regenerated every run. write_to_disk always overwrites. Everything under base/ plus py.typed falls in this bucket.
  • one_shot=True: emitted exactly once on first generation. write_to_disk skips paths that already exist. User-layer stubs and the project skeleton use this lifecycle.

write_to_disk returns a WriteReport:

  • Pruning. Reads the previous base/_generated.json from disk; deletes any base file in previous.base_files that isn't in the current VFS. User-layer files are never pruned.
  • Drift detection. Compares previous and current state-file edges; emits one warning per new/removed wiring on a one-shot user-layer parent that needs a manual edit.
  • Dry-run. dry_run=True computes the report without touching disk; WriteReport.would_change is True if anything would change. Powers okapipy generate --check.

GeneratedFile dataclass

GeneratedFile(content: str, one_shot: bool = False)

A single emitted file plus its lifecycle policy.

WriteReport dataclass

WriteReport(
    written: list[str] = list(),
    skipped: list[str] = list(),
    pruned: list[str] = list(),
    warnings: list[str] = list(),
    would_change: bool = False,
)

Outcome of a write_to_disk call.

would_change is True if writing would alter the on-disk state in any way (a base file's content differs, a one-shot file is missing, or a stale file would be pruned). The generated-state file itself is excluded from this check because its generated_at timestamp differs on every run.

write_to_disk

write_to_disk(
    vfs: dict[str, GeneratedFile], output_dir: Path, *, dry_run: bool = False
) -> WriteReport

Flush vfs to output_dir. Prune stale base files. Emit drift warnings.

Parameters:

Name Type Description Default
vfs dict[str, GeneratedFile]

the generator's virtual filesystem.

required
output_dir Path

target directory the project is being written into.

required
dry_run bool

when True, no files are written, deleted, or modified — the returned report reflects what would happen.

False

Models

models

datamodel-code-generator integration.

Loads the OpenAPI spec, runs flatten_inline_schemas to hoist anonymous inline schemas into components.schemas (so dmcg doesn't emit By / By1 / By2 duplicates for structurally identical shapes), writes the preprocessed document to a temp directory, invokes dmcg with output_model_type=PydanticV2BaseModel, and reads the resulting models.py back into the virtual FS. The user-supplied model_templates_dir is forwarded to dmcg as custom_template_dir; when omitted, the bundled relaxed templates under templates/model/ are used: every field is forced optional (| None), extra="allow" and populate_by_name=True, and the dmcg-generated Field(...) call is preserved verbatim so spec-level constraints (max_length, pattern, …) and metadata (title, description, examples) survive. When a field has an alias from snake_case_field=True, the bare alias= kwarg is rewritten to validation_alias=AliasChoices(snake, original), serialization_alias=original so payloads can be sent or received under either name.

emit_models

emit_models(
    raw_spec: dict[str, Any] | str | Path,
    model_templates_dir: Path | None,
    python_version: str,
) -> str

Render models.py content from raw_spec.

Returns the rendered Python source (banner-prefixed). The caller places it into the virtual FS under <package>/models.py.

Parameters:

Name Type Description Default
raw_spec dict[str, Any] | str | Path

original OpenAPI document — a path to a file/URL, or an already loaded dict. Forwarded to dmcg's input_ argument.

required
model_templates_dir Path | None

directory of dmcg Jinja templates that override the built-in ones; passed straight through to custom_template_dir. When None, the bundled relaxed templates under templates/model/ are used.

required
python_version str

target Python version, e.g. "3.13". Maps to dmcg's target_python_version enum.

required

public_names

public_names(source: str) -> set[str]

Return the set of top-level identifiers defined in source.

Used by the walker to filter from ..models import ... lines so they only reference symbols dmcg actually emitted. dmcg sometimes drops or aliases schemas (primitive aliases, empty objects, schemas resolving to Any), so a parser-recovered ref name like Data may not exist in the output. Includes class names, simple Name = ... assignments, and Name: ... = ... annotated assignments — covers both class Foo(BaseModel) and dmcg's Foo = list[Bar] / Foo: TypeAlias = ... outputs.

Generated-state file and edges

state

Cross-run generated-state file: tracks regenerated files and parser-tree edges.

The state file is written to src/{package_path}/base/_generated.json on every generation. It is named _generated.json (rather than the historical _manifest.json) so it is not confused with the user-authored project manifest read by okapipy.manifest. It serves two operational purposes:

  • Pruning. base_files is the set of files the regenerated tree owns. On the next generation, previous.base_files - current.base_files is the set of stale files (their parser-tree node no longer exists in the spec) that write_to_disk deletes from disk. User-layer files are never tracked here and never pruned.
  • Drift detection. edges is the set of parent → child wirings the current parser tree implies. On the next generation, current.edges - previous.edges are NEW children whose user-layer parent stub (one-shot, never overwritten) doesn't yet wire them; the difference is converted to warnings telling the user the exact one-line edit to make.

Edges are stored abstractly — one entry per sync/async pair, not per emitted Python class. The drift-warning formatter expands one Edge into the two lines (sync + async) the user needs to add.

This module is intentionally narrow: it owns the dataclasses and the JSON-on-disk format, and nothing else. The graph-walking logic that produces edges from a parsed APIModel lives in okapipy.generator.edges, kept apart so that state.py does not depend on emit/stubs.py (which itself depends on vfs.py, which depends back on state.py).

STATE_FILENAME module-attribute

STATE_FILENAME = '_generated.json'

Filename inside <package>/base/ where the generated-state file is stored.

GeneratedState dataclass

GeneratedState(
    generator_version: str, generated_at: str, base_files: list[str], edges: list[Edge]
)

The full generated-state record written under base/_generated.json.

Edge dataclass

Edge(
    parent_module: str, factory_attr: str, child_user_class: str, child_user_module: str
)

One parent → child wiring in the parser tree.

parent_module is the user-layer relative path (client.py, namespaces/commerce.py, collections/orders.py, …) — i.e. where the drift detector looks to check if the user has wired the factory. child_user_class is the sync user-layer class name; the async sibling is implicit (Async + the same name).

serialize

serialize(state: GeneratedState) -> str

Render the generated-state record as a deterministic JSON string.

edges

Generated-state edge computation: walks the parser tree and emits one Edge per wiring.

Lives in its own module so state.py can stay free of dependencies on emit/stubs.py and emit/walk.py. Without that split the import graph cycles (vfs → state → stubs → vfs), forcing function-local imports inside state.py. By concentrating the stubs/walk dependency here — at a higher layer than vfs.py — the cycle disappears and every import can stay at module scope.

compute_edges mirrors the auto-wiring logic in emit/stubs.py: each entry in the returned list represents the same __<factory>__ = ChildClass line a stub would emit. Drift detection on a later run computes current - previous over the generated-state file's edges field to surface child wirings the user hasn't yet hand-edited into their one-shot stubs.

compute_state

compute_state(
    api: APIModel, package: str, base_files: list[str], *, mount_relpath: str = ""
) -> GeneratedState

Build the generated-state record for the current generation.

base_files is the list of POSIX-style VFS keys that fall inside base/. The generated_at timestamp uses ISO-8601 with second precision in UTC so the state record is reproducible across runs that happen within the same second.

mount_relpath is forwarded to compute_edges so multi-mount projects record mount-prefixed parent/child module paths.

compute_edges

compute_edges(api: APIModel, package: str, *, mount_relpath: str = '') -> list[Edge]

Walk the parser tree and return one Edge per parent → child wiring.

mount_relpath lets multi-mount projects record parent_module paths that include the mount sub-directory (users/namespaces/foo.py), so drift detection finds the right user-layer stub on disk.

Top-level wirings for the root mount (mount_relpath == "") parent on client.py; for non-root mounts they parent on the mount namespace stub at <mount_relpath>__init__.py, since that is where the user-layer <Mount>Mount subclass that owns those wirings actually lives.

Mount composition

compose

Multi-spec composition: tag each parsed APIModel with its manifest mount path.

The parser is single-spec by design; this module turns a list of parsed trees plus their manifest mount declarations into the small data structure the generator's emit pipeline consumes. A MountedSpec is the unit the emit loop iterates over — one per specs[] entry in the project manifest, with the per-spec inputs (mount path, parsed APIModel, raw spec for datamodel-code-generator) bundled together so downstream code does not have to thread separate lists.

The mount path is a tuple of segments (("users",) for namespace: users; ("platform", "users") for namespace: platform.users; () for the root mount declared with namespace: ""). The same tuple drives:

  • the on-disk sub-directory under base/ and the user layer (base/platform/users/...),
  • the synthetic mount-namespace tree the client class wires its accessors through, and
  • the cross-mount-collision check that defends the emit pipeline against any manifest that snuck past the loader.

MountedSpec dataclass

MountedSpec(
    mount_path: tuple[str, ...],
    api: APIModel,
    raw_spec: dict[str, Any] | str | Path,
    spec_index: int,
)

One spec entry's parser output tagged with its mount path.

mount_path is the canonical tuple form (() for the root mount). The raw spec is preserved alongside the parsed APIModel because datamodel-code-generator consumes the raw document — not the parser's tree — and the emit pipeline calls it once per mount.

spec_index records the entry's original position in the manifest's specs[] array. Emit order matches manifest order so successive regenerations produce deterministic VFS contents.

mount_segments

mount_segments(namespace: str) -> tuple[str, ...]

Split a dotted manifest mount string into its segment tuple.

"" (or whitespace-only) returns the empty tuple — the root mount. Leading / trailing dots are stripped before splitting so a typo like .users doesn't produce a phantom empty leading segment.

Parameters:

Name Type Description Default
namespace str

the specs[].namespace value from the manifest.

required

Returns:

Type Description
tuple[str, ...]

Tuple of segments suitable for path joining and topology checks.

mount_relpath

mount_relpath(mount_path: Sequence[str]) -> str

Render a mount tuple as a POSIX relative path component.

The root mount produces the empty string so callers can build paths via simple concatenation (f"src/{pkg}/base/{relpath}collections/...") without a leading slash when there is no mount segment to prepend.

Parameters:

Name Type Description Default
mount_path Sequence[str]

the mount tuple, typically MountedSpec.mount_path.

required

Returns:

Type Description
str

"users/" for ("users",), "platform/users/" for

str

("platform", "users"), "" for ().

check_mount_collisions

check_mount_collisions(mounts: Sequence[MountedSpec]) -> None

Defensive cross-mount uniqueness check.

The manifest loader already enforces this; running it again at the composition boundary catches programmatic construction paths that sidestep the loader (tests, embedded callers).

Raises:

Type Description
GenerationError

when two entries share the same mount_path.

iter_mount_namespace_prefixes

iter_mount_namespace_prefixes(mounts: Sequence[MountedSpec]) -> list[tuple[str, ...]]

Enumerate every distinct mount-namespace prefix in deterministic order.

For mounts platform.users, platform.billing, and audit the result is [("audit",), ("platform",), ("platform", "billing"), ("platform", "users")] — sorted lexicographically by prefix so the client class's accessor tree emits in a stable, repeatable order. The empty prefix (root mount) is not included because it carries no synthetic namespace; its content sits directly on the client.

Parameters:

Name Type Description Default
mounts Sequence[MountedSpec]

the planned list of MountedSpecs.

required

Returns:

Type Description
list[tuple[str, ...]]

Sorted list of unique non-empty prefix tuples covering every

list[tuple[str, ...]]

intermediate and leaf segment introduced by the mounts.

Templating

templating

Jinja2 environment factory and post-format hook.

Single environment per generate() call. Loader is ChoiceLoader([user, packaged]) when the user passes templates_dir, otherwise just the packaged loader. Rendered Python files run through ruff format before they hit the virtual FS — non-Python files pass through unchanged.

make_environment

make_environment(templates_dir: Path | None) -> Environment

Build the Jinja2 environment used for the entire generation run.

templates_dir is searched first via FileSystemLoader; the packaged defaults in okapipy.generator.templates are searched second. StrictUndefined makes missing context variables fail loudly at render time rather than silently emitting "".

Errors

Generator error hierarchy.

GenerationError is the root; raise more specific subclasses for cases the user can act on (a template that didn't render, a template name that wasn't found, a ruff format failure carrying its stderr).

GenerationError

Bases: Exception

Base class for every error raised by the generator.

UnknownTemplateError

Bases: GenerationError

Raised when a referenced template name is not in the loader chain.

TemplateRenderError

TemplateRenderError(template_name: str, message: str)

Bases: GenerationError

Raised when Jinja2 fails to render a template (StrictUndefined, syntax, etc.).

template_name instance-attribute

template_name = template_name

FormatError

FormatError(template_name: str, stderr: str)

Bases: GenerationError

Raised when ruff format rejects a rendered Python file.

template_name instance-attribute

template_name = template_name

stderr instance-attribute

stderr = stderr

ManifestNotFoundError

Bases: GenerationError

Raised when the user-authored project manifest cannot be located on disk.

ManifestFormatError

Bases: GenerationError

Raised when the user-authored project manifest fails schema validation.

Generated runtime

These modules are vendored into every generated client as flat files directly under <package>/base/ (strategies.py, filters.py, sort.py, transport.py, exceptions.py, types.py). They're documented here because you'll import them in your user layer for custom strategies, custom filters, and custom transports — but you don't import them from the okapipy package itself at runtime.

Strategies

strategies

Pagination, filter, and sort strategies — Protocols and built-in implementations.

Strategies are stateless translators. Pagination strategies drive the iterator (initial, next, extract_items) and optionally produce count requests (supports_count, count_request_params, extract_count). Filter and sort strategies walk the user's Filter / Sort tree and produce wire parameters.

Strategies are duck-typed against the Protocols below — users do not have to inherit from them.

Pagination strategies own their default page size: each built-in requires a default_page_size argument that seeds initial(...) when the per-call page_size is None. The client no longer holds a default_page_size — it is a property of the wire dialect, which is exactly what the strategy models. Making it required (rather than int | None) is deliberate: a None would fall back to whatever default the backend chooses, leaving the client unable to tell what page size each request actually carries. Users can still override per-call via .page_size(n).

Filter and sort strategies return FilterEncoding / SortEncoding objects rather than bare params dicts. Each encoding carries both ordinary key/value params and an optional raw_query fragment for dialects whose expression must be emitted verbatim into the query string instead of as key=value pairs (e.g. RQL filters: ?and(eq(f1,v1),eq(f2,v2)), or RQL-style sort: ?ordering(+name,-created_at)). When both filter and sort produce raw fragments they are concatenated into the URL with & separators alongside the URL-encoded ordinary params (e.g. ?and(eq(f,v))&ordering(+name)&limit=100).

PaginationStrategy

Bases: Protocol

Drives the iterator and (optionally) the count and random-access requests.

supports_count / count_request_params / extract_count form the count capability. supports_random_access / page_params form the random-access capability — the ability to fetch the N-th page directly without walking pages 0..N-1. Cursor and link-header paginations are inherently sequential and report supports_random_access=False; offset and page-number paginations support it.

FilterStrategy

Bases: Protocol

Renders a Filter tree into a FilterEncoding.

SortStrategy

Bases: Protocol

Renders a Sort term list into a SortEncoding.

FilterEncoding dataclass

FilterEncoding(params: Mapping[str, Any] = dict(), raw_query: str | None = None)

The wire form of a Filter tree as produced by a FilterStrategy.

params are merged into the request's params= argument (key/value pairs that httpx URL-encodes). raw_query is a query-string fragment appended verbatim — required for expression-language filters like RQL where the operators (and(...), eq(...)) carry parentheses and commas that must not be split into separate parameters.

SortEncoding dataclass

SortEncoding(params: Mapping[str, Any] = dict(), raw_query: str | None = None)

The wire form of a Sort term list as produced by a SortStrategy.

Mirrors FilterEncoding. params are merged into the request's params= argument; raw_query is a query-string fragment appended verbatim — for sort dialects whose expression must not be URL-encoded as key=value pairs (e.g. an RQL-style ordering(+name,-created_at) term).

LimitOffsetPagination dataclass

LimitOffsetPagination(
    default_page_size: int,
    offset_param: str = "offset",
    limit_param: str = "limit",
    total_field: str | None = "total",
    total_header: str | None = None,
    content_range: bool = False,
)

?offset=&limit= pagination with configurable count source.

default_page_size is required — it seeds limit when the caller did not invoke .page_size(...). The per-call value passed to initial always wins when present.

total_field accepts a dotted path for envelopes that nest the total (e.g. "meta.pagination.total" reads body["meta"]["pagination"]["total"]).

PageNumberPagination dataclass

PageNumberPagination(
    default_page_size: int,
    page_param: str = "page",
    page_size_param: str = "page_size",
    start_page: int = 1,
    total_field: str | None = None,
    total_header: str | None = None,
)

?page=&page_size= pagination.

total_field accepts a dotted path for envelopes that nest the total (e.g. "meta.pagination.total" reads body["meta"]["pagination"]["total"]).

CursorPagination dataclass

CursorPagination(
    default_page_size: int,
    cursor_param: str = "cursor",
    next_cursor_field: str = "next_cursor",
    page_size_param: str | None = "page_size",
    content_range: bool = False,
)

Opaque-cursor pagination: ?cursor=... + a next-cursor field in the response.

page_size_param=None opts out of sending any size hint at all; otherwise default_page_size is used when the caller did not invoke .page_size(...).

LinkHeaderPagination dataclass

LinkHeaderPagination(
    default_page_size: int,
    page_size_param: str | None = "limit",
    content_range: bool = True,
    total_header: str | None = "X-Total-Count",
)

RFC 5988 Link: <…>; rel="next" pagination.

page_size_param=None opts out of sending any size hint at all; otherwise default_page_size is used when the caller did not invoke .page_size(...).

KeyValueFilter dataclass

KeyValueFilter()

Equality-only conjunctive filter: ?status=open&customer_id=42.

KeyOpValueFilter dataclass

KeyOpValueFilter()

Django-style operator-suffix filter: ?created_at__gte=…&status__in=….

SearchFilterStrategy dataclass

SearchFilterStrategy(param: str = 'q')

Single free-text param filter: ?q=…. Accepts only one Search leaf.

JsonFilterStrategy dataclass

JsonFilterStrategy(param: str = 'filter')

Encode the full Filter tree as a JSON expression in one query parameter.

CommaSignedSort dataclass

CommaSignedSort(param: str = 'sort')

?sort=-created_at,id — comma-joined, leading - for desc.

KeyDirectionSort dataclass

KeyDirectionSort(field_param: str = 'order_by', direction_param: str = 'order')

?order_by=created_at&order=desc — separate field/direction params.

Only single-term sort is meaningful for this encoding; multi-term raises.

JsonApiSort dataclass

JsonApiSort(param: str = 'sort')

JSON:API sort encoding (?sort=-created_at,id).

Identical wire format to CommaSignedSort with param='sort'; provided as a distinct class so user code can document the API style explicitly.

Filter and sort DSL

filters

Composable filter expressions.

Filter is the base class. Construct it directly with kwargs (Django-Q style) for the common case, or subclass for novel leaves (geo, JSON expressions, RSQL ASTs, etc.). Composition operators &, |, ~ build internal AndFilter / OrFilter / NotFilter nodes; concrete strategies walk the resulting tree at request time and produce wire parameters.

Filter

Filter(**kwargs: Any)

A filter expression. Subclassable for novel node types.

Default construction takes **kwargs and stores them on self.kwargs. Strategies inspect both type(node) and node.kwargs when encoding. Composition produces AndFilter, OrFilter, NotFilter — internal nodes that wrap operands.

iter_leaves

iter_leaves(of_type: type[Filter] | None = None) -> Iterator[Filter]

Yield leaf filters in tree order, optionally restricted to of_type.

Internal nodes (AndFilter / OrFilter / NotFilter) recurse; leaves either yield themselves (if matching) or skip.

without

without(of_type: type[Filter]) -> Filter | None

Return a copy of this tree with all leaves of of_type removed.

Returns None if the result is empty (every leaf was an instance).

AndFilter

AndFilter(left: Filter, right: Filter)

Bases: Filter

Conjunction of two filter expressions.

OrFilter

OrFilter(left: Filter, right: Filter)

Bases: Filter

Disjunction of two filter expressions.

NotFilter

NotFilter(operand: Filter)

Bases: Filter

Negation of a filter expression.

Search

Search(query: str)

Bases: Filter

Free-text query leaf used by search-style APIs (?q=…).

Construct as Search("running shoes"). The configured SearchFilterStrategy pulls self.query and emits the configured query parameter.

sort

Composable sort expressions.

Sort holds a list of (field, direction) terms. Composition via + appends; unary - flips every direction; Sort("-field") is shorthand for descending. Strategies (CommaSignedSort, KeyDirectionSort, JsonApiSort) walk the term list and emit the wire encoding their API expects.

Sort

Sort(field: str | None = None, direction: Direction = 'asc')

One or more sort terms. Composable via + and unary -.

Sort("created_at") ascending. Sort("-created_at") descending (the leading - is a shorthand). -Sort("created_at") flips direction. Adding two Sort instances concatenates their term lists; the empty Sort() is a valid neutral element.

Transport

transport

Retry policy and httpx transport wrapper for generated clients.

RetryTransport (sync) and AsyncRetryTransport (async) sit in front of any transport the user configures and re-issue the request when the response status is in RetryPolicy.retry_on_status or the underlying transport raises one of RetryPolicy.retry_on_exceptions.

Retries apply to GET requests only. Every other method bypasses the retry loop entirely and is forwarded to the wrapped transport untouched. This is deliberate: HTTP methods other than GET are not guaranteed to be idempotent, and silently re-issuing a POST/PATCH/PUT/DELETE after a failure would risk creating duplicate side effects on the server. Customers that need retry semantics for non-GET requests must add them at the application layer where the idempotency contract is known.

Backoff between attempts is exponential: delay = backoff * 2**attempt. A backoff of 0 means no sleep between attempts. total = 0 disables retries entirely.

RetryPolicy dataclass

RetryPolicy(
    total: int = 0,
    backoff: float = 0.0,
    retry_on_status: frozenset[int] = (lambda: frozenset({502, 503, 504}))(),
    retry_on_exceptions: tuple[type[BaseException], ...] = (httpx.TransportError,),
)

Configuration for RetryTransport.

total = 0 disables retries. backoff is the base seconds for exponential delay between attempts (delay = backoff * 2**attempt).

RetryTransport

RetryTransport(
    wrapped: BaseTransport,
    policy: RetryPolicy,
    sleep: Callable[[float], None] | None = None,
)

Bases: BaseTransport

A wrapper transport that retries GET requests per RetryPolicy.

Any non-GET request is forwarded to the wrapped transport untouched. GET requests are retried on matching status codes / exception types up to policy.total extra attempts, with exponential backoff.

AsyncRetryTransport

AsyncRetryTransport(wrapped: AsyncBaseTransport, policy: RetryPolicy)

Bases: AsyncBaseTransport

Async sibling of RetryTransport. Same GET-only rule.

Runtime exceptions

Exception hierarchy for generated clients.

Generated client code maps every non-2xx httpx response to an ApiError subclass so users have one tree to handle. Strategy errors live alongside — they share the same root because users will wrap their entire client call site in try: ... except ApiError.

ApiError

ApiError(
    message: str,
    *,
    status_code: int | None = None,
    request: Any = None,
    response: Any = None
)

Bases: Exception

Base for every error raised by the generated client.

status_code instance-attribute

status_code = status_code

request instance-attribute

request = request

response instance-attribute

response = response

ClientError

ClientError(
    message: str,
    *,
    status_code: int | None = None,
    request: Any = None,
    response: Any = None
)

Bases: ApiError

A 4xx response from the server.

ServerError

ServerError(
    message: str,
    *,
    status_code: int | None = None,
    request: Any = None,
    response: Any = None
)

Bases: ApiError

A 5xx response from the server.

ResponseValidationError

ResponseValidationError(
    message: str,
    *,
    status_code: int | None = None,
    request: Any = None,
    response: Any = None
)

Bases: ApiError

A 2xx response body failed Pydantic validation in models shape.

ConfigurationError

ConfigurationError(
    message: str,
    *,
    status_code: int | None = None,
    request: Any = None,
    response: Any = None
)

Bases: ApiError

A user-supplied strategy or option is invalid.

UnsupportedFilterError

UnsupportedFilterError(
    message: str,
    *,
    status_code: int | None = None,
    request: Any = None,
    response: Any = None
)

Bases: ApiError

The configured FilterStrategy cannot encode this Filter tree.

UnsupportedSortError

UnsupportedSortError(
    message: str,
    *,
    status_code: int | None = None,
    request: Any = None,
    response: Any = None
)

Bases: ApiError

The configured SortStrategy cannot encode this Sort term list.

UnsupportedPaginationError

UnsupportedPaginationError(
    message: str,
    *,
    status_code: int | None = None,
    request: Any = None,
    response: Any = None
)

Bases: ApiError

The configured PaginationStrategy cannot satisfy this operation.

Raised when a collection method asks the strategy for something the underlying wire protocol does not allow — e.g. count() against a cursor strategy with no count source, or get_page(n) against any strategy that walks pages sequentially (cursor, link header). The capability is fundamentally absent from the protocol, not merely unimplemented; callers should switch strategy or iterate sequentially instead.

UnsupportedFilterKeyError

UnsupportedFilterKeyError(
    message: str,
    *,
    status_code: int | None = None,
    request: Any = None,
    response: Any = None
)

Bases: UnsupportedFilterError

A filter key is not declared on the operation.

UnsupportedSortFieldError

UnsupportedSortFieldError(
    message: str,
    *,
    status_code: int | None = None,
    request: Any = None,
    response: Any = None
)

Bases: UnsupportedSortError

A sort field is not declared on the operation.