Source code for action0.openapi.generate

"""
Assembling and writing one generated client package.

:py:func:`generate_package` renders the whole package —
``__init__.py``, ``models.py``, ``operations.py``, ``client.py``,
``errors.py`` (when the schema documents error responses) and the
``py.typed`` marker — as a mapping of file names to file contents;
:py:func:`write_package` puts it on disk. The default package and
client class names are derived from the schema's ``info.title`` by
:py:func:`default_package_name` and :py:func:`default_client_name`.
"""

from __future__ import annotations

from pathlib import Path

from .ir import Api
from .ir import OperationIR
from .names import class_name
from .names import field_name
from .render import render_client
from .render import render_errors
from .render import render_init
from .render import render_models
from .render import render_operations


[docs] def default_package_name(title: str) -> str: """ The default generated-package name for a schema title. >>> default_package_name("Petstore") 'petstore_client' :param title: the schema's ``info.title`` :return: the package name """ return f"{field_name(title)}_client"
[docs] def default_client_name(title: str) -> str: """ The default client class name for a schema title. >>> default_client_name("Petstore") 'PetstoreClient' :param title: the schema's ``info.title`` :return: the class name """ return class_name(f"{title} client")
[docs] def generate_package( api: Api, *, client_name: str, schema_name: str, split_by_tag: bool = False ) -> dict[str, str]: """ Render one generated client package. :param api: the intermediate representation :param client_name: the client class name :param schema_name: the schema file's name, quoted in the header of every generated file :param split_by_tag: put each OpenAPI tag's operations into a module of its own (``operations_<tag>.py``; untagged operations stay in ``operations.py``) instead of one shared ``operations.py`` :return: file name to file content """ # imported lazily: the package root imports this module, so the # package-level __version__ does not exist yet at import time from action0.openapi import __version__ header = f"generated by action0-client-openapi v{__version__} from {schema_name} — do not edit" groups = _operation_groups(api, split_by_tag) operation_modules = { operation.class_name: module for module, operations in groups for operation in operations } files = { "__init__.py": render_init(api, header, client_name, operation_modules), "models.py": render_models(api, header), "client.py": render_client(api, header, client_name), "py.typed": "", } if any(operation.errors for operation in api.operations): files["errors.py"] = render_errors(api, header) for module, operations in groups: files[f"{module}.py"] = render_operations(api, header, operations) return files
def _operation_groups(api: Api, split_by_tag: bool) -> list[tuple[str, tuple[OperationIR, ...]]]: """ Group the operations into their modules. :param api: the intermediate representation :param split_by_tag: whether to group by the operations' first tag :return: ``(module name, operations)`` pairs, in first-appearance order; tags converging on one Python module name share it """ if not split_by_tag: return [("operations", api.operations)] groups: dict[str, list[OperationIR]] = {} for operation in api.operations: module = f"operations_{field_name(operation.tag)}" if operation.tag else "operations" groups.setdefault(module, []).append(operation) return [(module, tuple(operations)) for module, operations in groups.items()]
[docs] def write_package(files: dict[str, str], package_dir: Path, *, force: bool = False) -> list[Path]: """ Write a generated package to disk. :param files: file name to file content, as returned by :py:func:`generate_package` :param package_dir: the package directory (created if missing) :param force: overwrite existing files instead of refusing :return: the written paths, in file-name order :raises FileExistsError: if a file exists and ``force`` is not set (nothing is written then) """ package_dir.mkdir(parents=True, exist_ok=True) if not force: existing = [name for name in sorted(files) if (package_dir / name).exists()] if existing: raise FileExistsError(f"{package_dir / existing[0]} exists — pass force to overwrite") written = [] for name in sorted(files): path = package_dir / name path.write_text(files[name], encoding="utf-8") written.append(path) return written