"""
The intermediate representation (IR) between OpenAPI and generated code.
The translation stage turns a loaded OpenAPI document into one
:py:class:`Api` value — plain, frozen dataclasses that carry everything
the code emitter needs and nothing else: models with their fields,
enums, operations with parameters and body, and the security schemes.
All names in the IR are the final *Python* names (classes PascalCase,
fields snake_case, path templates rewritten to match the field names);
the original schema spellings survive as the ``wire_name``.
Keeping this layer independent of both the OpenAPI document shape and
the emitted source text is deliberate: a future dynamic mode (building
operation classes at import time instead of writing files) would
consume the very same :py:class:`Api`.
"""
from __future__ import annotations
import enum
from dataclasses import dataclass
from dataclasses import field
from typing import TypeAlias
[docs]
class Scalar(enum.Enum):
"""The scalar types generated code distinguishes."""
STR = "str"
INT = "int"
FLOAT = "float"
BOOL = "bool"
DATE = "date"
DATETIME = "datetime"
UUID = "uuid"
BYTES = "bytes"
#: free-form values that stay whatever the JSON decoder produced
ANY = "any"
[docs]
@dataclass(frozen=True)
class ScalarType:
"""A scalar type."""
kind: Scalar
[docs]
@dataclass(frozen=True)
class ArrayType:
"""A JSON array — ``list[item]`` in generated code."""
item: TypeExpr
[docs]
@dataclass(frozen=True)
class MapType:
"""
A JSON object with ``additionalProperties`` only — ``dict[str, value]``
in generated code.
"""
value: TypeExpr
[docs]
@dataclass(frozen=True)
class ModelType:
"""A reference to a generated model dataclass, by Python class name."""
name: str
[docs]
@dataclass(frozen=True)
class EnumType:
"""A reference to a generated enum class, by Python class name."""
name: str
[docs]
@dataclass(frozen=True)
class UnionType:
"""
A reference to a generated union alias, by Python name.
The members ride along so the type logic (annotations, whether a
conversion is needed at all) works without looking the union up.
:param name: the alias name
:param members: the union's member types, in schema order
"""
name: str
members: tuple[TypeExpr, ...]
#: any type generated code can express
TypeExpr: TypeAlias = "ScalarType | ArrayType | MapType | ModelType | EnumType | UnionType"
[docs]
@dataclass(frozen=True)
class Field:
"""
One property of a model, or one field of a request body.
:param name: the Python field name
:param wire_name: the property name in the JSON/form payload
:param type: the field's type
:param required: whether the payload must contain the property
:param nullable: whether ``null`` is a legal payload value
:param default: the schema's default value (scalars only), or
``None`` when the schema declares none
:param description: the schema's description, for docstrings
"""
name: str
wire_name: str
type: TypeExpr
required: bool
nullable: bool = False
default: object | None = None
description: str | None = None
[docs]
@dataclass(frozen=True)
class Model:
"""
One generated dataclass model.
:param name: the Python class name
:param fields: the model's fields; the ones rendered without a
dataclass default (required and not nullable) come first
:param additional_field: the catch-all field collecting the payload
keys not declared under ``properties`` (schemas combining
``properties`` with ``additionalProperties``), or ``None`` when
the schema declares no additional properties; its type is
always a :py:class:`MapType`, and its ``wire_name`` is empty —
the catch-all has no single wire spelling
:param description: the schema's description, for the docstring
"""
name: str
fields: tuple[Field, ...]
additional_field: Field | None = None
description: str | None = None
[docs]
@dataclass(frozen=True)
class EnumModel:
"""
One generated :py:class:`enum.Enum` class.
:param name: the Python class name
:param base: the scalar kind of the values (:py:attr:`Scalar.STR`
or :py:attr:`Scalar.INT`)
:param members: ``(member_name, value)`` pairs
:param description: the schema's description, for the docstring
"""
name: str
base: Scalar
members: tuple[tuple[str, str | int], ...]
description: str | None = None
[docs]
class UnionCheck(enum.Enum):
"""How one union member is recognized in a decoded payload."""
#: an ``isinstance`` check against a JSON-level Python type
JSON_TYPE = "json-type"
#: the discriminator property equals a tag value
TAG = "tag"
#: a required key only this member has is present
KEY = "key"
[docs]
@dataclass(frozen=True)
class UnionCase:
"""
One branch of a union's dispatching converter.
:param member: the member built when the check matches
:param check: how the member is recognized
:param value: the check's argument — the Python type name (e.g.
``str``, ``(int, float)``) for :py:attr:`UnionCheck.JSON_TYPE`,
the tag value for :py:attr:`UnionCheck.TAG`, the property name
for :py:attr:`UnionCheck.KEY`
"""
member: TypeExpr
check: UnionCheck
value: str
[docs]
@dataclass(frozen=True)
class UnionModel:
"""
One generated union: a type alias plus a dispatching converter.
:param name: the Python alias name
:param members: the member types, in schema order
:param cases: the dispatch branches, in the order they are emitted
:param discriminator: the wire property carrying the tag (for
:py:attr:`UnionCheck.TAG` cases)
:param description: the schema's description, for the docstring
"""
name: str
members: tuple[TypeExpr, ...]
cases: tuple[UnionCase, ...]
discriminator: str | None = None
description: str | None = None
[docs]
class ParamLocation(enum.Enum):
"""Where an operation parameter is placed."""
PATH = "path"
QUERY = "query"
HEADER = "header"
[docs]
@dataclass(frozen=True)
class Param:
"""
One path, query or header parameter of an operation.
:param name: the Python field name
:param wire_name: the parameter name on the wire
:param location: where the parameter goes
:param type: the parameter's type
:param required: whether the parameter must be sent
:param nullable: whether the schema allows ``null``
:param default: the schema's default value (scalars only), or
``None`` when the schema declares none
:param join_with: the separator joining an array parameter's items
into one ``key=value`` pair (a non-exploded ``style``), or
``None`` for the default one-pair-per-item serialization
:param description: the parameter's description, for docstrings
"""
name: str
wire_name: str
location: ParamLocation
type: TypeExpr
required: bool
nullable: bool = False
default: object | None = None
join_with: str | None = None
description: str | None = None
[docs]
class BodyKind(enum.Enum):
"""How an operation's request body is expressed as fields."""
#: an inline JSON object schema, one ``json_field()`` per property
JSON_FIELDS = "json-fields"
#: a referenced/array/scalar JSON schema, one ``json_body()`` field
JSON_BODY = "json-body"
#: ``application/x-www-form-urlencoded``, one ``form_field()`` per property
FORM_FIELDS = "form-fields"
#: any other media type, one raw-bytes ``body()`` field
RAW_BODY = "raw-body"
[docs]
@dataclass(frozen=True)
class Body:
"""
An operation's request body.
:param kind: how the body maps to operation fields
:param fields: the properties (for :py:attr:`BodyKind.JSON_FIELDS`
and :py:attr:`BodyKind.FORM_FIELDS`), or the single payload
field (:py:attr:`BodyKind.JSON_BODY` and
:py:attr:`BodyKind.RAW_BODY`)
:param type: the whole-body type (for :py:attr:`BodyKind.JSON_BODY`;
``None`` otherwise)
:param required: whether the request must carry the body
:param media_type: the media type sent as ``Content-Type`` (for
:py:attr:`BodyKind.RAW_BODY`; ``None`` otherwise)
"""
kind: BodyKind
fields: tuple[Field, ...] = ()
type: TypeExpr | None = None
required: bool = True
media_type: str | None = None
[docs]
@dataclass(frozen=True)
class ErrorCase:
"""
One documented non-2xx response, raised as a typed exception.
:param status: the response key — a concrete status (``"400"``), a
range (``"4XX"``/``"5XX"``) or ``"default"``
:param exception: the generated exception class name; operations
documenting the same status with the same model share it
:param model: the class name of the model the error payload parses
into
:param description: the response's description, for the ``check``
docstring
"""
status: str
exception: str
model: str
description: str | None = None
[docs]
class ResponseKind(enum.Enum):
"""What an operation's success response parses into."""
#: a JSON payload loaded into a typed value
MODEL = "model"
#: no content (e.g. 204) — the operation returns ``None``
NONE = "none"
#: non-JSON content returned as raw ``bytes``
BYTES = "bytes"
[docs]
@dataclass(frozen=True)
class OperationIR:
"""
One generated operation class.
:param class_name: the Python class name
:param method: the HTTP method, uppercase
:param path_template: the path with ``{placeholder}`` names already
rewritten to the Python parameter names
:param wire_path: the original path as spelled in the schema
:param params: the path/query/header parameters
:param body: the request body, if any
:param response_kind: what the success response parses into
:param response_type: the parsed type (for
:py:attr:`ResponseKind.MODEL`; ``None`` otherwise)
:param errors: the documented non-2xx responses raised as typed
exceptions (concrete statuses first, then ranges, then
``default`` — the order the generated ``check`` tests them in)
:param summary: the schema's summary, for the docstring
:param description: the schema's description, for the docstring
:param tag: the operation's first ``tags`` entry, if any — the
grouping key when the generated package splits operations into
per-tag modules
"""
class_name: str
method: str
path_template: str
wire_path: str
params: tuple[Param, ...] = ()
body: Body | None = None
response_kind: ResponseKind = ResponseKind.NONE
response_type: TypeExpr | None = None
errors: tuple[ErrorCase, ...] = ()
summary: str | None = None
description: str | None = None
tag: str | None = None
[docs]
class SecurityKind(enum.Enum):
"""The supported OpenAPI security scheme kinds."""
HTTP_BEARER = "http-bearer"
HTTP_BASIC = "http-basic"
API_KEY_HEADER = "api-key-header"
API_KEY_QUERY = "api-key-query"
[docs]
@dataclass(frozen=True)
class SecurityScheme:
"""
One security scheme, turned into client credentials.
:param kind: the scheme kind
:param param_name: the Python name of the credential parameter on
the generated client's ``__init__`` (e.g. ``token``, ``api_key``)
:param wire_name: the header or query parameter carrying the
credential (``None`` for HTTP bearer/basic, which fix the
``Authorization`` header)
"""
kind: SecurityKind
param_name: str
wire_name: str | None = None
[docs]
@dataclass(frozen=True)
class Api:
"""
Everything the emitter needs to generate one client package.
:param title: the schema's ``info.title``
:param version: the schema's ``info.version``
:param base_url: the default base URL from ``servers``, if any
:param models: the models and enums, in schema order
:param operations: the operations, in path order
:param security: the security schemes becoming client credentials
:param warnings: notes about constructs the translation flattened
or skipped (printed by the CLI, documented in the generated
code where possible)
"""
title: str
version: str
base_url: str | None = None
models: tuple[Model | EnumModel | UnionModel, ...] = field(default=())
operations: tuple[OperationIR, ...] = field(default=())
security: tuple[SecurityScheme, ...] = field(default=())
warnings: tuple[str, ...] = field(default=())