Generator API reference¶
The generator package exposes two supported entry points:
generate(manifest)— the manifest-driven public API that powersokapipy generate.generate_for_mount(api, raw_spec, ...)— the per-mount building block used bygenerateand 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 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
¶
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 |
Returns:
| Type | Description |
|---|---|
GenerationManifest
|
A populated, frozen |
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
¶
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
|
required |
Returns:
| Type | Description |
|---|---|
dict[str, GeneratedFile]
|
A |
Raises:
| Type | Description |
|---|---|
GenerationError
|
when a |
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 |
required |
raw_spec
|
dict[str, Any] | str | Path
|
original OpenAPI document (path, URL string, or already-loaded dict).
Forwarded to |
required |
output_dir
|
Path | None
|
accepted for symmetry with |
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
|
required |
project_name
|
str | None
|
PEP 503 distribution name. Defaults to the last segment of
|
None
|
project_description
|
str | None
|
free-form PEP 621 |
None
|
project_version
|
str
|
initial version string emitted into |
'0.1.0'
|
python_version
|
str
|
pinned Python version for the generated project. |
'3.13'
|
license
|
str
|
SPDX identifier; drives the |
'Proprietary'
|
author
|
str | None
|
copyright holder for the generated |
None
|
repo_url
|
str | None
|
optional source-repository URL. When set, drives the
|
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 |
None
|
shape
|
Shape
|
response-shape policy for the generated client.
|
'auto'
|
Returns:
| Type | Description |
|---|---|
dict[str, GeneratedFile]
|
A |
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_diskalways overwrites. Everything underbase/pluspy.typedfalls in this bucket.one_shot=True: emitted exactly once on first generation.write_to_diskskips 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.jsonfrom disk; deletes any base file inprevious.base_filesthat 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=Truecomputes the report without touching disk;WriteReport.would_changeisTrueif anything would change. Powersokapipy generate --check.
GeneratedFile
dataclass
¶
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 |
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 |
required |
model_templates_dir
|
Path | None
|
directory of dmcg Jinja templates that override the
built-in ones; passed straight through to |
required |
python_version
|
str
|
target Python version, e.g. |
required |
public_names
¶
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_filesis the set of files the regenerated tree owns. On the next generation,previous.base_files - current.base_filesis the set of stale files (their parser-tree node no longer exists in the spec) thatwrite_to_diskdeletes from disk. User-layer files are never tracked here and never pruned. - Drift detection.
edgesis the set of parent → child wirings the current parser tree implies. On the next generation,current.edges - previous.edgesare 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
¶
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
¶
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
¶
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
¶
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
¶
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 |
required |
Returns:
| Type | Description |
|---|---|
tuple[str, ...]
|
Tuple of segments suitable for path joining and topology checks. |
mount_relpath
¶
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 |
required |
Returns:
| Type | Description |
|---|---|
str
|
|
str
|
|
check_mount_collisions
¶
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 |
iter_mount_namespace_prefixes
¶
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 |
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
¶
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
¶
Bases: GenerationError
Raised when Jinja2 fails to render a template (StrictUndefined, syntax, etc.).
FormatError
¶
Bases: GenerationError
Raised when ruff format rejects a rendered Python file.
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
¶
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
¶
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
¶
Equality-only conjunctive filter: ?status=open&customer_id=42.
KeyOpValueFilter
dataclass
¶
Django-style operator-suffix filter: ?created_at__gte=…&status__in=….
SearchFilterStrategy
dataclass
¶
Single free-text param filter: ?q=…. Accepts only one Search leaf.
JsonFilterStrategy
dataclass
¶
Encode the full Filter tree as a JSON expression in one query parameter.
CommaSignedSort
dataclass
¶
?sort=-created_at,id — comma-joined, leading - for desc.
KeyDirectionSort
dataclass
¶
?order_by=created_at&order=desc — separate field/direction params.
Only single-term sort is meaningful for this encoding; multi-term raises.
JsonApiSort
dataclass
¶
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
¶
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
¶
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
¶
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
¶
OrFilter
¶
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
¶
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
¶
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
¶
ClientError
¶
ServerError
¶
ResponseValidationError
¶
ConfigurationError
¶
UnsupportedFilterError
¶
UnsupportedSortError
¶
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.