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