diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..73f69f7 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,23 @@ +{ + "name": "openapi-pydantic", + "image": "mcr.microsoft.com/devcontainers/python:0-3.11", + "features": { + "ghcr.io/devcontainers-contrib/features/poetry:2": { + "version": "latest" + } + }, + "containerEnv": { + "POETRY_VIRTUALENVS_IN_PROJECT": "true" + }, + "postCreateCommand": "poetry install && pip install --upgrade tox", + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.black-formatter", + "charliermarsh.ruff" + ] + } + } +} diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..6cd3771 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,24 @@ +changelog: + exclude: + labels: + - ignore-for-release + categories: + - title: Breaking 💥 + labels: + - breaking + - title: Added 🎉 + labels: + - feature + - title: Changed 🛠 + labels: + - change + - title: Fixed 🐛 + labels: + - fix + - bug + - title: Dependencies 📦 + labels: + - dependencies + - title: Docs 📝 + labels: + - documentation diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..69cfe2e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,69 @@ +name: Publish + +on: + workflow_dispatch: + release: + types: + - published + +jobs: + build: + name: Build package + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: false + version: 1.8.3 + - name: Build package distribution + run: poetry build + - name: Upload package artifact + uses: actions/upload-artifact@v4 + with: + name: distribution + path: dist + test-release: + name: Publish release to Test PyPI + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + needs: build + environment: + name: test + url: https://test.pypi.org/p/openapi-pydantic + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + steps: + - name: Download package distribution + uses: actions/download-artifact@v4 + with: + name: distribution + path: dist + - name: Publish package distribution to Test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + release: + name: Publish release to PyPI + if: github.event_name == 'release' + runs-on: ubuntu-latest + needs: build + environment: + name: production + url: https://pypi.org/p/openapi-pydantic + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + steps: + - name: Download package distribution + uses: actions/download-artifact@v4 + with: + name: distribution + path: dist + - name: Publish package distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..24382aa --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + +jobs: + tox: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox tox-gh-actions + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: 1.8.3 + virtualenvs-create: true + virtualenvs-in-project: true + - name: Run tox test suite + run: tox diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a98b534 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__ +/.idea +/.mypy_cache +/.pytest_cache +/.ruff_cache +/.tox +/.venv +/*.egg-info +/build +/dist diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1b9ca3f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "python.testing.pytestArgs": [ + "tests", + "-vv" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python.analysis.typeCheckingMode": "basic", + "python.linting.enabled": true, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "ms-python.black-formatter" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4bca1da --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,42 @@ +# OpenAPI Pydantic Contribution Guide + +We welcome all contributions! + +## Issues + +Questions, feature requests and bug reports are all welcome as issues. When raising a bug or +question, please include as much information as possible including the specific version you +are using. + +## Pull Requests + +It should be very simple to get started and open a pull request, however for anything non-trivial +please open an issue to discuss your intended change _before_ creating your PR. This avoids wasting +time by ensuring that your changes will be accepted with fewer revisions down the line! + +### Local Development + +A [devcontainer](https://code.visualstudio.com/docs/devcontainers/containers) configuration is provided in the repo to get your environment setup automatically. Alternatively you can install [tox](https://tox.wiki/en/latest/) and [poetry](https://python-poetry.org/) manually. + +### Testing + +Please ensure all changes have good test coverage and are formatted correctly. You can run the test +suite and linters using [tox](https://tox.wiki/en/latest/) - just run `tox` from the root of this +repo to run the checks. These will also be run automatically in CI once your PR is opened. Don't +worry about testing against every Ptyhon version - the CI action will do this for you! + +### Tagging + +When your PR is ready, please tag it with the appropriate tags: one of `feature`, `change`, `fix`, +as well as `breaking` if you've introduced backwards-incompatible changes to the public API or +behaviour. + +## Review + +We'll review your PR as soon as possible - either approving or requesting changes. Once the PR is +approved, it will be merged into main and cut into the next release. + +## Releases + +The release schedule is not set in stone and will depend on the number of changes in flight, but where possible we'll look to cut a release with your changes as soon as possible. Once a new version is tagged, +a package version is uploaded to PyPI automatically. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..04d2e21 --- /dev/null +++ b/LICENSE @@ -0,0 +1,40 @@ +MIT License + +Copyright (c) 2023 mike-oakley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +---- + +Forked from the original implementation by Kuimono, under MIT licence, from the repository at: +https://github.com/kuimono/openapi-schema-pydantic + +Copyright (c) 2020 Kuimono + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +License URL: https://github.com/kuimono/openapi-schema-pydantic/blob/master/LICENSE diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca2bc17 --- /dev/null +++ b/README.md @@ -0,0 +1,322 @@ +# openapi-pydantic + +[![PyPI](https://img.shields.io/pypi/v/openapi-pydantic)](https://pypi.org/project/openapi-pydantic/) +[![PyPI - License](https://img.shields.io/pypi/l/openapi-pydantic)](https://github.com/mike-oakley/openapi-pydantic/blob/main/LICENSE) + +OpenAPI schema implemented in [Pydantic](https://github.com/samuelcolvin/pydantic). Both Pydantic 1.8+ and 2.x are supported. + +The naming of the classes follows the schema in +[OpenAPI specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#schema). + +> This library is forked from [OpenAPI Schema Pydantic](https://github.com/kuimono/openapi-schema-pydantic) (at version [1.2.4](https://github.com/kuimono/openapi-schema-pydantic/releases/tag/v1.2.4)) which is no longer actively maintained. + +## Installation + +`pip install openapi-pydantic` + +## Try me + +```python +from openapi_pydantic import OpenAPI, Info, PathItem, Operation, Response + +# Construct OpenAPI by pydantic objects +open_api = OpenAPI( + info=Info( + title="My own API", + version="v0.0.1", + ), + paths={ + "/ping": PathItem( + get=Operation( + responses={ + "200": Response( + description="pong" + ) + } + ) + ) + }, +) +# Note: for Pydantic 1.x, replace `model_dump_json` with `json` +print(open_api.model_dump_json(by_alias=True, exclude_none=True, indent=2)) +``` + +Result: + +```json +{ + "openapi": "3.1.1", + "info": { + "title": "My own API", + "version": "v0.0.1" + }, + "servers": [ + { + "url": "/" + } + ], + "paths": { + "/ping": { + "get": { + "responses": { + "200": { + "description": "pong" + } + }, + "deprecated": false + } + } + } +} +``` + +## Take advantage of Pydantic + +Pydantic is a great tool. It allows you to use object / dict / mixed data for input. + +The following examples give the same OpenAPI result as above: + +```python +from openapi_pydantic import parse_obj, OpenAPI, PathItem, Response + +# Construct OpenAPI from dict, inferring the correct schema version +open_api = parse_obj({ + "openapi": "3.1.1", + "info": {"title": "My own API", "version": "v0.0.1"}, + "paths": { + "/ping": { + "get": {"responses": {"200": {"description": "pong"}}} + } + }, +}) + + +# Construct OpenAPI v3.1 schema from dict +# Note: for Pydantic 1.x, replace `model_validate` with `parse_obj` +open_api = OpenAPI.model_validate({ + "info": {"title": "My own API", "version": "v0.0.1"}, + "paths": { + "/ping": { + "get": {"responses": {"200": {"description": "pong"}}} + } + }, +}) + +# Construct OpenAPI with mix of dict/object +# Note: for Pydantic 1.x, replace `model_validate` with `parse_obj` +open_api = OpenAPI.model_validate({ + "info": {"title": "My own API", "version": "v0.0.1"}, + "paths": { + "/ping": PathItem( + get={"responses": {"200": Response(description="pong")}} + ) + }, +}) +``` + +## Use Pydantic classes as schema + +- The [Schema Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.4.md#schemaObject) + in OpenAPI has definitions and tweaks in JSON Schema, which are hard to comprehend and define a good data class +- Pydantic already has a good way to [create JSON schema](https://pydantic-docs.helpmanual.io/usage/schema/). + Let's not reinvent the wheel. + +The approach to deal with this: + +1. Use `PydanticSchema` objects to represent the `Schema` in `OpenAPI` object +2. Invoke `construct_open_api_with_schema_class` to resolve the JSON schemas and references + +```python +from pydantic import BaseModel, Field + +from openapi_pydantic import OpenAPI +from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class + +def construct_base_open_api() -> OpenAPI: + # Note: for Pydantic 1.x, replace `model_validate` with `parse_obj` + return OpenAPI.model_validate({ + "info": {"title": "My own API", "version": "v0.0.1"}, + "paths": { + "/ping": { + "post": { + "requestBody": {"content": {"application/json": { + "schema": PydanticSchema(schema_class=PingRequest) + }}}, + "responses": {"200": { + "description": "pong", + "content": {"application/json": { + "schema": PydanticSchema(schema_class=PingResponse) + }}, + }}, + } + } + }, + }) + +class PingRequest(BaseModel): + """Ping Request""" + req_foo: str = Field(description="foo value of the request") + req_bar: str = Field(description="bar value of the request") + +class PingResponse(BaseModel): + """Ping response""" + resp_foo: str = Field(description="foo value of the response") + resp_bar: str = Field(description="bar value of the response") + +open_api = construct_base_open_api() +open_api = construct_open_api_with_schema_class(open_api) + +# print the result openapi.json +# Note: for Pydantic 1.x, replace `model_dump_json` with `json` +print(open_api.model_dump_json(by_alias=True, exclude_none=True, indent=2)) +``` + +Result: + +```json +{ + "openapi": "3.1.1", + "info": { + "title": "My own API", + "version": "v0.0.1" + }, + "servers": [ + { + "url": "/" + } + ], + "paths": { + "/ping": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PingRequest" + } + } + }, + "required": false + }, + "responses": { + "200": { + "description": "pong", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PingResponse" + } + } + } + } + }, + "deprecated": false + } + } + }, + "components": { + "schemas": { + "PingRequest": { + "title": "PingRequest", + "required": [ + "req_foo", + "req_bar" + ], + "type": "object", + "properties": { + "req_foo": { + "title": "Req Foo", + "type": "string", + "description": "foo value of the request" + }, + "req_bar": { + "title": "Req Bar", + "type": "string", + "description": "bar value of the request" + } + }, + "description": "Ping Request" + }, + "PingResponse": { + "title": "PingResponse", + "required": [ + "resp_foo", + "resp_bar" + ], + "type": "object", + "properties": { + "resp_foo": { + "title": "Resp Foo", + "type": "string", + "description": "foo value of the response" + }, + "resp_bar": { + "title": "Resp Bar", + "type": "string", + "description": "bar value of the response" + } + }, + "description": "Ping response" + } + } + } +} +``` + +## Notes + +### Use of OpenAPI.model_dump() / OpenAPI.model_dump_json() / OpenAPI.json() / OpenAPI.dict() + +When using `OpenAPI.model_dump()` / `OpenAPI.model_dump_json()` / `OpenAPI.json()` / `OpenAPI.dict()` functions, +the arguments `by_alias=True, exclude_none=True` have to be in place. +Otherwise the resulting json will not fit the OpenAPI standard. + +```python +# OK (Pydantic 2) +open_api.model_dump_json(by_alias=True, exclude_none=True, indent=2) +# OK (Pydantic 1) +open_api.json(by_alias=True, exclude_none=True, indent=2) + +# Not good +open_api.model_dump_json(indent=2) +open_api.json(indent=2) +``` + +More info about field aliases: + +| OpenAPI version | Field alias info | +| --------------- | ---------------- | +| 3.1 | [here](https://github.com/mike-oakley/openapi-pydantic/blob/main/openapi_pydantic/v3/v3_1/README.md#alias) | +| 3.0 | [here](https://github.com/mike-oakley/openapi-pydantic/blob/main/openapi_pydantic/v3/v3_0/README.md#alias) | + +### Non-pydantic schema types + +Some schema types are not implemented as pydantic classes. +Please refer to the following for more info: + +| OpenAPI version | Non-pydantic schema type info | +| --------------- | ----------------------------- | +| 3.1 | [here](https://github.com/mike-oakley/openapi-pydantic/blob/main/openapi_pydantic/v3/v3_1/README.md#non-pydantic-schema-types) | +| 3.0 | [here](https://github.com/mike-oakley/openapi-pydantic/blob/main/openapi_pydantic/v3/v3_0/README.md#non-pydantic-schema-types) | + +### Use OpenAPI 3.0 instead of 3.1 + +Some UI renderings (e.g. Swagger) still do not support OpenAPI 3.1.x. +The old 3.0.x version is available by importing from different paths: + +```python +from openapi_pydantic.v3.v3_0 import OpenAPI, ... +from openapi_pydantic.v3.v3_0.util import PydanticSchema, construct_open_api_with_schema_class +``` + +### Pydantic version compatibility + +Compatibility with both major versions of Pydantic (1.8+ and 2.*) is mostly achieved using a module called `compat.py`. It detects the installed version of Pydantic and exports version-specific symbols for use by the rest of the package. It also provides all symbols necessary for type checking. The `compat.py` module is not intended to be imported by other packages, but other packages may find it helpful as an example of how to span major versions of Pydantic. + +## Credits + +This library is based from the original implementation by Kuimono of [OpenAPI Schema Pydantic](https://github.com/kuimono/openapi-schema-pydantic) which is no longer actively maintained. + +## License + +[MIT License](https://github.com/mike-oakley/openapi-pydantic/blob/main/LICENSE) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1103077 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +As we're a small project and have limited maintainer capacity, only the latest major version of OpenAPI Pydantic is actively maintained. Whilst +we are pre-v1, this extends to the latest minor version - so only the latest 0.x version is actively maintained. Please update to the latest available +version before reporting bugs or security vulnerabilities. + +## Reporting a Vulnerability + +To report a vulnerability associated with this project, use the [GitHub vulnerability reporting tool](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability). +Head over to the [Security](https://github.com/mike-oakley/openapi-pydantic/security) tab +and click **Report a vulnerability** to open the advisory form. diff --git a/openapi_pydantic/__init__.py b/openapi_pydantic/__init__.py new file mode 100644 index 0000000..23339dc --- /dev/null +++ b/openapi_pydantic/__init__.py @@ -0,0 +1,38 @@ +import logging + +from .v3 import XML as XML +from .v3 import Callback as Callback +from .v3 import Components as Components +from .v3 import Contact as Contact +from .v3 import DataType as DataType +from .v3 import Discriminator as Discriminator +from .v3 import Encoding as Encoding +from .v3 import Example as Example +from .v3 import ExternalDocumentation as ExternalDocumentation +from .v3 import Header as Header +from .v3 import Info as Info +from .v3 import License as License +from .v3 import Link as Link +from .v3 import MediaType as MediaType +from .v3 import OAuthFlow as OAuthFlow +from .v3 import OAuthFlows as OAuthFlows +from .v3 import OpenAPI as OpenAPI +from .v3 import Operation as Operation +from .v3 import Parameter as Parameter +from .v3 import ParameterLocation as ParameterLocation +from .v3 import PathItem as PathItem +from .v3 import Paths as Paths +from .v3 import Reference as Reference +from .v3 import RequestBody as RequestBody +from .v3 import Response as Response +from .v3 import Responses as Responses +from .v3 import Schema as Schema +from .v3 import SecurityRequirement as SecurityRequirement +from .v3 import SecurityScheme as SecurityScheme +from .v3 import Server as Server +from .v3 import ServerVariable as ServerVariable +from .v3 import Tag as Tag +from .v3 import parse_obj as parse_obj +from .v3 import schema_validate as schema_validate + +logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/openapi_pydantic/compat.py b/openapi_pydantic/compat.py new file mode 100644 index 0000000..840089b --- /dev/null +++ b/openapi_pydantic/compat.py @@ -0,0 +1,119 @@ +"""Compatibility layer to make this package usable with Pydantic 1 or 2""" + +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple + +from pydantic.version import VERSION as PYDANTIC_VERSION + +__all__ = [ + "PYDANTIC_V2", + "ConfigDict", + "JsonSchemaMode", + "models_json_schema", + "RootModel", + "Extra", + "v1_schema", + "DEFS_KEY", + "min_length_arg", +] + +PYDANTIC_MAJOR_VERSION = int(PYDANTIC_VERSION.split(".", 1)[0]) +PYDANTIC_MINOR_VERSION = int(PYDANTIC_VERSION.split(".")[1]) +PYDANTIC_V2 = PYDANTIC_MAJOR_VERSION >= 2 + +if TYPE_CHECKING: + # Provide stubs for either version of Pydantic + + from enum import Enum + from typing import Any, Literal, Type, TypedDict + + from pydantic import BaseModel + from pydantic import ConfigDict as PydanticConfigDict + + def ConfigDict( + extra: Literal["allow", "ignore", "forbid"] = "allow", + json_schema_extra: Optional[Dict[str, Any]] = None, + populate_by_name: bool = True, + ) -> PydanticConfigDict: + """Stub for pydantic.ConfigDict in Pydantic 2""" + ... + + class Extra(Enum): + """Stub for pydantic.Extra in Pydantic 1""" + + allow = "allow" + ignore = "ignore" + forbid = "forbid" + + class RootModel(BaseModel): + """Stub for pydantic.RootModel in Pydantic 2""" + + JsonSchemaMode = Literal["validation", "serialization"] + + def models_json_schema( + models: List[Tuple[Type[BaseModel], JsonSchemaMode]], + *, + by_alias: bool = True, + ref_template: str = "#/$defs/{model}", + schema_generator: Optional[type] = None, + ) -> Tuple[Dict, Dict[str, Any]]: + """Stub for pydantic.json_schema.models_json_schema in Pydantic 2""" + ... + + def v1_schema( + models: List[Type[BaseModel]], + *, + by_alias: bool = True, + ref_prefix: str = "#/$defs", + ) -> Dict[str, Any]: + """Stub for pydantic.schema.schema in Pydantic 1""" + ... + + DEFS_KEY = "$defs" + + class MinLengthArg(TypedDict): + pass + + def min_length_arg(min_length: int) -> MinLengthArg: + """Generate a min_length or min_items parameter for Field(...)""" + ... + +elif PYDANTIC_V2: + from typing import TypedDict + + from pydantic import ConfigDict, RootModel + from pydantic.json_schema import JsonSchemaMode, models_json_schema + + # Pydantic 2 renders JSON schemas using the keyword "$defs" + DEFS_KEY = "$defs" + + class MinLengthArg(TypedDict): + min_length: int + + def min_length_arg(min_length: int) -> MinLengthArg: + return {"min_length": min_length} + + # Create V1 stubs. These should not be used when PYDANTIC_V2 is true. + Extra = None + v1_schema = None + + +else: + from typing import TypedDict + + from pydantic import Extra + from pydantic.schema import schema as v1_schema + + # Pydantic 1 renders JSON schemas using the keyword "definitions" + DEFS_KEY = "definitions" + + class MinLengthArg(TypedDict): + min_items: int + + def min_length_arg(min_length: int) -> MinLengthArg: + return {"min_items": min_length} + + # Create V2 stubs. These should not be used when PYDANTIC_V2 is false. + ConfigDict = None + models_json_schema = None + JsonSchemaMode = None + RootModel = None diff --git a/openapi_pydantic/py.typed b/openapi_pydantic/py.typed new file mode 100644 index 0000000..7632ecf --- /dev/null +++ b/openapi_pydantic/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561 diff --git a/openapi_pydantic/util.py b/openapi_pydantic/util.py new file mode 100644 index 0000000..62aeb89 --- /dev/null +++ b/openapi_pydantic/util.py @@ -0,0 +1,188 @@ +import logging +import re +from typing import Any, Dict, Generic, List, Optional, Set, Type, TypeVar, cast + +from pydantic import BaseModel + +from openapi_pydantic.compat import ( + DEFS_KEY, + PYDANTIC_V2, + JsonSchemaMode, + models_json_schema, + v1_schema, +) + +from . import Components, OpenAPI, Reference, Schema, schema_validate + +logger = logging.getLogger(__name__) + +PydanticType = TypeVar("PydanticType", bound=BaseModel) +ref_prefix = "#/components/schemas/" +ref_template = "#/components/schemas/{model}" + + +class PydanticSchema(Schema, Generic[PydanticType]): + """Special `Schema` class to indicate a reference from pydantic class""" + + schema_class: Type[PydanticType] + """the class that is used for generate the schema""" + + +def get_mode( + cls: Type[BaseModel], default: JsonSchemaMode = "validation" +) -> JsonSchemaMode: + """Get the JSON schema mode for a model class. + + The mode can be either "validation" or "serialization". In validation mode, + computed fields are dropped and optional fields remain optional. In + serialization mode, computed and optional fields are required. + """ + if not hasattr(cls, "model_config"): + return default + mode = cls.model_config.get("json_schema_mode", default) + if mode not in ("validation", "serialization"): + raise ValueError(f"invalid json_schema_mode: {mode}") + return cast(JsonSchemaMode, mode) + + +def construct_open_api_with_schema_class( + open_api: OpenAPI, + schema_classes: Optional[List[Type[BaseModel]]] = None, + scan_for_pydantic_schema_reference: bool = True, + by_alias: bool = True, +) -> OpenAPI: + """ + Construct a new OpenAPI object, utilising pydantic classes to produce JSON schemas. + + :param open_api: the base `OpenAPI` object + :param schema_classes: Pydantic classes that their schema will be used + "#/components/schemas" values + :param scan_for_pydantic_schema_reference: flag to indicate if scanning for + `PydanticSchemaReference` class + is needed for "#/components/schemas" + value updates + :param by_alias: construct schema by alias (default is True) + :return: new OpenAPI object with "#/components/schemas" values updated. + If there is no update in "#/components/schemas" values, the original + `open_api` will be returned. + """ + copy_func = getattr(open_api, "model_copy" if PYDANTIC_V2 else "copy") + new_open_api: OpenAPI = copy_func(deep=True) + + if scan_for_pydantic_schema_reference: + extracted_schema_classes = _handle_pydantic_schema(new_open_api) + if schema_classes: + schema_classes = list({*schema_classes, *extracted_schema_classes}) + else: + schema_classes = extracted_schema_classes + + if not schema_classes: + return open_api + + schema_classes.sort(key=lambda x: x.__name__) + logger.debug("schema_classes: %s", schema_classes) + + # update new_open_api with new #/components/schemas + if PYDANTIC_V2: + _key_map, schema_definitions = models_json_schema( + [(c, get_mode(c)) for c in schema_classes], + by_alias=by_alias, + ref_template=ref_template, + ) + else: + schema_definitions = v1_schema( + schema_classes, by_alias=by_alias, ref_prefix=ref_prefix + ) + + if not new_open_api.components: + new_open_api.components = Components() + if new_open_api.components.schemas: + for existing_key in new_open_api.components.schemas: + if existing_key in schema_definitions[DEFS_KEY]: + logger.warning( + f'"{existing_key}" already exists in {ref_prefix}. ' + f'The value of "{ref_prefix}{existing_key}" will be overwritten.' + ) + new_open_api.components.schemas.update(_validate_schemas(schema_definitions)) + else: + new_open_api.components.schemas = _validate_schemas(schema_definitions) + return new_open_api + + +def _validate_schemas(schema_definitions: Dict[str, Any]) -> Dict[str, Schema]: + """Convert JSON Schema definitions to parsed OpenAPI objects""" + # Note: if an error occurs in schema_validate(), it may indicate that + # the generated JSON schemas are not compatible with the version + # of OpenAPI this module depends on. + return { + key: schema_validate(schema_dict) + for key, schema_dict in schema_definitions[DEFS_KEY].items() + } + + +def _handle_pydantic_schema(open_api: OpenAPI) -> List[Type[BaseModel]]: + """ + This function traverses the `OpenAPI` object and + + 1. Replaces the `PydanticSchema` object with `Reference` object, with correct ref + value; + 2. Extracts the involved schema class from `PydanticSchema` object. + + **This function will mutate the input `OpenAPI` object.** + + :param open_api: the `OpenAPI` object to be traversed and mutated + :return: a list of schema classes extracted from `PydanticSchema` objects + """ + + pydantic_types: Set[Type[BaseModel]] = set() + + def _traverse(obj: Any) -> None: + if isinstance(obj, BaseModel): + fields = getattr( + obj, "model_fields_set" if PYDANTIC_V2 else "__fields_set__" + ) + for field in fields: + child_obj = obj.__getattribute__(field) + if isinstance(child_obj, PydanticSchema): + logger.debug("PydanticSchema found in %s: %s", obj, child_obj) + obj.__setattr__(field, _construct_ref_obj(child_obj)) + pydantic_types.add(child_obj.schema_class) + else: + _traverse(child_obj) + elif isinstance(obj, list): + for index, elem in enumerate(obj): + if isinstance(elem, PydanticSchema): + logger.debug(f"PydanticSchema found in list: {elem}") + obj[index] = _construct_ref_obj(elem) + pydantic_types.add(elem.schema_class) + else: + _traverse(elem) + elif isinstance(obj, dict): + for key, value in obj.items(): + if isinstance(value, PydanticSchema): + logger.debug(f"PydanticSchema found in dict: {value}") + obj[key] = _construct_ref_obj(value) + pydantic_types.add(value.schema_class) + else: + _traverse(value) + + _traverse(open_api) + return list(pydantic_types) + + +def _construct_ref_obj(pydantic_schema: PydanticSchema[PydanticType]) -> Reference: + """ + Construct a reference object from the Pydantic schema name + + characters in the schema name that are invalid/problematic + for JSONschema $ref names will get replaced with underscores. + Especially needed for Pydantic generic Models with brackets "[]" + + see: https://github.com/pydantic/pydantic/blob/aee6057378ccfec02126bf9c984a9b6d6b411777/pydantic/json_schema.py#L2031 + """ + ref_name = re.sub( + r"[^a-zA-Z0-9.\-_]", "_", pydantic_schema.schema_class.__name__ + ).replace(".", "__") + ref_obj = Reference(**{"$ref": ref_prefix + ref_name}) + logger.debug(f"ref_obj={ref_obj}") + return ref_obj diff --git a/openapi_pydantic/v3/__init__.py b/openapi_pydantic/v3/__init__.py new file mode 100644 index 0000000..ee575e8 --- /dev/null +++ b/openapi_pydantic/v3/__init__.py @@ -0,0 +1,34 @@ +from .parser import parse_obj as parse_obj +from .v3_1 import XML as XML +from .v3_1 import Callback as Callback +from .v3_1 import Components as Components +from .v3_1 import Contact as Contact +from .v3_1 import DataType as DataType +from .v3_1 import Discriminator as Discriminator +from .v3_1 import Encoding as Encoding +from .v3_1 import Example as Example +from .v3_1 import ExternalDocumentation as ExternalDocumentation +from .v3_1 import Header as Header +from .v3_1 import Info as Info +from .v3_1 import License as License +from .v3_1 import Link as Link +from .v3_1 import MediaType as MediaType +from .v3_1 import OAuthFlow as OAuthFlow +from .v3_1 import OAuthFlows as OAuthFlows +from .v3_1 import OpenAPI as OpenAPI +from .v3_1 import Operation as Operation +from .v3_1 import Parameter as Parameter +from .v3_1 import ParameterLocation as ParameterLocation +from .v3_1 import PathItem as PathItem +from .v3_1 import Paths as Paths +from .v3_1 import Reference as Reference +from .v3_1 import RequestBody as RequestBody +from .v3_1 import Response as Response +from .v3_1 import Responses as Responses +from .v3_1 import Schema as Schema +from .v3_1 import SecurityRequirement as SecurityRequirement +from .v3_1 import SecurityScheme as SecurityScheme +from .v3_1 import Server as Server +from .v3_1 import ServerVariable as ServerVariable +from .v3_1 import Tag as Tag +from .v3_1 import schema_validate as schema_validate diff --git a/openapi_pydantic/v3/parser.py b/openapi_pydantic/v3/parser.py new file mode 100644 index 0000000..0149bff --- /dev/null +++ b/openapi_pydantic/v3/parser.py @@ -0,0 +1,33 @@ +from typing import TYPE_CHECKING, Any, Union + +from pydantic import BaseModel, Field + +from openapi_pydantic.compat import PYDANTIC_V2 + +from .v3_0 import OpenAPI as OpenAPIv3_0 +from .v3_1 import OpenAPI as OpenAPIv3_1 + +OpenAPIv3 = Union[OpenAPIv3_1, OpenAPIv3_0] + +if TYPE_CHECKING: + + def parse_obj(data: Any) -> OpenAPIv3: + """Parse a raw object into an OpenAPI model with version inference.""" + ... + +elif PYDANTIC_V2: + from pydantic import RootModel + + class _OpenAPI(RootModel): + root: OpenAPIv3 = Field(discriminator="openapi") + + def parse_obj(data: Any) -> OpenAPIv3: + return _OpenAPI.model_validate(data).root + +else: + + class _OpenAPI(BaseModel): + __root__: OpenAPIv3 = Field(discriminator="openapi") + + def parse_obj(data: Any) -> OpenAPIv3: + return _OpenAPI.parse_obj(data).__root__ diff --git a/openapi_pydantic/v3/v3_0/README.md b/openapi_pydantic/v3/v3_0/README.md new file mode 100644 index 0000000..16b9c44 --- /dev/null +++ b/openapi_pydantic/v3/v3_0/README.md @@ -0,0 +1,39 @@ +# OpenAPI v3.0 schema classes + +## Alias + +Due to the reserved words in python and pydantic, +the following fields are used with [alias](https://pydantic-docs.helpmanual.io/usage/schema/#field-customisation) feature provided by pydantic: + +| Class | Field name in the class | Alias (as in OpenAPI spec) | +| ----- | ----------------------- | -------------------------- | +| Header[*](#header_param_in) | param_in | in | +| MediaType | media_type_schema | schema | +| Parameter | param_in | in | +| Parameter | param_schema | schema | +| PathItem | ref | $ref | +| Reference | ref | $ref | +| SecurityScheme | security_scheme_in | in | +| Schema | schema_format | format | +| Schema | schema_not | not | + +> The "in" field in Header object is actually a constant (`{"in": "header"}`). + +> For convenience of object creation, the classes mentioned in above +> have configured `allow_population_by_field_name=True` (Pydantic V1) or `populate_by_name=True` (Pydantic V2). +> +> Reference: [Pydantic's Model Config](https://pydantic-docs.helpmanual.io/usage/model_config/) + +## Non-pydantic schema types + +Due to the constriants of python typing structure (not able to handle dynamic field names), +the following schema classes are actually just a typing of `Dict`: + +| Schema Type | Implementation | +| ----------- | -------------- | +| Callback | `Callback = Dict[str, PathItem]` | +| Paths | `Paths = Dict[str, PathItem]` | +| Responses | `Responses = Dict[str, Union[Response, Reference]]` | +| SecurityRequirement | `SecurityRequirement = Dict[str, List[str]]` | + +On creating such schema instances, please use python's `dict` type instead to instantiate. diff --git a/openapi_pydantic/v3/v3_0/__init__.py b/openapi_pydantic/v3/v3_0/__init__.py new file mode 100644 index 0000000..af250d4 --- /dev/null +++ b/openapi_pydantic/v3/v3_0/__init__.py @@ -0,0 +1,59 @@ +""" +OpenAPI v3.0 schema types, created according to the specification: +https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.4.md + +The type orders are according to the contents of the specification: +https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.4.md#table-of-contents +""" + +from typing import TYPE_CHECKING + +from openapi_pydantic.compat import PYDANTIC_V2 + +from .callback import Callback as Callback +from .components import Components as Components +from .contact import Contact as Contact +from .datatype import DataType as DataType +from .discriminator import Discriminator as Discriminator +from .encoding import Encoding as Encoding +from .example import Example as Example +from .external_documentation import ExternalDocumentation as ExternalDocumentation +from .header import Header as Header +from .info import Info as Info +from .license import License as License +from .link import Link as Link +from .media_type import MediaType as MediaType +from .oauth_flow import OAuthFlow as OAuthFlow +from .oauth_flows import OAuthFlows as OAuthFlows +from .open_api import OpenAPI as OpenAPI +from .operation import Operation as Operation +from .parameter import Parameter as Parameter +from .parameter import ParameterLocation as ParameterLocation +from .path_item import PathItem as PathItem +from .paths import Paths as Paths +from .reference import Reference as Reference +from .request_body import RequestBody as RequestBody +from .response import Response as Response +from .responses import Responses as Responses +from .schema import Schema as Schema +from .schema import schema_validate as schema_validate +from .security_requirement import SecurityRequirement as SecurityRequirement +from .security_scheme import SecurityScheme as SecurityScheme +from .server import Server as Server +from .server_variable import ServerVariable as ServerVariable +from .tag import Tag as Tag +from .xml import XML as XML + +if TYPE_CHECKING: + pass +elif PYDANTIC_V2: + # resolve forward references + Encoding.model_rebuild() + OpenAPI.model_rebuild() + Components.model_rebuild() + Operation.model_rebuild() +else: + # resolve forward references + Encoding.update_forward_refs(Header=Header) + Schema.update_forward_refs() + Operation.update_forward_refs(PathItem=PathItem) diff --git a/openapi_pydantic/v3/v3_0/callback.py b/openapi_pydantic/v3/v3_0/callback.py new file mode 100644 index 0000000..bd9a0bc --- /dev/null +++ b/openapi_pydantic/v3/v3_0/callback.py @@ -0,0 +1,24 @@ +from typing import TYPE_CHECKING, Dict + +if TYPE_CHECKING: + from .path_item import PathItem + + +Callback = Dict[str, "PathItem"] +""" +A map of possible out-of band callbacks related to the parent operation. +Each value in the map is a [Path Item Object](#pathItemObject) +that describes a set of requests that may be initiated by the API provider and the +expected responses. The key value used to identify the path item object is an +expression, evaluated at runtime, that identifies a URL to use for the callback +operation. +""" + +"""Patterned Fields""" + +# {expression}: 'PathItem' = ... +""" +A Path Item Object used to define a callback request and expected responses. + +A [complete example](../examples/v3.0/callback-example.yaml) is available. +""" diff --git a/openapi_pydantic/v3/v3_0/components.py b/openapi_pydantic/v3/v3_0/components.py new file mode 100644 index 0000000..7010e93 --- /dev/null +++ b/openapi_pydantic/v3/v3_0/components.py @@ -0,0 +1,138 @@ +from typing import Dict, Optional, Union + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .callback import Callback +from .example import Example +from .header import Header +from .link import Link +from .parameter import Parameter +from .reference import Reference +from .request_body import RequestBody +from .response import Response +from .schema import Schema +from .security_scheme import SecurityScheme + +_examples = [ + { + "schemas": { + "GeneralError": { + "type": "object", + "properties": { + "code": {"type": "integer", "format": "int32"}, + "message": {"type": "string"}, + }, + }, + "Category": { + "type": "object", + "properties": { + "id": {"type": "integer", "format": "int64"}, + "name": {"type": "string"}, + }, + }, + "Tag": { + "type": "object", + "properties": { + "id": {"type": "integer", "format": "int64"}, + "name": {"type": "string"}, + }, + }, + }, + "parameters": { + "skipParam": { + "name": "skip", + "in": "query", + "description": "number of items to skip", + "required": True, + "schema": {"type": "integer", "format": "int32"}, + }, + "limitParam": { + "name": "limit", + "in": "query", + "description": "max records to return", + "required": True, + "schema": {"type": "integer", "format": "int32"}, + }, + }, + "responses": { + "NotFound": {"description": "Entity not found."}, + "IllegalInput": {"description": "Illegal input for operation."}, + "GeneralError": { + "description": "General Error", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/GeneralError"} + } + }, + }, + }, + "securitySchemes": { + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header", + }, + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "http://example.org/api/oauth/dialog", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + } + }, + }, + }, + } +] + + +class Components(BaseModel): + """ + Holds a set of reusable objects for different aspects of the OAS. + All objects defined within the components object will have no effect on the API + unless they are explicitly referenced from properties outside the components object. + """ + + schemas: Optional[Dict[str, Union[Reference, Schema]]] = None + """An object to hold reusable [Schema Objects](#schemaObject).""" + + responses: Optional[Dict[str, Union[Response, Reference]]] = None + """An object to hold reusable [Response Objects](#responseObject).""" + + parameters: Optional[Dict[str, Union[Parameter, Reference]]] = None + """An object to hold reusable [Parameter Objects](#parameterObject).""" + + examples: Optional[Dict[str, Union[Example, Reference]]] = None + """An object to hold reusable [Example Objects](#exampleObject).""" + + requestBodies: Optional[Dict[str, Union[RequestBody, Reference]]] = None + """An object to hold reusable [Request Body Objects](#requestBodyObject).""" + + headers: Optional[Dict[str, Union[Header, Reference]]] = None + """An object to hold reusable [Header Objects](#headerObject).""" + + securitySchemes: Optional[Dict[str, Union[SecurityScheme, Reference]]] = None + """An object to hold reusable [Security Scheme Objects](#securitySchemeObject).""" + + links: Optional[Dict[str, Union[Link, Reference]]] = None + """An object to hold reusable [Link Objects](#linkObject).""" + + callbacks: Optional[Dict[str, Union[Callback, Reference]]] = None + """An object to hold reusable [Callback Objects](#callbackObject).""" + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/contact.py b/openapi_pydantic/v3/v3_0/contact.py new file mode 100644 index 0000000..9c76af6 --- /dev/null +++ b/openapi_pydantic/v3/v3_0/contact.py @@ -0,0 +1,48 @@ +from typing import Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [ + { + "name": "API Support", + "url": "http://www.example.com/support", + "email": "support@example.com", + } +] + + +class Contact(BaseModel): + """ + Contact information for the exposed API. + """ + + name: Optional[str] = None + """ + The identifying name of the contact person/organization. + """ + + url: Optional[str] = None + """ + The URL pointing to the contact information. + MUST be in the format of a URL. + """ + + email: Optional[str] = None + """ + The email address of the contact person/organization. + MUST be in the format of an email address. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/datatype.py b/openapi_pydantic/v3/v3_0/datatype.py new file mode 100644 index 0000000..c62492a --- /dev/null +++ b/openapi_pydantic/v3/v3_0/datatype.py @@ -0,0 +1,15 @@ +import enum + + +class DataType(str, enum.Enum): + """Data type of an object. + + Note: OpenAPI 3.0.x does not support null as a data type. + """ + + STRING = "string" + NUMBER = "number" + INTEGER = "integer" + BOOLEAN = "boolean" + ARRAY = "array" + OBJECT = "object" diff --git a/openapi_pydantic/v3/v3_0/discriminator.py b/openapi_pydantic/v3/v3_0/discriminator.py new file mode 100644 index 0000000..1348906 --- /dev/null +++ b/openapi_pydantic/v3/v3_0/discriminator.py @@ -0,0 +1,52 @@ +from typing import Dict, Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [ + { + "propertyName": "petType", + "mapping": { + "dog": "#/components/schemas/Dog", + "monster": "https://gigantic-server.com/schemas/Monster/schema.json", + }, + } +] + + +class Discriminator(BaseModel): + """ + When request bodies or response payloads may be one of a number of different + schemas, a `discriminator` object can be used to aid in serialization, + deserialization, and validation. + + The discriminator is a specific object in a schema which is used to inform the + consumer of the specification of an alternative schema based on the value + associated with it. + + When using the discriminator, _inline_ schemas will not be considered. + """ + + propertyName: str + """ + **REQUIRED**. The name of the property in the payload that will hold the + discriminator value. + """ + + mapping: Optional[Dict[str, str]] = None + """ + An object to hold mappings between payload values and schema names or references. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/encoding.py b/openapi_pydantic/v3/v3_0/encoding.py new file mode 100644 index 0000000..1cbe6c9 --- /dev/null +++ b/openapi_pydantic/v3/v3_0/encoding.py @@ -0,0 +1,94 @@ +from typing import TYPE_CHECKING, Dict, Optional, Union + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .reference import Reference + +if TYPE_CHECKING: + from .header import Header + +_examples = [ + { + "contentType": "image/png, image/jpeg", + "headers": { + "X-Rate-Limit-Limit": { + "description": "The number of allowed requests in the " + "current period", + "schema": {"type": "integer"}, + } + }, + } +] + + +class Encoding(BaseModel): + """A single encoding definition applied to a single schema property.""" + + contentType: Optional[str] = None + """ + The Content-Type for encoding a specific property. + Default value depends on the property type: + + - for `string` with `format` being `binary` – `application/octet-stream`; + - for other primitive types – `text/plain`; + - for `object` - `application/json`; + - for `array` – the default is defined based on the inner type. + + The value can be a specific media type (e.g. `application/json`), a wildcard media + type (e.g. `image/*`), or a comma-separated list of the two types. + """ + + headers: Optional[Dict[str, Union["Header", Reference]]] = None + """ + A map allowing additional information to be provided as headers, for example + `Content-Disposition`. + + `Content-Type` is described separately and SHALL be ignored in this section. + This property SHALL be ignored if the request body media type is not a `multipart`. + """ + + style: Optional[str] = None + """ + Describes how a specific property value will be serialized depending on its type. + + See [Parameter Object](#parameterObject) for details on the + [`style`](#parameterStyle) property. The behavior follows the same values as + `query` parameters, including default values. This property SHALL be ignored if + the request body media type is not `application/x-www-form-urlencoded`. + """ + + explode: Optional[bool] = None + """ + When this is true, property values of type `array` or `object` generate separate + parameters for each value of the array, or key-value-pair of the map. + + For other types of properties this property has no effect. + When [`style`](#encodingStyle) is `form`, the default value is `true`. + For all other styles, the default value is `false`. + This property SHALL be ignored if the request body media type is not + `application/x-www-form-urlencoded`. + """ + + allowReserved: bool = False + """ + Determines whether the parameter value SHOULD allow reserved characters, + as defined by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.2) + `:/?#[]@!$&'()*+,;=` to be included without percent-encoding. + The default value is `false`. + This property SHALL be ignored if the request body media type is not + `application/x-www-form-urlencoded`. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/example.py b/openapi_pydantic/v3/v3_0/example.py new file mode 100644 index 0000000..5820a17 --- /dev/null +++ b/openapi_pydantic/v3/v3_0/example.py @@ -0,0 +1,60 @@ +from typing import Any, Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [ + {"summary": "A foo example", "value": {"foo": "bar"}}, + { + "summary": "This is an example in XML", + "externalValue": "http://example.org/examples/address-example.xml", + }, + { + "summary": "This is a text example", + "externalValue": "http://foo.bar/examples/address-example.txt", + }, +] + + +class Example(BaseModel): + summary: Optional[str] = None + """ + Short description for the example. + """ + + description: Optional[str] = None + """ + Long description for the example. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + value: Optional[Any] = None + """ + Embedded literal example. + The `value` field and `externalValue` field are mutually exclusive. + To represent examples of media types that cannot naturally represented in JSON or + YAML, use a string value to contain the example, escaping where necessary. + """ + + externalValue: Optional[str] = None + """ + A URL that points to the literal example. + This provides the capability to reference examples that cannot easily be included + in JSON or YAML documents. + + The `value` field and `externalValue` field are mutually exclusive. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/external_documentation.py b/openapi_pydantic/v3/v3_0/external_documentation.py new file mode 100644 index 0000000..7d1faf8 --- /dev/null +++ b/openapi_pydantic/v3/v3_0/external_documentation.py @@ -0,0 +1,36 @@ +from typing import Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [{"description": "Find more info here", "url": "https://example.com"}] + + +class ExternalDocumentation(BaseModel): + """Allows referencing an external resource for extended documentation.""" + + description: Optional[str] = None + """ + A short description of the target documentation. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + url: str + """ + **REQUIRED**. The URL for the target documentation. + Value MUST be in the format of a URL. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/header.py b/openapi_pydantic/v3/v3_0/header.py new file mode 100644 index 0000000..451d77e --- /dev/null +++ b/openapi_pydantic/v3/v3_0/header.py @@ -0,0 +1,37 @@ +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .parameter import ParameterBase + +_examples = [ + { + "description": "The number of allowed requests in the current period", + "schema": {"type": "integer"}, + } +] + + +class Header(ParameterBase): + """ + The Header Object follows the structure of the + [Parameter Object](#parameterObject) with the following changes: + + 1. `name` MUST NOT be specified, it is given in the corresponding + `headers` map. + 2. `in` MUST NOT be specified, it is implicitly in `header`. + 3. All traits that are affected by the location MUST be applicable + to a location of `header` (for example, [`style`](#parameterStyle)). + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + allow_population_by_field_name = True + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/info.py b/openapi_pydantic/v3/v3_0/info.py new file mode 100644 index 0000000..cc6986b --- /dev/null +++ b/openapi_pydantic/v3/v3_0/info.py @@ -0,0 +1,81 @@ +from typing import Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .contact import Contact +from .license import License + +_examples = [ + { + "title": "Sample Pet Store App", + "description": "This is a sample server for a pet store.", + "termsOfService": "http://example.com/terms/", + "contact": { + "name": "API Support", + "url": "http://www.example.com/support", + "email": "support@example.com", + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html", + }, + "version": "1.0.1", + } +] + + +class Info(BaseModel): + """ + The object provides metadata about the API. + The metadata MAY be used by the clients if needed, + and MAY be presented in editing or documentation generation tools for convenience. + """ + + title: str + """ + **REQUIRED**. The title of the API. + """ + + description: Optional[str] = None + """ + A short description of the API. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + termsOfService: Optional[str] = None + """ + A URL to the Terms of Service for the API. + MUST be in the format of a URL. + """ + + contact: Optional[Contact] = None + """ + The contact information for the exposed API. + """ + + license: Optional[License] = None + """ + The license information for the exposed API. + """ + + version: str + """ + **REQUIRED**. The version of the OpenAPI document + (which is distinct from the [OpenAPI Specification version](#oasVersion) or the API + implementation version). + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/license.py b/openapi_pydantic/v3/v3_0/license.py new file mode 100644 index 0000000..fddd70c --- /dev/null +++ b/openapi_pydantic/v3/v3_0/license.py @@ -0,0 +1,41 @@ +from typing import Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [ + { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html", + } +] + + +class License(BaseModel): + """ + License information for the exposed API. + """ + + name: str + """ + **REQUIRED**. The license name used for the API. + """ + + url: Optional[str] = None + """ + A URL to the license used for the API. + MUST be in the format of a URL. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/link.py b/openapi_pydantic/v3/v3_0/link.py new file mode 100644 index 0000000..f73de4c --- /dev/null +++ b/openapi_pydantic/v3/v3_0/link.py @@ -0,0 +1,94 @@ +from typing import Any, Dict, Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .server import Server + +_examples = [ + { + "operationId": "getUserAddressByUUID", + "parameters": {"userUuid": "$response.body#/uuid"}, + }, + { + "operationRef": "#/paths/~12.0~1repositories~1{username}/get", + "parameters": {"username": "$response.body#/username"}, + }, +] + + +class Link(BaseModel): + """ + The `Link object` represents a possible design-time link for a response. + The presence of a link does not guarantee the caller's ability to successfully + invoke it, rather it provides a known relationship and traversal mechanism between + responses and other operations. + + Unlike _dynamic_ links (i.e. links provided **in** the response payload), + the OAS linking mechanism does not require link information in the runtime response. + + For computing links, and providing instructions to execute them, + a [runtime expression](#runtimeExpression) is used for accessing values in an + operation and using them as parameters while invoking the linked operation. + """ + + operationRef: Optional[str] = None + """ + A relative or absolute URI reference to an OAS operation. + This field is mutually exclusive of the `operationId` field, + and MUST point to an [Operation Object](#operationObject). + Relative `operationRef` values MAY be used to locate an existing + [Operation Object](#operationObject) in the OpenAPI definition. + """ + + operationId: Optional[str] = None + """ + The name of an _existing_, resolvable OAS operation, as defined with a unique + `operationId`. + + This field is mutually exclusive of the `operationRef` field. + """ + + parameters: Optional[Dict[str, Any]] = None + """ + A map representing parameters to pass to an operation + as specified with `operationId` or identified via `operationRef`. + The key is the parameter name to be used, + whereas the value can be a constant or an expression to be evaluated and passed to + the linked operation. + + The parameter name can be qualified using the [parameter location](#parameterIn) + `[{in}.]{name}` for operations that use the same parameter name in different + locations (e.g. path.id). + """ + + requestBody: Optional[Any] = None + """ + A literal value or [{expression}](#runtimeExpression) to use as a request body when + calling the target operation. + """ + + description: Optional[str] = None + """ + A description of the link. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + server: Optional[Server] = None + """ + A server object to be used by the target operation. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/media_type.py b/openapi_pydantic/v3/v3_0/media_type.py new file mode 100644 index 0000000..b9f591f --- /dev/null +++ b/openapi_pydantic/v3/v3_0/media_type.py @@ -0,0 +1,96 @@ +from typing import Any, Dict, Optional, Union + +from pydantic import BaseModel, Field + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .encoding import Encoding +from .example import Example +from .reference import Reference +from .schema import Schema + +_examples = [ + { + "schema": {"$ref": "#/components/schemas/Pet"}, + "examples": { + "cat": { + "summary": "An example of a cat", + "value": { + "name": "Fluffy", + "petType": "Cat", + "color": "White", + "gender": "male", + "breed": "Persian", + }, + }, + "dog": { + "summary": "An example of a dog with a cat's name", + "value": { + "name": "Puma", + "petType": "Dog", + "color": "Black", + "gender": "Female", + "breed": "Mixed", + }, + }, + }, + } +] + + +class MediaType(BaseModel): + """Each Media Type Object provides schema and examples for the media type + identified by its key.""" + + media_type_schema: Optional[Union[Reference, Schema]] = Field( + default=None, alias="schema" + ) + """ + The schema defining the content of the request, response, or parameter. + """ + + example: Optional[Any] = None + """ + Example of the media type. + + The example object SHOULD be in the correct format as specified by the media type. + + The `example` field is mutually exclusive of the `examples` field. + + Furthermore, if referencing a `schema` which contains an example, + the `example` value SHALL _override_ the example provided by the schema. + """ + + examples: Optional[Dict[str, Union[Example, Reference]]] = None + """ + Examples of the media type. + + Each example object SHOULD match the media type and specified schema if present. + + The `examples` field is mutually exclusive of the `example` field. + + Furthermore, if referencing a `schema` which contains an example, + the `examples` value SHALL _override_ the example provided by the schema. + """ + + encoding: Optional[Dict[str, Encoding]] = None + """ + A map between a property name and its encoding information. + The key, being the property name, MUST exist in the schema as a property. + The encoding object SHALL only apply to `requestBody` objects + when the media type is `multipart` or `application/x-www-form-urlencoded`. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + allow_population_by_field_name = True + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/oauth_flow.py b/openapi_pydantic/v3/v3_0/oauth_flow.py new file mode 100644 index 0000000..658d62b --- /dev/null +++ b/openapi_pydantic/v3/v3_0/oauth_flow.py @@ -0,0 +1,76 @@ +from typing import Dict, Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [ + { + "authorizationUrl": "https://example.com/api/oauth/dialog", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + }, + { + "authorizationUrl": "https://example.com/api/oauth/dialog", + "tokenUrl": "https://example.com/api/oauth/token", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + }, + { + "authorizationUrl": "/api/oauth/dialog", + "tokenUrl": "/api/oauth/token", + "refreshUrl": "/api/oauth/token", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + }, +] + + +class OAuthFlow(BaseModel): + """ + Configuration details for a supported OAuth Flow + """ + + authorizationUrl: Optional[str] = None + """ + **REQUIRED** for `oauth2 ("implicit", "authorizationCode")`. + The authorization URL to be used for this flow. + This MUST be in the form of a URL. + """ + + tokenUrl: Optional[str] = None + """ + **REQUIRED** for `oauth2 ("password", "clientCredentials", "authorizationCode")`. + The token URL to be used for this flow. + This MUST be in the form of a URL. + """ + + refreshUrl: Optional[str] = None + """ + The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. + """ + + scopes: Dict[str, str] + """ + **REQUIRED**. The available scopes for the OAuth2 security scheme. + A map between the scope name and a short description for it. + The map MAY be empty. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/oauth_flows.py b/openapi_pydantic/v3/v3_0/oauth_flows.py new file mode 100644 index 0000000..c8a8e5a --- /dev/null +++ b/openapi_pydantic/v3/v3_0/oauth_flows.py @@ -0,0 +1,47 @@ +from typing import Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .oauth_flow import OAuthFlow + + +class OAuthFlows(BaseModel): + """ + Allows configuration of the supported OAuth Flows. + """ + + implicit: Optional[OAuthFlow] = None + """ + Configuration for the OAuth Implicit flow + """ + + password: Optional[OAuthFlow] = None + """ + Configuration for the OAuth Resource Owner Password flow + """ + + clientCredentials: Optional[OAuthFlow] = None + """ + Configuration for the OAuth Client Credentials flow. + + Previously called `application` in OpenAPI 2.0. + """ + + authorizationCode: Optional[OAuthFlow] = None + """ + Configuration for the OAuth Authorization Code flow. + + Previously called `accessCode` in OpenAPI 2.0. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + ) + + else: + + class Config: + extra = Extra.allow diff --git a/openapi_pydantic/v3/v3_0/open_api.py b/openapi_pydantic/v3/v3_0/open_api.py new file mode 100644 index 0000000..b436ffa --- /dev/null +++ b/openapi_pydantic/v3/v3_0/open_api.py @@ -0,0 +1,84 @@ +from typing import List, Literal, Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .components import Components +from .external_documentation import ExternalDocumentation +from .info import Info +from .paths import Paths +from .security_requirement import SecurityRequirement +from .server import Server +from .tag import Tag + + +class OpenAPI(BaseModel): + """This is the root document object of the OpenAPI document.""" + + openapi: Literal["3.0.4", "3.0.3", "3.0.2", "3.0.1", "3.0.0"] = "3.0.4" + """ + **REQUIRED**. This string MUST be the [semantic version number](https://semver.org/spec/v2.0.0.html) + of the [OpenAPI Specification version](#versions) that the OpenAPI document uses. + The `openapi` field SHOULD be used by tooling specifications and clients to + interpret the OpenAPI document. This is *not* related to the API + [`info.version`](#infoVersion) string. + """ + + info: Info + """ + **REQUIRED**. Provides metadata about the API. The metadata MAY be used by tooling + as required. + """ + + servers: List[Server] = [Server(url="/")] + """ + An array of Server Objects, which provide connectivity information to a target + server. If the `servers` property is not provided, or is an empty array, + the default value would be a [Server Object](#serverObject) with a + [url](#serverUrl) value of `/`. + """ + + paths: Paths + """ + **REQUIRED**. The available paths and operations for the API. + """ + + components: Optional[Components] = None + """ + An element to hold various schemas for the specification. + """ + + security: Optional[List[SecurityRequirement]] = None + """ + A declaration of which security mechanisms can be used across the API. + The list of values includes alternative security requirement objects that can be + used. Only one of the security requirement objects need to be satisfied to + authorize a request. Individual operations can override this definition. + To make security optional, an empty security requirement (`{}`) can be included in + the array. + """ + + tags: Optional[List[Tag]] = None + """ + A list of tags used by the specification with additional metadata. + The order of the tags can be used to reflect on their order by the parsing tools. + Not all tags that are used by the [Operation Object](#operationObject) must be + declared. The tags that are not declared MAY be organized randomly or based on the + tools' logic. Each tag name in the list MUST be unique. + """ + + externalDocs: Optional[ExternalDocumentation] = None + """ + Additional external documentation. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + ) + + else: + + class Config: + extra = Extra.allow diff --git a/openapi_pydantic/v3/v3_0/operation.py b/openapi_pydantic/v3/v3_0/operation.py new file mode 100644 index 0000000..d8f50cc --- /dev/null +++ b/openapi_pydantic/v3/v3_0/operation.py @@ -0,0 +1,173 @@ +from typing import Dict, List, Optional, Union + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .callback import Callback +from .external_documentation import ExternalDocumentation +from .parameter import Parameter +from .reference import Reference +from .request_body import RequestBody +from .responses import Responses +from .security_requirement import SecurityRequirement +from .server import Server + +_examples = [ + { + "tags": ["pet"], + "summary": "Updates a pet in the store with form data", + "operationId": "updatePetWithForm", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": True, + "schema": {"type": "string"}, + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "name": { + "description": "Updated name of the pet", + "type": "string", + }, + "status": { + "description": "Updated status of the pet", + "type": "string", + }, + }, + "required": ["status"], + } + } + } + }, + "responses": { + "200": { + "description": "Pet updated.", + "content": {"application/json": {}, "application/xml": {}}, + }, + "405": { + "description": "Method Not Allowed", + "content": {"application/json": {}, "application/xml": {}}, + }, + }, + "security": [{"petstore_auth": ["write:pets", "read:pets"]}], + } +] + + +class Operation(BaseModel): + """Describes a single API operation on a path.""" + + tags: Optional[List[str]] = None + """ + A list of tags for API documentation control. + Tags can be used for logical grouping of operations by resources or any other + qualifier. + """ + + summary: Optional[str] = None + """ + A short summary of what the operation does. + """ + + description: Optional[str] = None + """ + A verbose explanation of the operation behavior. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + externalDocs: Optional[ExternalDocumentation] = None + """ + Additional external documentation for this operation. + """ + + operationId: Optional[str] = None + """ + Unique string used to identify the operation. + The id MUST be unique among all operations described in the API. + The operationId value is **case-sensitive**. + Tools and libraries MAY use the operationId to uniquely identify an operation, + therefore, it is RECOMMENDED to follow common programming naming conventions. + """ + + parameters: Optional[List[Union[Parameter, Reference]]] = None + """ + A list of parameters that are applicable for this operation. + If a parameter is already defined at the [Path Item](#pathItemParameters), + the new definition will override it but can never remove it. + The list MUST NOT include duplicated parameters. + A unique parameter is defined by a combination of a [name](#parameterName) and + [location](#parameterIn). The list can use the [Reference Object](#referenceObject) + to link to parameters that are defined at the + [OpenAPI Object's components/parameters](#componentsParameters). + """ + + requestBody: Optional[Union[RequestBody, Reference]] = None + """ + The request body applicable for this operation. + + The `requestBody` is only supported in HTTP methods where the HTTP 1.1 specification + [RFC7231](https://tools.ietf.org/html/rfc7231#section-4.3.1) has explicitly defined + semantics for request bodies. In other cases where the HTTP spec is vague, + `requestBody` SHALL be ignored by consumers. + """ + + responses: Responses + """ + **REQUIRED**. The list of possible responses as they are returned from executing + this operation. + """ + + callbacks: Optional[Dict[str, Callback]] = None + """ + A map of possible out-of band callbacks related to the parent operation. + The key is a unique identifier for the Callback Object. + Each value in the map is a [Callback Object](#callbackObject) + that describes a request that may be initiated by the API provider and the expected + responses. + """ + + deprecated: bool = False + """ + Declares this operation to be deprecated. + Consumers SHOULD refrain from usage of the declared operation. + Default value is `false`. + """ + + security: Optional[List[SecurityRequirement]] = None + """ + A declaration of which security mechanisms can be used for this operation. + The list of values includes alternative security requirement objects that can be + used. Only one of the security requirement objects need to be satisfied to + authorize a request. To make security optional, an empty security requirement + (`{}`) can be included in the array. This definition overrides any declared + top-level [`security`](#oasSecurity). To remove a top-level security declaration, + an empty array can be used. + """ + + servers: Optional[List[Server]] = None + """ + An alternative `server` array to service this operation. + If an alternative `server` object is specified at the Path Item Object or Root + level, it will be overridden by this value. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/parameter.py b/openapi_pydantic/v3/v3_0/parameter.py new file mode 100644 index 0000000..80498ba --- /dev/null +++ b/openapi_pydantic/v3/v3_0/parameter.py @@ -0,0 +1,234 @@ +import enum +from typing import Any, Dict, Optional, Union + +from pydantic import BaseModel, Field + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .example import Example +from .media_type import MediaType +from .reference import Reference +from .schema import Schema + +_examples = [ + { + "name": "token", + "in": "header", + "description": "token to be passed as a header", + "required": True, + "schema": { + "type": "array", + "items": {"type": "integer", "format": "int64"}, + }, + "style": "simple", + }, + { + "name": "username", + "in": "path", + "description": "username to fetch", + "required": True, + "schema": {"type": "string"}, + }, + { + "name": "id", + "in": "query", + "description": "ID of the object to fetch", + "required": False, + "schema": {"type": "array", "items": {"type": "string"}}, + "style": "form", + "explode": True, + }, + { + "in": "query", + "name": "freeForm", + "schema": { + "type": "object", + "additionalProperties": {"type": "integer"}, + }, + "style": "form", + }, + { + "in": "query", + "name": "coordinates", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["lat", "long"], + "properties": { + "lat": {"type": "number"}, + "long": {"type": "number"}, + }, + } + } + }, + }, +] + + +class ParameterLocation(str, enum.Enum): + """The location of a given parameter.""" + + QUERY = "query" + HEADER = "header" + PATH = "path" + COOKIE = "cookie" + + +class ParameterBase(BaseModel): + """ + Base class for Parameter and Header. + + (Header is like Parameter, but has no `name` or `in` fields.) + """ + + description: Optional[str] = None + """ + A brief description of the parameter. + This could contain examples of use. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + required: bool = False + """ + Determines whether this parameter is mandatory. + If the [parameter location](#parameterIn) is `"path"`, this property is + **REQUIRED** and its value MUST be `true`. + Otherwise, the property MAY be included and its default value is `false`. + """ + + deprecated: bool = False + """ + Specifies that a parameter is deprecated and SHOULD be transitioned out of usage. + Default value is `false`. + """ + + style: Optional[str] = None + """ + Describes how the parameter value will be serialized depending on the type of the + parameter value. Default values (based on value of `in`): + + - for `query` - `form`; + - for `path` - `simple`; + - for `header` - `simple`; + - for `cookie` - `form`. + """ + + explode: Optional[bool] = None + """ + When this is true, parameter values of type `array` or `object` generate separate + parameters for each value of the array or key-value pair of the map. + For other types of parameters this property has no effect. + When [`style`](#parameterStyle) is `form`, the default value is `true`. + For all other styles, the default value is `false`. + """ + + param_schema: Optional[Union[Reference, Schema]] = Field( + default=None, alias="schema" + ) + """ + The schema defining the type used for the parameter. + """ + + example: Optional[Any] = None + """ + Example of the parameter's potential value. + The example SHOULD match the specified schema and encoding properties if present. + The `example` field is mutually exclusive of the `examples` field. + Furthermore, if referencing a `schema` that contains an example, + the `example` value SHALL _override_ the example provided by the schema. + To represent examples of media types that cannot naturally be represented in JSON + or YAML, a string value can contain the example with escaping where necessary. + """ + + examples: Optional[Dict[str, Union[Example, Reference]]] = None + """ + Examples of the parameter's potential value. + Each example SHOULD contain a value in the correct format as specified in the + parameter encoding. The `examples` field is mutually exclusive of the `example` + field. Furthermore, if referencing a `schema` that contains an example, + the `examples` value SHALL _override_ the example provided by the schema. + """ + + """ + For more complex scenarios, the [`content`](#parameterContent) property + can define the media type and schema of the parameter. + A parameter MUST contain either a `schema` property, or a `content` property, but + not both. When `example` or `examples` are provided in conjunction with the + `schema` object, the example MUST follow the prescribed serialization strategy for + the parameter. + """ + + content: Optional[Dict[str, MediaType]] = None + """ + A map containing the representations for the parameter. + The key is the media type and the value describes it. + The map MUST only contain one entry. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + allow_population_by_field_name = True + schema_extra = {"examples": _examples} + + +class Parameter(ParameterBase): + """ + Describes a single operation parameter. + + A unique parameter is defined by a combination of a [name](#parameterName) and + [location](#parameterIn). + """ + + """Fixed Fields""" + + name: str + """ + **REQUIRED**. The name of the parameter. + Parameter names are *case sensitive*. + + - If [`in`](#parameterIn) is `"path"`, the `name` field MUST correspond to a + template expression occurring within the [path](#pathsPath) field in the + [Paths Object](#pathsObject). See [Path Templating](#pathTemplating) for further + information. + - If [`in`](#parameterIn) is `"header"` and the `name` field is `"Accept"`, + `"Content-Type"` or `"Authorization"`, the parameter definition SHALL be ignored. + - For all other cases, the `name` corresponds to the parameter name used by the + [`in`](#parameterIn) property. + """ + + param_in: ParameterLocation = Field(alias="in") + """ + **REQUIRED**. The location of the parameter. Possible values are `"query"`, + `"header"`, `"path"` or `"cookie"`. + """ + + allowEmptyValue: bool = False + """ + Sets the ability to pass empty-valued parameters. + This is valid only for `query` parameters and allows sending a parameter with an + empty value. Default value is `false`. + If [`style`](#parameterStyle) is used, and if behavior is `n/a` (cannot be + serialized), the value of `allowEmptyValue` SHALL be ignored. + Use of this property is NOT RECOMMENDED, as it is likely to be removed in a later + revision. + """ + + allowReserved: bool = False + """ + Determines whether the parameter value SHOULD allow reserved characters, + as defined by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.2) + `:/?#[]@!$&'()*+,;=` to be included without percent-encoding. + This property only applies to parameters with an `in` value of `query`. + The default value is `false`. + """ diff --git a/openapi_pydantic/v3/v3_0/path_item.py b/openapi_pydantic/v3/v3_0/path_item.py new file mode 100644 index 0000000..2344e9c --- /dev/null +++ b/openapi_pydantic/v3/v3_0/path_item.py @@ -0,0 +1,152 @@ +from typing import List, Optional, Union + +from pydantic import BaseModel, Field + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .operation import Operation +from .parameter import Parameter +from .reference import Reference +from .server import Server + +_examples = [ + { + "get": { + "description": "Returns pets based on ID", + "summary": "Find pets by ID", + "operationId": "getPetsById", + "responses": { + "200": { + "description": "pet response", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": {"$ref": "#/components/schemas/Pet"}, + } + } + }, + }, + "default": { + "description": "error payload", + "content": { + "text/html": { + "schema": {"$ref": "#/components/schemas/ErrorModel"} + } + }, + }, + }, + }, + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to use", + "required": True, + "schema": {"type": "array", "items": {"type": "string"}}, + "style": "simple", + } + ], + } +] + + +class PathItem(BaseModel): + """ + Describes the operations available on a single path. + A Path Item MAY be empty, due to [ACL constraints](#securityFiltering). + The path itself is still exposed to the documentation viewer + but they will not know which operations and parameters are available. + """ + + ref: Optional[str] = Field(default=None, alias="$ref") + """ + Allows for an external definition of this path item. + The referenced structure MUST be in the format of a + [Path Item Object](#pathItemObject). + + In case a Path Item Object field appears both in the defined object and the + referenced object, the behavior is undefined. + """ + + summary: Optional[str] = None + """ + An optional, string summary, intended to apply to all operations in this path. + """ + + description: Optional[str] = None + """ + An optional, string description, intended to apply to all operations in this path. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + get: Optional[Operation] = None + """ + A definition of a GET operation on this path. + """ + + put: Optional[Operation] = None + """ + A definition of a PUT operation on this path. + """ + + post: Optional[Operation] = None + """ + A definition of a POST operation on this path. + """ + + delete: Optional[Operation] = None + """ + A definition of a DELETE operation on this path. + """ + + options: Optional[Operation] = None + """ + A definition of a OPTIONS operation on this path. + """ + + head: Optional[Operation] = None + """ + A definition of a HEAD operation on this path. + """ + + patch: Optional[Operation] = None + """ + A definition of a PATCH operation on this path. + """ + + trace: Optional[Operation] = None + """ + A definition of a TRACE operation on this path. + """ + + servers: Optional[List[Server]] = None + """ + An alternative `server` array to service all operations in this path. + """ + + parameters: Optional[List[Union[Parameter, Reference]]] = None + """ + A list of parameters that are applicable for all the operations described under + this path. These parameters can be overridden at the operation level, but cannot be + removed there. The list MUST NOT include duplicated parameters. + A unique parameter is defined by a combination of a [name](#parameterName) and + [location](#parameterIn). The list can use the [Reference Object](#referenceObject) + to link to parameters that are defined at the + [OpenAPI Object's components/parameters](#componentsParameters). + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + allow_population_by_field_name = True + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/paths.py b/openapi_pydantic/v3/v3_0/paths.py new file mode 100644 index 0000000..db258e6 --- /dev/null +++ b/openapi_pydantic/v3/v3_0/paths.py @@ -0,0 +1,27 @@ +from typing import Dict + +from .path_item import PathItem + +Paths = Dict[str, PathItem] +""" +Holds the relative paths to the individual endpoints and their operations. +The path is appended to the URL from the [`Server Object`](#serverObject) in order to +construct the full URL. + +The Paths MAY be empty, due to [ACL constraints](#securityFiltering). +""" + +"""Patterned Fields""" + +# "/{path}" : PathItem +""" +A relative path to an individual endpoint. +The field name MUST begin with a forward slash (`/`). +The path is **appended** (no relative URL resolution) to the expanded URL +from the [`Server Object`](#serverObject)'s `url` field in order to construct the full +URL. [Path templating](#pathTemplating) is allowed. +When matching URLs, concrete (non-templated) paths would be matched before their +templated counterparts. Templated paths with the same hierarchy but different templated +names MUST NOT exist as they are identical. In case of ambiguous matching, it's up to +the tooling to decide which one to use. +""" diff --git a/openapi_pydantic/v3/v3_0/reference.py b/openapi_pydantic/v3/v3_0/reference.py new file mode 100644 index 0000000..29b2206 --- /dev/null +++ b/openapi_pydantic/v3/v3_0/reference.py @@ -0,0 +1,38 @@ +from pydantic import BaseModel, Field + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [ + {"$ref": "#/components/schemas/Pet"}, + {"$ref": "Pet.json"}, + {"$ref": "definitions.json#/Pet"}, +] + + +class Reference(BaseModel): + """ + A simple object to allow referencing other components in the specification. + + The Reference Object is defined by [JSON Reference](https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03) + and follows the same structure, behavior and rules. + + For this specification, reference resolution is accomplished as defined by the JSON + Reference specification and not by the JSON Schema specification. + """ + + ref: str = Field(alias="$ref") + """**REQUIRED**. The reference string.""" + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + allow_population_by_field_name = True + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/request_body.py b/openapi_pydantic/v3/v3_0/request_body.py new file mode 100644 index 0000000..1741a5a --- /dev/null +++ b/openapi_pydantic/v3/v3_0/request_body.py @@ -0,0 +1,95 @@ +from typing import Dict, Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .media_type import MediaType + +_examples = [ + { + "description": "user to add to the system", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"}, + "examples": { + "user": { + "summary": "User Example", + "externalValue": "http://foo.bar/examples/user-example.json", + } + }, + }, + "application/xml": { + "schema": {"$ref": "#/components/schemas/User"}, + "examples": { + "user": { + "summary": "User example in XML", + "externalValue": "http://foo.bar/examples/user-example.xml", + } + }, + }, + "text/plain": { + "examples": { + "user": { + "summary": "User example in Plain text", + "externalValue": "http://foo.bar/examples/user-example.txt", + } + } + }, + "*/*": { + "examples": { + "user": { + "summary": "User example in other format", + "externalValue": "http://foo.bar/examples/user-example.whatever", + } + } + }, + }, + }, + { + "description": "user to add to the system", + "content": { + "text/plain": {"schema": {"type": "array", "items": {"type": "string"}}} + }, + }, +] + + +class RequestBody(BaseModel): + """Describes a single request body.""" + + description: Optional[str] = None + """ + A brief description of the request body. + This could contain examples of use. + + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + content: Dict[str, MediaType] + """ + **REQUIRED**. The content of the request body. + The key is a media type or [media type range](https://tools.ietf.org/html/rfc7231#appendix-D) + and the value describes it. + + For requests that match multiple keys, only the most specific key is applicable. + e.g. text/plain overrides text/* + """ + + required: bool = False + """ + Determines if the request body is required in the request. Defaults to `false`. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/response.py b/openapi_pydantic/v3/v3_0/response.py new file mode 100644 index 0000000..107962d --- /dev/null +++ b/openapi_pydantic/v3/v3_0/response.py @@ -0,0 +1,101 @@ +from typing import Dict, Optional, Union + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .header import Header +from .link import Link +from .media_type import MediaType +from .reference import Reference + +_examples = [ + { + "description": "A complex object array response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": {"$ref": "#/components/schemas/VeryComplexType"}, + } + } + }, + }, + { + "description": "A simple string response", + "content": {"text/plain": {"schema": {"type": "string"}}}, + }, + { + "description": "A simple string response", + "content": {"text/plain": {"schema": {"type": "string", "example": "whoa!"}}}, + "headers": { + "X-Rate-Limit-Limit": { + "description": ("The number of allowed requests in the current period"), + "schema": {"type": "integer"}, + }, + "X-Rate-Limit-Remaining": { + "description": ( + "The number of remaining requests in the current period" + ), + "schema": {"type": "integer"}, + }, + "X-Rate-Limit-Reset": { + "description": ("The number of seconds left in the current period"), + "schema": {"type": "integer"}, + }, + }, + }, + {"description": "object created"}, +] + + +class Response(BaseModel): + """ + Describes a single response from an API Operation, including design-time, + static `links` to operations based on the response. + """ + + description: str + """ + **REQUIRED**. A short description of the response. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + headers: Optional[Dict[str, Union[Header, Reference]]] = None + """ + Maps a header name to its definition. + [RFC7230](https://tools.ietf.org/html/rfc7230#page-22) states header names are case + insensitive. If a response header is defined with the name `"Content-Type"`, it + SHALL be ignored. + """ + + content: Optional[Dict[str, MediaType]] = None + """ + A map containing descriptions of potential response payloads. + The key is a media type or [media type range](https://tools.ietf.org/html/rfc7231#appendix-D) + and the value describes it. + + For responses that match multiple keys, only the most specific key is applicable. + e.g. text/plain overrides text/* + """ + + links: Optional[Dict[str, Union[Link, Reference]]] = None + """ + A map of operations links that can be followed from the response. + The key of the map is a short name for the link, + following the naming constraints of the names for + [Component Objects](#componentsObject). + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/responses.py b/openapi_pydantic/v3/v3_0/responses.py new file mode 100644 index 0000000..4bd45e3 --- /dev/null +++ b/openapi_pydantic/v3/v3_0/responses.py @@ -0,0 +1,49 @@ +from typing import Dict, Union + +from .reference import Reference +from .response import Response + +Responses = Dict[str, Union[Response, Reference]] +""" +A container for the expected responses of an operation. +The container maps a HTTP response code to the expected response. + +The documentation is not necessarily expected to cover all possible HTTP response codes +because they may not be known in advance. +However, documentation is expected to cover a successful operation response and any +known errors. + +The `default` MAY be used as a default response object for all HTTP codes +that are not covered individually by the specification. + +The `Responses Object` MUST contain at least one response code, and it +SHOULD be the response for a successful operation call. +""" + +"""Fixed Fields""" + +# default: Optional[Union[Response, Reference]] +""" +The documentation of responses other than the ones declared for specific HTTP response +codes. Use this field to cover undeclared responses. +A [Reference Object](#referenceObject) can link to a response +that the [OpenAPI Object's components/responses](#componentsResponses) section defines. +""" + +"""Patterned Fields""" +# {httpStatusCode]: Optional[Union[Response, Reference]] +""" +Any [HTTP status code](#httpCodes) can be used as the property name, +but only one property per code, to describe the expected response for that HTTP status +code. + +A [Reference Object](#referenceObject) can link to a response +that is defined in the [OpenAPI Object's components/responses](#componentsResponses) +section. This field MUST be enclosed in quotation marks (for example, "200") for +compatibility between JSON and YAML. To define a range of response codes, this field +MAY contain the uppercase wildcard character `X`. For example, `2XX` represents all +response codes between `[200-299]`. Only the following range definitions are allowed: +`1XX`, `2XX`, `3XX`, `4XX`, and `5XX`. +If a response is defined using an explicit code, +the explicit code definition takes precedence over the range definition for that code. +""" diff --git a/openapi_pydantic/v3/v3_0/schema.py b/openapi_pydantic/v3/v3_0/schema.py new file mode 100644 index 0000000..e3f04ce --- /dev/null +++ b/openapi_pydantic/v3/v3_0/schema.py @@ -0,0 +1,614 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union + +from pydantic import BaseModel, Field + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra, min_length_arg + +from .datatype import DataType +from .discriminator import Discriminator +from .external_documentation import ExternalDocumentation +from .reference import Reference +from .xml import XML + +_examples = [ + {"type": "string", "format": "email"}, + { + "type": "object", + "required": ["name"], + "properties": { + "name": {"type": "string"}, + "address": {"$ref": "#/components/schemas/Address"}, + "age": {"type": "integer", "format": "int32", "minimum": 0}, + }, + }, + {"type": "object", "additionalProperties": {"type": "string"}}, + { + "type": "object", + "additionalProperties": {"$ref": "#/components/schemas/ComplexModel"}, + }, + { + "type": "object", + "properties": { + "id": {"type": "integer", "format": "int64"}, + "name": {"type": "string"}, + }, + "required": ["name"], + "example": {"name": "Puma", "id": 1}, + }, + { + "type": "object", + "required": ["message", "code"], + "properties": { + "message": {"type": "string"}, + "code": {"type": "integer", "minimum": 100, "maximum": 600}, + }, + }, + { + "allOf": [ + {"$ref": "#/components/schemas/ErrorModel"}, + { + "type": "object", + "required": ["rootCause"], + "properties": {"rootCause": {"type": "string"}}, + }, + ] + }, + { + "type": "object", + "discriminator": {"propertyName": "petType"}, + "properties": { + "name": {"type": "string"}, + "petType": {"type": "string"}, + }, + "required": ["name", "petType"], + }, + { + "description": "A representation of a cat. " + "Note that `Cat` will be used as the discriminator value.", + "allOf": [ + {"$ref": "#/components/schemas/Pet"}, + { + "type": "object", + "properties": { + "huntingSkill": { + "type": "string", + "description": "The measured skill for hunting", + "default": "lazy", + "enum": [ + "clueless", + "lazy", + "adventurous", + "aggressive", + ], + } + }, + "required": ["huntingSkill"], + }, + ], + }, + { + "description": "A representation of a dog. " + "Note that `Dog` will be used as the discriminator value.", + "allOf": [ + {"$ref": "#/components/schemas/Pet"}, + { + "type": "object", + "properties": { + "packSize": { + "type": "integer", + "format": "int32", + "description": ("the size of the pack the dog is from"), + "default": 0, + "minimum": 0, + } + }, + "required": ["packSize"], + }, + ], + }, +] + + +class Schema(BaseModel): + """ + The Schema Object allows the definition of input and output data types. + These types can be objects, but also primitives and arrays. + This object is an extended subset of the [JSON Schema Specification Wright Draft 00](https://json-schema.org/). + + For more information about the properties, + see [JSON Schema Core](https://tools.ietf.org/html/draft-wright-json-schema-00) + and [JSON Schema Validation](https://tools.ietf.org/html/draft-wright-json-schema-validation-00). + Unless stated otherwise, the property definitions follow the JSON Schema. + """ + + """ + The following properties are taken directly from the JSON Schema definition and + follow the same specifications: + """ + + title: Optional[str] = None + """ + The value of "title" MUST be a string. + + The title can be used to decorate a user interface with + information about the data produced by this user interface. + The title will preferrably be short. + """ + + multipleOf: Optional[float] = Field(default=None, gt=0.0) + """ + The value of "multipleOf" MUST be a number, strictly greater than 0. + + A numeric instance is only valid if division by this keyword's value + results in an integer. + """ + + maximum: Optional[float] = None + """ + The value of "maximum" MUST be a number, representing an upper limit + for a numeric instance. + + If the instance is a number, then this keyword validates if + "exclusiveMaximum" is true and instance is less than the provided + value, or else if the instance is less than or exactly equal to the + provided value. + """ + + exclusiveMaximum: Optional[bool] = None + """ + The value of "exclusiveMaximum" MUST be a boolean, representing + whether the limit in "maximum" is exclusive or not. An undefined + value is the same as false. + + If "exclusiveMaximum" is true, then a numeric instance SHOULD NOT be + equal to the value specified in "maximum". If "exclusiveMaximum" is + false (or not specified), then a numeric instance MAY be equal to the + value of "maximum". + """ + + minimum: Optional[float] = None + """ + The value of "minimum" MUST be a number, representing a lower limit + for a numeric instance. + + If the instance is a number, then this keyword validates if + "exclusiveMinimum" is true and instance is greater than the provided + value, or else if the instance is greater than or exactly equal to + the provided value. + """ + + exclusiveMinimum: Optional[bool] = None + """ + The value of "exclusiveMinimum" MUST be a boolean, representing + whether the limit in "minimum" is exclusive or not. An undefined + value is the same as false. + + If "exclusiveMinimum" is true, then a numeric instance SHOULD NOT be + equal to the value specified in "minimum". If "exclusiveMinimum" is + false (or not specified), then a numeric instance MAY be equal to the + value of "minimum". + """ + + maxLength: Optional[int] = Field(default=None, ge=0) + """ + The value of this keyword MUST be a non-negative integer. + + The value of this keyword MUST be an integer. This integer MUST be + greater than, or equal to, 0. + + A string instance is valid against this keyword if its length is less + than, or equal to, the value of this keyword. + + The length of a string instance is defined as the number of its + characters as defined by RFC 7159 [RFC7159]. + """ + + minLength: Optional[int] = Field(default=None, ge=0) + """ + A string instance is valid against this keyword if its length is + greater than, or equal to, the value of this keyword. + + The length of a string instance is defined as the number of its + characters as defined by RFC 7159 [RFC7159]. + + The value of this keyword MUST be an integer. This integer MUST be + greater than, or equal to, 0. + + "minLength", if absent, may be considered as being present with + integer value 0. + """ + + pattern: Optional[str] = None + """ + The value of this keyword MUST be a string. This string SHOULD be a + valid regular expression, according to the ECMA 262 regular + expression dialect. + + A string instance is considered valid if the regular expression + matches the instance successfully. Recall: regular expressions are + not implicitly anchored. + """ + + maxItems: Optional[int] = Field(default=None, ge=0) + """ + The value of this keyword MUST be an integer. This integer MUST be + greater than, or equal to, 0. + + An array instance is valid against "maxItems" if its size is less + than, or equal to, the value of this keyword. + """ + + minItems: Optional[int] = Field(default=None, ge=0) + """ + The value of this keyword MUST be an integer. This integer MUST be + greater than, or equal to, 0. + + An array instance is valid against "minItems" if its size is greater + than, or equal to, the value of this keyword. + + If this keyword is not present, it may be considered present with a + value of 0. + """ + + uniqueItems: Optional[bool] = None + """ + The value of this keyword MUST be a boolean. + + If this keyword has boolean value false, the instance validates + successfully. If it has boolean value true, the instance validates + successfully if all of its elements are unique. + + If not present, this keyword may be considered present with boolean + value false. + """ + + maxProperties: Optional[int] = Field(default=None, ge=0) + """ + The value of this keyword MUST be an integer. This integer MUST be + greater than, or equal to, 0. + + An object instance is valid against "maxProperties" if its number of + properties is less than, or equal to, the value of this keyword. + """ + + minProperties: Optional[int] = Field(default=None, ge=0) + """ + The value of this keyword MUST be an integer. This integer MUST be + greater than, or equal to, 0. + + An object instance is valid against "minProperties" if its number of + properties is greater than, or equal to, the value of this keyword. + + If this keyword is not present, it may be considered present with a + value of 0. + """ + + required: Optional[List[str]] = Field(default=None, **min_length_arg(1)) + """ + The value of this keyword MUST be an array. This array MUST have at + least one element. Elements of this array MUST be strings, and MUST + be unique. + + An object instance is valid against this keyword if its property set + contains all elements in this keyword's array value. + """ + + enum: Optional[List[Any]] = Field(default=None, **min_length_arg(1)) + """ + The value of this keyword MUST be an array. This array SHOULD have + at least one element. Elements in the array SHOULD be unique. + + Elements in the array MAY be of any type, including null. + + An instance validates successfully against this keyword if its value + is equal to one of the elements in this keyword's array value. + """ + + """ + The following properties are taken from the JSON Schema definition + but their definitions were adjusted to the OpenAPI Specification. + """ + + type: Optional[DataType] = None + """ + **From OpenAPI spec: + Value MUST be a string. Multiple types via an array are not supported.** + + From JSON Schema: + The value of this keyword MUST be either a string or an array. If it + is an array, elements of the array MUST be strings and MUST be + unique. + + String values MUST be one of the seven primitive types defined by the + core specification. + + An instance matches successfully if its primitive type is one of the + types defined by keyword. Recall: "number" includes "integer". + """ + + allOf: Optional[List[Union[Reference, "Schema"]]] = None + """ + **From OpenAPI spec: + Inline or referenced schema MUST be of a [Schema Object](#schemaObject) and not a + standard JSON Schema.** + + From JSON Schema: + This keyword's value MUST be an array. This array MUST have at least + one element. + + Elements of the array MUST be objects. Each object MUST be a valid + JSON Schema. + + An instance validates successfully against this keyword if it + validates successfully against all schemas defined by this keyword's + value. + """ + + oneOf: Optional[List[Union[Reference, "Schema"]]] = None + """ + **From OpenAPI spec: + Inline or referenced schema MUST be of a [Schema Object](#schemaObject) and not a + standard JSON Schema.** + + From JSON Schema: + This keyword's value MUST be an array. This array MUST have at least + one element. + + Elements of the array MUST be objects. Each object MUST be a valid + JSON Schema. + + An instance validates successfully against this keyword if it + validates successfully against exactly one schema defined by this + keyword's value. + """ + + anyOf: Optional[List[Union[Reference, "Schema"]]] = None + """ + **From OpenAPI spec: + Inline or referenced schema MUST be of a [Schema Object](#schemaObject) and not a + standard JSON Schema.** + + From JSON Schema: + This keyword's value MUST be an array. This array MUST have at least + one element. + + Elements of the array MUST be objects. Each object MUST be a valid + JSON Schema. + + An instance validates successfully against this keyword if it + validates successfully against at least one schema defined by this + keyword's value. + """ + + schema_not: Optional[Union[Reference, "Schema"]] = Field(default=None, alias="not") + """ + **From OpenAPI spec: + Inline or referenced schema MUST be of a [Schema Object](#schemaObject) and not a + standard JSON Schema.** + + From JSON Schema: + This keyword's value MUST be an object. This object MUST be a valid + JSON Schema. + + An instance is valid against this keyword if it fails to validate + successfully against the schema defined by this keyword. + """ + + items: Optional[Union[Reference, "Schema"]] = None + """ + **From OpenAPI spec: + Value MUST be an object and not an array. + Inline or referenced schema MUST be of a [Schema Object](#schemaObject) and not a + standard JSON Schema. `items` MUST be present if the `type` is `array`.** + + From JSON Schema: + The value of "items" MUST be either a schema or array of schemas. + + Successful validation of an array instance with regards to these two + keywords is determined as follows: + + - if "items" is not present, or its value is an object, validation + of the instance always succeeds, regardless of the value of + "additionalItems"; + - if the value of "additionalItems" is boolean value true or an + object, validation of the instance always succeeds; + - if the value of "additionalItems" is boolean value false and the + value of "items" is an array, the instance is valid if its size is + less than, or equal to, the size of "items". + """ + + properties: Optional[Dict[str, Union[Reference, "Schema"]]] = None + """ + **From OpenAPI spec: + Property definitions MUST be a [Schema Object](#schemaObject) + and not a standard JSON Schema (inline or referenced).** + + From JSON Schema: + The value of "properties" MUST be an object. Each value of this + object MUST be an object, and each object MUST be a valid JSON + Schema. + + If absent, it can be considered the same as an empty object. + """ + + additionalProperties: Optional[Union[bool, Reference, "Schema"]] = None + """ + **From OpenAPI spec: + Value can be boolean or object. + Inline or referenced schema MUST be of a [Schema Object](#schemaObject) and not a + standard JSON Schema. + Consistent with JSON Schema, `additionalProperties` defaults to `true`.** + + From JSON Schema: + The value of "additionalProperties" MUST be a boolean or a schema. + + If "additionalProperties" is absent, it may be considered present + with an empty schema as a value. + + If "additionalProperties" is true, validation always succeeds. + + If "additionalProperties" is false, validation succeeds only if the + instance is an object and all properties on the instance were covered + by "properties" and/or "patternProperties". + + If "additionalProperties" is an object, validate the value as a + schema to all of the properties that weren't validated by + "properties" nor "patternProperties". + """ + + description: Optional[str] = None + """ + **From OpenAPI spec: + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation.** + + From JSON Schema: + The value "description" MUST be a string. + + The description can be used to decorate a user interface with + information about the data produced by this user interface. + The description will provide explanation about the purpose of + the instance described by this schema. + """ + + schema_format: Optional[str] = Field(default=None, alias="format") + """ + **From OpenAPI spec: + [Data Type Formats](#dataTypeFormat) for further details. + While relying on JSON Schema's defined formats, the OAS offers a few additional + predefined formats.** + + From JSON Schema: + Structural validation alone may be insufficient to validate that an + instance meets all the requirements of an application. The "format" + keyword is defined to allow interoperable semantic validation for a + fixed subset of values which are accurately described by + authoritative resources, be they RFCs or other external + specifications. + + The value of this keyword is called a format attribute. It MUST be a + string. A format attribute can generally only validate a given set + of instance types. If the type of the instance to validate is not in + this set, validation for this format attribute and instance SHOULD + succeed. + """ + + default: Optional[Any] = None + """ + **From OpenAPI spec: + The default value represents what would be assumed by the consumer of the input + as the value of the schema if one is not provided. + Unlike JSON Schema, the value MUST conform to the defined type for the Schema + Object defined at the same level. For example, if `type` is `string`, then + `default` can be `"foo"` but cannot be `1`.** + + From JSON Schema: + There are no restrictions placed on the value of this keyword. + + This keyword can be used to supply a default JSON value associated + with a particular schema. It is RECOMMENDED that a default value be + valid against the associated schema. + + This keyword MAY be used in root schemas, and in any subschemas. + """ + + """ + Other than the JSON Schema subset fields, the following fields MAY be used for + further schema documentation: + """ + + nullable: Optional[bool] = None + """ + A `true` value adds `"null"` to the allowed type specified by the `type` keyword, + only if `type` is explicitly defined within the same Schema Object. + Other Schema Object constraints retain their defined behavior, + and therefore may disallow the use of `null` as a value. + A `false` value leaves the specified or default `type` unmodified. + The default value is `false`. + """ + + discriminator: Optional[Discriminator] = None + """ + Adds support for polymorphism. + The discriminator is an object name that is used to differentiate between other + schemas which may satisfy the payload description. + See [Composition and Inheritance](#schemaComposition) for more details. + """ + + readOnly: Optional[bool] = None + """ + Relevant only for Schema `"properties"` definitions. + Declares the property as "read only". + This means that it MAY be sent as part of a response but SHOULD NOT be sent as part + of the request. If the property is marked as `readOnly` being `true` and is in the + `required` list, the `required` will take effect on the response only. + A property MUST NOT be marked as both `readOnly` and `writeOnly` being `true`. + Default value is `false`. + """ + + writeOnly: Optional[bool] = None + """ + Relevant only for Schema `"properties"` definitions. + Declares the property as "write only". + Therefore, it MAY be sent as part of a request but SHOULD NOT be sent as part of + the response. If the property is marked as `writeOnly` being `true` and is in the + `required` list, the `required` will take effect on the request only. + A property MUST NOT be marked as both `readOnly` and `writeOnly` being `true`. + Default value is `false`. + """ + + xml: Optional[XML] = None + """ + This MAY be used only on properties schemas. + It has no effect on root schemas. + Adds additional metadata to describe the XML representation of this property. + """ + + externalDocs: Optional[ExternalDocumentation] = None + """ + Additional external documentation for this schema. + """ + + example: Optional[Any] = None + """ + A free-form property to include an example of an instance for this schema. + To represent examples that cannot be naturally represented in JSON or YAML, + a string value can be used to contain the example with escaping where necessary. + """ + + deprecated: Optional[bool] = None + """ + Specifies that a schema is deprecated and SHOULD be transitioned out of usage. + Default value is `false`. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + allow_population_by_field_name = True + schema_extra = {"examples": _examples} + + +if TYPE_CHECKING: + + def schema_validate( + obj: Any, + *, + strict: Optional[bool] = None, + from_attributes: Optional[bool] = None, + context: Optional[Dict[str, Any]] = None + ) -> Schema: ... + +elif PYDANTIC_V2: + schema_validate = Schema.model_validate + +else: + schema_validate = Schema.parse_obj diff --git a/openapi_pydantic/v3/v3_0/security_requirement.py b/openapi_pydantic/v3/v3_0/security_requirement.py new file mode 100644 index 0000000..2b4e72b --- /dev/null +++ b/openapi_pydantic/v3/v3_0/security_requirement.py @@ -0,0 +1,32 @@ +from typing import Dict, List + +SecurityRequirement = Dict[str, List[str]] +""" +Lists the required security schemes to execute this operation. +The name used for each property MUST correspond to a security scheme declared in the +[Security Schemes](#componentsSecuritySchemes) under the +[Components Object](#componentsObject). + +Security Requirement Objects that contain multiple schemes require that +all schemes MUST be satisfied for a request to be authorized. +This enables support for scenarios where multiple query parameters or HTTP headers +are required to convey security information. + +When a list of Security Requirement Objects is defined on the +[OpenAPI Object](#oasObject) or [Operation Object](#operationObject), +only one of the Security Requirement Objects in the list needs to be satisfied to +authorize the request. +""" + +"""Patterned Fields""" + +# {name}: List[str] +""" +Each name MUST correspond to a security scheme which is declared +in the [Security Schemes](#componentsSecuritySchemes) under the +[Components Object](#componentsObject). +If the security scheme is of type `"oauth2"` or `"openIdConnect"`, +then the value is a list of scope names required for the execution, +and the list MAY be empty if authorization does not require a specified scope. +For other security scheme types, the array MUST be empty. +""" diff --git a/openapi_pydantic/v3/v3_0/security_scheme.py b/openapi_pydantic/v3/v3_0/security_scheme.py new file mode 100644 index 0000000..f0ab52e --- /dev/null +++ b/openapi_pydantic/v3/v3_0/security_scheme.py @@ -0,0 +1,112 @@ +from typing import Optional + +from pydantic import BaseModel, Field + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .oauth_flows import OAuthFlows + +_examples = [ + {"type": "http", "scheme": "basic"}, + {"type": "apiKey", "name": "api_key", "in": "header"}, + {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}, + { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://example.com/api/oauth/dialog", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + } + }, + }, + { + "type": "openIdConnect", + "openIdConnectUrl": "https://example.com/openIdConnect", + }, + { + "type": "openIdConnect", + "openIdConnectUrl": "openIdConnect", + }, # #5: allow relative path +] + + +class SecurityScheme(BaseModel): + """ + Defines a security scheme that can be used by the operations. + Supported schemes are HTTP authentication, + an API key (either as a header, a cookie parameter or as a query parameter), + OAuth2's common flows (implicit, password, client credentials and authorization + code) as defined in [RFC6749](https://tools.ietf.org/html/rfc6749), + and [OpenID Connect Discovery](https://tools.ietf.org/html/draft-ietf-oauth-discovery-06). + """ + + type: str + """ + **REQUIRED**. The type of the security scheme. + Valid values are `"apiKey"`, `"http"`, `"oauth2"`, `"openIdConnect"`. + """ + + description: Optional[str] = None + """ + A short description for security scheme. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + name: Optional[str] = None + """ + **REQUIRED** for `apiKey`. The name of the header, query or cookie parameter to be + used. + """ + + security_scheme_in: Optional[str] = Field(alias="in", default=None) + """ + **REQUIRED** for `apiKey`. The location of the API key. Valid values are `"query"`, + `"header"` or `"cookie"`. + """ + + scheme: Optional[str] = None + """ + **REQUIRED** for `http`. The name of the HTTP Authorization scheme to be used in the + [Authorization header as defined in RFC7235](https://tools.ietf.org/html/rfc7235#section-5.1). + + The values used SHOULD be registered in the + [IANA Authentication Scheme registry](https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml). + """ + + bearerFormat: Optional[str] = None + """ + A hint to the client to identify how the bearer token is formatted. + + Bearer tokens are usually generated by an authorization server, + so this information is primarily for documentation purposes. + """ + + flows: Optional[OAuthFlows] = None + """ + **REQUIRED** for `oauth2`. An object containing configuration information for the + flow types supported. + """ + + openIdConnectUrl: Optional[str] = None + """ + **REQUIRED** for `openIdConnect`. OpenId Connect URL to discover OAuth2 + configuration values. This MUST be in the form of a URL. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + allow_population_by_field_name = True + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/server.py b/openapi_pydantic/v3/v3_0/server.py new file mode 100644 index 0000000..104ba5b --- /dev/null +++ b/openapi_pydantic/v3/v3_0/server.py @@ -0,0 +1,67 @@ +from typing import Dict, Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .server_variable import ServerVariable + +_examples = [ + { + "url": "https://development.gigantic-server.com/v1", + "description": "Development server", + }, + { + "url": "https://{username}.gigantic-server.com:{port}/{basePath}", + "description": "The production API server", + "variables": { + "username": { + "default": "demo", + "description": "this value is assigned by the service" + "provider, in this example `gigantic-server.com`", + }, + "port": {"enum": ["8443", "443"], "default": "8443"}, + "basePath": {"default": "v2"}, + }, + }, +] + + +class Server(BaseModel): + """An object representing a Server.""" + + url: str + """ + **REQUIRED**. A URL to the target host. + + This URL supports Server Variables and MAY be relative, + to indicate that the host location is relative to the location where the OpenAPI + document is being served. + Variable substitutions will be made when a variable is named in `{`brackets`}`. + """ + + description: Optional[str] = None + """ + An optional string describing the host designated by the URL. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + variables: Optional[Dict[str, ServerVariable]] = None + """ + A map between a variable name and its value. + + The value is used for substitution in the server's URL template. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/server_variable.py b/openapi_pydantic/v3/v3_0/server_variable.py new file mode 100644 index 0000000..1b6b57e --- /dev/null +++ b/openapi_pydantic/v3/v3_0/server_variable.py @@ -0,0 +1,42 @@ +from typing import List, Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + + +class ServerVariable(BaseModel): + """An object representing a Server Variable for server URL template substitution.""" + + enum: Optional[List[str]] = None + """ + An enumeration of string values to be used if the substitution options are from a + limited set. The array SHOULD NOT be empty. + """ + + default: str + """ + **REQUIRED**. The default value to use for substitution, + which SHALL be sent if an alternate value is _not_ supplied. + Note this behavior is different than the [Schema Object's](#schemaObject) treatment + of default values, because in those cases parameter values are optional. + If the [`enum`](#serverVariableEnum) is defined, the value SHOULD exist in the + enum's values. + """ + + description: Optional[str] = None + """ + An optional description for the server variable. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + ) + + else: + + class Config: + extra = Extra.allow diff --git a/openapi_pydantic/v3/v3_0/tag.py b/openapi_pydantic/v3/v3_0/tag.py new file mode 100644 index 0000000..9ec0d03 --- /dev/null +++ b/openapi_pydantic/v3/v3_0/tag.py @@ -0,0 +1,47 @@ +from typing import Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .external_documentation import ExternalDocumentation + +_examples = [{"name": "pet", "description": "Pets operations"}] + + +class Tag(BaseModel): + """ + Adds metadata to a single tag that is used by the + [Operation Object](#operationObject). + It is not mandatory to have a Tag Object per tag defined in the Operation Object + instances. + """ + + name: str + """ + **REQUIRED**. The name of the tag. + """ + + description: Optional[str] = None + """ + A short description for the tag. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + externalDocs: Optional[ExternalDocumentation] = None + """ + Additional external documentation for this tag. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_0/util.py b/openapi_pydantic/v3/v3_0/util.py new file mode 100644 index 0000000..1b904b5 --- /dev/null +++ b/openapi_pydantic/v3/v3_0/util.py @@ -0,0 +1,260 @@ +import logging +import re +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Generic, + List, + Optional, + Set, + Type, + TypeVar, + Union, + cast, +) + +from pydantic import BaseModel + +from openapi_pydantic.compat import ( + DEFS_KEY, + PYDANTIC_V2, + JsonSchemaMode, + models_json_schema, + v1_schema, +) + +from . import Components, OpenAPI, Reference, Schema, schema_validate + +logger = logging.getLogger(__name__) + +PydanticType = TypeVar("PydanticType", bound=BaseModel) +ref_prefix = "#/components/schemas/" +ref_template = "#/components/schemas/{model}" + + +class PydanticSchema(Schema, Generic[PydanticType]): + """Special `Schema` class to indicate a reference from pydantic class""" + + schema_class: Type[PydanticType] + """the class that is used for generate the schema""" + + +def get_mode( + cls: Type[BaseModel], default: JsonSchemaMode = "validation" +) -> JsonSchemaMode: + """Get the JSON schema mode for a model class. + + The mode can be either "validation" or "serialization". In validation mode, + computed fields are dropped and optional fields remain optional. In + serialization mode, computed and optional fields are required. + """ + if not hasattr(cls, "model_config"): + return default + mode = cls.model_config.get("json_schema_mode", default) + if mode not in ("validation", "serialization"): + raise ValueError(f"invalid json_schema_mode: {mode}") + return cast(JsonSchemaMode, mode) + + +if TYPE_CHECKING: + + class GenerateOpenAPI30Schema: ... + +elif PYDANTIC_V2: + from enum import Enum + + from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue + from pydantic_core import core_schema + + class GenerateOpenAPI30Schema(GenerateJsonSchema): + """Modify the schema generation for OpenAPI 3.0.""" + + def nullable_schema( + self, + schema: core_schema.NullableSchema, + ) -> JsonSchemaValue: + """Generates a JSON schema that matches a schema that allows null values. + + In OpenAPI 3.0, types can not be None, but a special "nullable" field is + available. + """ + inner_json_schema = self.generate_inner(schema["schema"]) + inner_json_schema["nullable"] = True + return inner_json_schema + + def literal_schema(self, schema: core_schema.LiteralSchema) -> JsonSchemaValue: + """Generates a JSON schema that matches a literal value. + + In OpenAPI 3.0, the "const" keyword is not supported, so this + version of this method skips that optimization. + """ + expected = [ + v.value if isinstance(v, Enum) else v for v in schema["expected"] + ] + + types = {type(e) for e in expected} + if types == {str}: + return {"enum": expected, "type": "string"} + elif types == {int}: + return {"enum": expected, "type": "integer"} + elif types == {float}: + return {"enum": expected, "type": "number"} + elif types == {bool}: + return {"enum": expected, "type": "boolean"} + elif types == {list}: + return {"enum": expected, "type": "array"} + # there is not None case because if it's mixed it hits the final `else` + # if it's a single Literal[None] then it becomes a `const` schema above + else: + return {"enum": expected} + +else: + + class GenerateOpenAPI30Schema: ... + + +def construct_open_api_with_schema_class( + open_api: OpenAPI, + schema_classes: Optional[List[Type[BaseModel]]] = None, + scan_for_pydantic_schema_reference: bool = True, + by_alias: bool = True, +) -> OpenAPI: + """ + Construct a new OpenAPI object, utilising pydantic classes to produce JSON schemas. + + :param open_api: the base `OpenAPI` object + :param schema_classes: Pydantic classes that their schema will be used + "#/components/schemas" values + :param scan_for_pydantic_schema_reference: flag to indicate if scanning for + `PydanticSchemaReference` class + is needed for "#/components/schemas" + value updates + :param by_alias: construct schema by alias (default is True) + :return: new OpenAPI object with "#/components/schemas" values updated. + If there is no update in "#/components/schemas" values, the original + `open_api` will be returned. + """ + copy_func = getattr(open_api, "model_copy" if PYDANTIC_V2 else "copy") + new_open_api: OpenAPI = copy_func(deep=True) + + if scan_for_pydantic_schema_reference: + extracted_schema_classes = _handle_pydantic_schema(new_open_api) + if schema_classes: + schema_classes = list({*schema_classes, *extracted_schema_classes}) + else: + schema_classes = extracted_schema_classes + + if not schema_classes: + return open_api + + schema_classes.sort(key=lambda x: x.__name__) + logger.debug("schema_classes: %s", schema_classes) + + # update new_open_api with new #/components/schemas + if PYDANTIC_V2: + _key_map, schema_definitions = models_json_schema( + [(c, get_mode(c)) for c in schema_classes], + by_alias=by_alias, + ref_template=ref_template, + schema_generator=GenerateOpenAPI30Schema, + ) + else: + schema_definitions = v1_schema( + schema_classes, by_alias=by_alias, ref_prefix=ref_prefix + ) + + if not new_open_api.components: + new_open_api.components = Components() + if new_open_api.components.schemas: + for existing_key in new_open_api.components.schemas: + if existing_key in schema_definitions[DEFS_KEY]: + logger.warning( + f'"{existing_key}" already exists in {ref_prefix}. ' + f'The value of "{ref_prefix}{existing_key}" will be overwritten.' + ) + new_open_api.components.schemas.update(_validate_schemas(schema_definitions)) + else: + new_open_api.components.schemas = _validate_schemas(schema_definitions) + return new_open_api + + +def _validate_schemas( + schema_definitions: Dict[str, Any] +) -> Dict[str, Union[Reference, Schema]]: + """Convert JSON Schema definitions to parsed OpenAPI objects""" + # Note: if an error occurs in schema_validate(), it may indicate that + # the generated JSON schemas are not compatible with the version + # of OpenAPI this module depends on. + return { + key: schema_validate(schema_dict) + for key, schema_dict in schema_definitions[DEFS_KEY].items() + } + + +def _handle_pydantic_schema(open_api: OpenAPI) -> List[Type[BaseModel]]: + """ + This function traverses the `OpenAPI` object and + + 1. Replaces the `PydanticSchema` object with `Reference` object, with correct ref + value; + 2. Extracts the involved schema class from `PydanticSchema` object. + + **This function will mutate the input `OpenAPI` object.** + + :param open_api: the `OpenAPI` object to be traversed and mutated + :return: a list of schema classes extracted from `PydanticSchema` objects + """ + + pydantic_types: Set[Type[BaseModel]] = set() + + def _traverse(obj: Any) -> None: + if isinstance(obj, BaseModel): + fields = getattr( + obj, "model_fields_set" if PYDANTIC_V2 else "__fields_set__" + ) + for field in fields: + child_obj = obj.__getattribute__(field) + if isinstance(child_obj, PydanticSchema): + logger.debug("PydanticSchema found in %s: %s", obj, child_obj) + obj.__setattr__(field, _construct_ref_obj(child_obj)) + pydantic_types.add(child_obj.schema_class) + else: + _traverse(child_obj) + elif isinstance(obj, list): + for index, elem in enumerate(obj): + if isinstance(elem, PydanticSchema): + logger.debug(f"PydanticSchema found in list: {elem}") + obj[index] = _construct_ref_obj(elem) + pydantic_types.add(elem.schema_class) + else: + _traverse(elem) + elif isinstance(obj, dict): + for key, value in obj.items(): + if isinstance(value, PydanticSchema): + logger.debug(f"PydanticSchema found in dict: {value}") + obj[key] = _construct_ref_obj(value) + pydantic_types.add(value.schema_class) + else: + _traverse(value) + + _traverse(open_api) + return list(pydantic_types) + + +def _construct_ref_obj(pydantic_schema: PydanticSchema[PydanticType]) -> Reference: + """ + Construct a reference object from the Pydantic schema name + + characters in the schema name that are invalid/problematic + for JSONschema $ref names will get replaced with underscores. + Especially needed for Pydantic generic Models with brackets "[]" + + see: https://github.com/pydantic/pydantic/blob/aee6057378ccfec02126bf9c984a9b6d6b411777/pydantic/json_schema.py#L2031 + """ + ref_name = re.sub( + r"[^a-zA-Z0-9.\-_]", "_", pydantic_schema.schema_class.__name__ + ).replace(".", "__") + ref_obj = Reference(**{"$ref": ref_prefix + ref_name}) + logger.debug(f"ref_obj={ref_obj}") + return ref_obj diff --git a/openapi_pydantic/v3/v3_0/xml.py b/openapi_pydantic/v3/v3_0/xml.py new file mode 100644 index 0000000..25775bf --- /dev/null +++ b/openapi_pydantic/v3/v3_0/xml.py @@ -0,0 +1,68 @@ +from typing import Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [ + {"namespace": "http://example.com/schema/sample", "prefix": "sample"}, + {"name": "aliens", "wrapped": True}, +] + + +class XML(BaseModel): + """ + A metadata object that allows for more fine-tuned XML model definitions. + + When using arrays, XML element names are *not* inferred (for singular/plural forms) + and the `name` property SHOULD be used to add that information. + See examples for expected behavior. + """ + + name: Optional[str] = None + """ + Replaces the name of the element/attribute used for the described schema property. + When defined within `items`, it will affect the name of the individual XML elements + within the list. When defined alongside `type` being `array` (outside the `items`), + it will affect the wrapping element and only if `wrapped` is `true`. + If `wrapped` is `false`, it will be ignored. + """ + + namespace: Optional[str] = None + """ + The URI of the namespace definition. + Value MUST be in the form of an absolute URI. + """ + + prefix: Optional[str] = None + """ + The prefix to be used for the [name](#xmlName). + """ + + attribute: bool = False + """ + Declares whether the property definition translates to an attribute instead of an + element. Default value is `false`. + """ + + wrapped: bool = False + """ + MAY be used only for an array definition. + Signifies whether the array is wrapped (for example, + ``) or unwrapped (``). + Default value is `false`. + The definition takes effect only when defined alongside `type` being `array` + (outside the `items`). + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/README.md b/openapi_pydantic/v3/v3_1/README.md new file mode 100644 index 0000000..2232c46 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/README.md @@ -0,0 +1,41 @@ +# OpenAPI v3.1 schema classes + +## Alias + +Due to the reserved words in python and pydantic, +the following fields are used with [alias](https://pydantic-docs.helpmanual.io/usage/schema/#field-customisation) feature provided by pydantic: + +| Class | Field name in the class | Alias (as in OpenAPI spec) | +| ----- | ----------------------- | -------------------------- | +| Header[*](#header_param_in) | param_in | in | +| MediaType | media_type_schema | schema | +| Parameter | param_in | in | +| Parameter | param_schema | schema | +| PathItem | ref | $ref | +| Reference | ref | $ref | +| SecurityScheme | security_scheme_in | in | +| Schema | schema_format | format | +| Schema | schema_not | not | +| Schema | schema_if | if | +| Schema | schema_else | else | + +> The "in" field in Header object is actually a constant (`{"in": "header"}`). + +> For convenience of object creation, the classes mentioned in above +> have configured `allow_population_by_field_name=True` (Pydantic V1) or `populate_by_name=True` (Pydantic V2). +> +> Reference: [Pydantic's Model Config](https://pydantic-docs.helpmanual.io/usage/model_config/) + +## Non-pydantic schema types + +Due to the constriants of python typing structure (not able to handle dynamic field names), +the following schema classes are actually just a typing of `Dict`: + +| Schema Type | Implementation | +| ----------- | -------------- | +| Callback | `Callback = Dict[str, PathItem]` | +| Paths | `Paths = Dict[str, PathItem]` | +| Responses | `Responses = Dict[str, Union[Response, Reference]]` | +| SecurityRequirement | `SecurityRequirement = Dict[str, List[str]]` | + +On creating such schema instances, please use python's `dict` type instead to instantiate. diff --git a/openapi_pydantic/v3/v3_1/__init__.py b/openapi_pydantic/v3/v3_1/__init__.py new file mode 100644 index 0000000..0574504 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/__init__.py @@ -0,0 +1,59 @@ +""" +OpenAPI v3.1 schema types, created according to the specification: +https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md + +The type orders are according to the contents of the specification: +https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.1.md#table-of-contents +""" + +from typing import TYPE_CHECKING + +from openapi_pydantic.compat import PYDANTIC_V2 + +from .callback import Callback as Callback +from .components import Components as Components +from .contact import Contact as Contact +from .datatype import DataType as DataType +from .discriminator import Discriminator as Discriminator +from .encoding import Encoding as Encoding +from .example import Example as Example +from .external_documentation import ExternalDocumentation as ExternalDocumentation +from .header import Header as Header +from .info import Info as Info +from .license import License as License +from .link import Link as Link +from .media_type import MediaType as MediaType +from .oauth_flow import OAuthFlow as OAuthFlow +from .oauth_flows import OAuthFlows as OAuthFlows +from .open_api import OpenAPI as OpenAPI +from .operation import Operation as Operation +from .parameter import Parameter as Parameter +from .parameter import ParameterLocation as ParameterLocation +from .path_item import PathItem as PathItem +from .paths import Paths as Paths +from .reference import Reference as Reference +from .request_body import RequestBody as RequestBody +from .response import Response as Response +from .responses import Responses as Responses +from .schema import Schema as Schema +from .schema import schema_validate as schema_validate +from .security_requirement import SecurityRequirement as SecurityRequirement +from .security_scheme import SecurityScheme as SecurityScheme +from .server import Server as Server +from .server_variable import ServerVariable as ServerVariable +from .tag import Tag as Tag +from .xml import XML as XML + +if TYPE_CHECKING: + pass +elif PYDANTIC_V2: + # resolve forward references + Encoding.model_rebuild() + OpenAPI.model_rebuild() + Components.model_rebuild() + Operation.model_rebuild() +else: + # resolve forward references + Encoding.update_forward_refs(Header=Header) + Schema.update_forward_refs() + Operation.update_forward_refs(PathItem=PathItem) diff --git a/openapi_pydantic/v3/v3_1/callback.py b/openapi_pydantic/v3/v3_1/callback.py new file mode 100644 index 0000000..9b8b4b9 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/callback.py @@ -0,0 +1,26 @@ +from typing import TYPE_CHECKING, Dict, Union + +from .reference import Reference + +if TYPE_CHECKING: + from .path_item import PathItem + + +Callback = Dict[str, Union["PathItem", Reference]] +""" +A map of possible out-of band callbacks related to the parent operation. +Each value in the map is a [Path Item Object](#pathItemObject) +that describes a set of requests that may be initiated by the API provider and the +expected responses. The key value used to identify the path item object is an +expression, evaluated at runtime, that identifies a URL to use for the callback +operation. +""" + +"""Patterned Fields""" + +# {expression}: 'PathItem' = ... +""" +A Path Item Object used to define a callback request and expected responses. + +A [complete example](../examples/v3.0/callback-example.yaml) is available. +""" diff --git a/openapi_pydantic/v3/v3_1/components.py b/openapi_pydantic/v3/v3_1/components.py new file mode 100644 index 0000000..956ae03 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/components.py @@ -0,0 +1,142 @@ +from typing import Dict, Optional, Union + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .callback import Callback +from .example import Example +from .header import Header +from .link import Link +from .parameter import Parameter +from .path_item import PathItem +from .reference import Reference +from .request_body import RequestBody +from .response import Response +from .schema import Schema +from .security_scheme import SecurityScheme + +_examples = [ + { + "schemas": { + "GeneralError": { + "type": "object", + "properties": { + "code": {"type": "integer", "format": "int32"}, + "message": {"type": "string"}, + }, + }, + "Category": { + "type": "object", + "properties": { + "id": {"type": "integer", "format": "int64"}, + "name": {"type": "string"}, + }, + }, + "Tag": { + "type": "object", + "properties": { + "id": {"type": "integer", "format": "int64"}, + "name": {"type": "string"}, + }, + }, + }, + "parameters": { + "skipParam": { + "name": "skip", + "in": "query", + "description": "number of items to skip", + "required": True, + "schema": {"type": "integer", "format": "int32"}, + }, + "limitParam": { + "name": "limit", + "in": "query", + "description": "max records to return", + "required": True, + "schema": {"type": "integer", "format": "int32"}, + }, + }, + "responses": { + "NotFound": {"description": "Entity not found."}, + "IllegalInput": {"description": "Illegal input for operation."}, + "GeneralError": { + "description": "General Error", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/GeneralError"} + } + }, + }, + }, + "securitySchemes": { + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header", + }, + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "http://example.org/api/oauth/dialog", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + } + }, + }, + }, + } +] + + +class Components(BaseModel): + """ + Holds a set of reusable objects for different aspects of the OAS. + All objects defined within the components object will have no effect on the API + unless they are explicitly referenced from properties outside the components object. + """ + + schemas: Optional[Dict[str, Schema]] = None + """An object to hold reusable [Schema Objects](#schemaObject).""" + + responses: Optional[Dict[str, Union[Response, Reference]]] = None + """An object to hold reusable [Response Objects](#responseObject).""" + + parameters: Optional[Dict[str, Union[Parameter, Reference]]] = None + """An object to hold reusable [Parameter Objects](#parameterObject).""" + + examples: Optional[Dict[str, Union[Example, Reference]]] = None + """An object to hold reusable [Example Objects](#exampleObject).""" + + requestBodies: Optional[Dict[str, Union[RequestBody, Reference]]] = None + """An object to hold reusable [Request Body Objects](#requestBodyObject).""" + + headers: Optional[Dict[str, Union[Header, Reference]]] = None + """An object to hold reusable [Header Objects](#headerObject).""" + + securitySchemes: Optional[Dict[str, Union[SecurityScheme, Reference]]] = None + """An object to hold reusable [Security Scheme Objects](#securitySchemeObject).""" + + links: Optional[Dict[str, Union[Link, Reference]]] = None + """An object to hold reusable [Link Objects](#linkObject).""" + + callbacks: Optional[Dict[str, Union[Callback, Reference]]] = None + """An object to hold reusable [Callback Objects](#callbackObject).""" + + pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None + """An object to hold reusable [Path Item Object](#pathItemObject).""" + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/contact.py b/openapi_pydantic/v3/v3_1/contact.py new file mode 100644 index 0000000..5b11d3a --- /dev/null +++ b/openapi_pydantic/v3/v3_1/contact.py @@ -0,0 +1,48 @@ +from typing import Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [ + { + "name": "API Support", + "url": "http://www.example.com/support", + "email": "support@example.com", + } +] + + +class Contact(BaseModel): + """ + Contact information for the exposed API. + """ + + name: Optional[str] = None + """ + The identifying name of the contact person/organization. + """ + + url: Optional[str] = None + """ + The URL pointing to the contact information. + MUST be in the form of a URL. + """ + + email: Optional[str] = None + """ + The email address of the contact person/organization. + MUST be in the form of an email address. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/datatype.py b/openapi_pydantic/v3/v3_1/datatype.py new file mode 100644 index 0000000..6477eee --- /dev/null +++ b/openapi_pydantic/v3/v3_1/datatype.py @@ -0,0 +1,13 @@ +from enum import Enum + + +class DataType(str, Enum): + """Data type of an object.""" + + NULL = "null" + STRING = "string" + NUMBER = "number" + INTEGER = "integer" + BOOLEAN = "boolean" + ARRAY = "array" + OBJECT = "object" diff --git a/openapi_pydantic/v3/v3_1/discriminator.py b/openapi_pydantic/v3/v3_1/discriminator.py new file mode 100644 index 0000000..1348906 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/discriminator.py @@ -0,0 +1,52 @@ +from typing import Dict, Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [ + { + "propertyName": "petType", + "mapping": { + "dog": "#/components/schemas/Dog", + "monster": "https://gigantic-server.com/schemas/Monster/schema.json", + }, + } +] + + +class Discriminator(BaseModel): + """ + When request bodies or response payloads may be one of a number of different + schemas, a `discriminator` object can be used to aid in serialization, + deserialization, and validation. + + The discriminator is a specific object in a schema which is used to inform the + consumer of the specification of an alternative schema based on the value + associated with it. + + When using the discriminator, _inline_ schemas will not be considered. + """ + + propertyName: str + """ + **REQUIRED**. The name of the property in the payload that will hold the + discriminator value. + """ + + mapping: Optional[Dict[str, str]] = None + """ + An object to hold mappings between payload values and schema names or references. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/encoding.py b/openapi_pydantic/v3/v3_1/encoding.py new file mode 100644 index 0000000..580d3d1 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/encoding.py @@ -0,0 +1,101 @@ +from typing import TYPE_CHECKING, Dict, Optional, Union + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .reference import Reference + +if TYPE_CHECKING: + from .header import Header + +_examples = [ + { + "contentType": "image/png, image/jpeg", + "headers": { + "X-Rate-Limit-Limit": { + "description": "The number of allowed requests in the " + "current period", + "schema": {"type": "integer"}, + } + }, + } +] + + +class Encoding(BaseModel): + """A single encoding definition applied to a single schema property.""" + + contentType: Optional[str] = None + """ + The Content-Type for encoding a specific property. + Default value depends on the property type: + + for `object` - `application/json`; + for `array` – the default is defined based on the inner type; + for all other cases the default is `application/octet-stream`. + + The value can be a specific media type (e.g. `application/json`), a wildcard media + type (e.g. `image/*`), or a comma-separated list of the two types. + """ + + headers: Optional[Dict[str, Union["Header", Reference]]] = None + """ + A map allowing additional information to be provided as headers, for example + `Content-Disposition`. + + `Content-Type` is described separately and SHALL be ignored in this section. + This property SHALL be ignored if the request body media type is not a `multipart`. + """ + + style: Optional[str] = None + """ + Describes how a specific property value will be serialized depending on its type. + + See [Parameter Object](#parameterObject) for details on the + [`style`](#parameterStyle) property. The behavior follows the same values as + `query` parameters, including default values. + This property SHALL be ignored if the request body media type + is not `application/x-www-form-urlencoded` or `multipart/form-data`. + If a value is explicitly defined, then the value of + [`contentType`](#encodingContentType) (implicit or explicit) SHALL be ignored. + """ + + explode: Optional[bool] = None + """ + When this is true, property values of type `array` or `object` generate separate + parameters for each value of the array, or key-value-pair of the map. + + For other types of properties this property has no effect. + When [`style`](#encodingStyle) is `form`, the default value is `true`. + For all other styles, the default value is `false`. + This property SHALL be ignored if the request body media type + is not `application/x-www-form-urlencoded` or `multipart/form-data`. + If a value is explicitly defined, then the value of + [`contentType`](#encodingContentType) (implicit or explicit) SHALL be ignored. + """ + + allowReserved: bool = False + """ + Determines whether the parameter value SHOULD allow reserved characters, + as defined by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.2) + `:/?#[]@!$&'()*+,;=` to be included without percent-encoding. + The default value is `false`. + This property SHALL be ignored if the request body media type + is not `application/x-www-form-urlencoded` or `multipart/form-data`. + If a value is explicitly defined, + then the value of [`contentType`](#encodingContentType) (implicit or explicit) + SHALL be ignored. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/example.py b/openapi_pydantic/v3/v3_1/example.py new file mode 100644 index 0000000..7d4cf4e --- /dev/null +++ b/openapi_pydantic/v3/v3_1/example.py @@ -0,0 +1,64 @@ +from typing import Any, Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [ + { + "summary": "A foo example", + "value": {"foo": "bar"}, + }, + { + "summary": "This is an example in XML", + "externalValue": "http://example.org/examples/address-example.xml", + }, + { + "summary": "This is a text example", + "externalValue": "http://foo.bar/examples/address-example.txt", + }, +] + + +class Example(BaseModel): + summary: Optional[str] = None + """ + Short description for the example. + """ + + description: Optional[str] = None + """ + Long description for the example. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + value: Optional[Any] = None + """ + Embedded literal example. + The `value` field and `externalValue` field are mutually exclusive. + To represent examples of media types that cannot naturally represented in JSON or + YAML, use a string value to contain the example, escaping where necessary. + """ + + externalValue: Optional[str] = None + """ + A URL that points to the literal example. + This provides the capability to reference examples that cannot easily be included + in JSON or YAML documents. + + The `value` field and `externalValue` field are mutually exclusive. + See the rules for resolving [Relative References](#relativeReferencesURI). + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/external_documentation.py b/openapi_pydantic/v3/v3_1/external_documentation.py new file mode 100644 index 0000000..b88e8b2 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/external_documentation.py @@ -0,0 +1,36 @@ +from typing import Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [{"description": "Find more info here", "url": "https://example.com"}] + + +class ExternalDocumentation(BaseModel): + """Allows referencing an external resource for extended documentation.""" + + description: Optional[str] = None + """ + A short description of the target documentation. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + url: str + """ + **REQUIRED**. The URL for the target documentation. + Value MUST be in the form of a URL. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/header.py b/openapi_pydantic/v3/v3_1/header.py new file mode 100644 index 0000000..451d77e --- /dev/null +++ b/openapi_pydantic/v3/v3_1/header.py @@ -0,0 +1,37 @@ +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .parameter import ParameterBase + +_examples = [ + { + "description": "The number of allowed requests in the current period", + "schema": {"type": "integer"}, + } +] + + +class Header(ParameterBase): + """ + The Header Object follows the structure of the + [Parameter Object](#parameterObject) with the following changes: + + 1. `name` MUST NOT be specified, it is given in the corresponding + `headers` map. + 2. `in` MUST NOT be specified, it is implicitly in `header`. + 3. All traits that are affected by the location MUST be applicable + to a location of `header` (for example, [`style`](#parameterStyle)). + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + allow_population_by_field_name = True + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/info.py b/openapi_pydantic/v3/v3_1/info.py new file mode 100644 index 0000000..f27ef12 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/info.py @@ -0,0 +1,87 @@ +from typing import Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .contact import Contact +from .license import License + +_examples = [ + { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "http://example.com/terms/", + "contact": { + "name": "API Support", + "url": "http://www.example.com/support", + "email": "support@example.com", + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html", + }, + "version": "1.0.1", + } +] + + +class Info(BaseModel): + """ + The object provides metadata about the API. + The metadata MAY be used by the clients if needed, + and MAY be presented in editing or documentation generation tools for convenience. + """ + + title: str + """ + **REQUIRED**. The title of the API. + """ + + summary: Optional[str] = None + """ + A short summary of the API. + """ + + description: Optional[str] = None + """ + A description of the API. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + termsOfService: Optional[str] = None + """ + A URL to the Terms of Service for the API. + MUST be in the form of a URL. + """ + + contact: Optional[Contact] = None + """ + The contact information for the exposed API. + """ + + license: Optional[License] = None + """ + The license information for the exposed API. + """ + + version: str + """ + **REQUIRED**. The version of the OpenAPI document + (which is distinct from the [OpenAPI Specification version](#oasVersion) or the API + implementation version). + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/license.py b/openapi_pydantic/v3/v3_1/license.py new file mode 100644 index 0000000..58d103e --- /dev/null +++ b/openapi_pydantic/v3/v3_1/license.py @@ -0,0 +1,50 @@ +from typing import Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [ + {"name": "Apache 2.0", "identifier": "Apache-2.0"}, + { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html", + }, +] + + +class License(BaseModel): + """ + License information for the exposed API. + """ + + name: str + """ + **REQUIRED**. The license name used for the API. + """ + + identifier: Optional[str] = None + """ + An [SPDX](https://spdx.org/spdx-specification-21-web-version#h.jxpfx0ykyb60) + license expression for the API. The `identifier` field is mutually exclusive of the + `url` field. + """ + + url: Optional[str] = None + """ + A URL to the license used for the API. + This MUST be in the form of a URL. + The `url` field is mutually exclusive of the `identifier` field. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/link.py b/openapi_pydantic/v3/v3_1/link.py new file mode 100644 index 0000000..c6dec93 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/link.py @@ -0,0 +1,95 @@ +from typing import Any, Dict, Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .server import Server + +_examples = [ + { + "operationId": "getUserAddressByUUID", + "parameters": {"userUuid": "$response.body#/uuid"}, + }, + { + "operationRef": "#/paths/~12.0~1repositories~1{username}/get", + "parameters": {"username": "$response.body#/username"}, + }, +] + + +class Link(BaseModel): + """ + The `Link object` represents a possible design-time link for a response. + The presence of a link does not guarantee the caller's ability to successfully + invoke it, rather it provides a known relationship and traversal mechanism between + responses and other operations. + + Unlike _dynamic_ links (i.e. links provided **in** the response payload), + the OAS linking mechanism does not require link information in the runtime response. + + For computing links, and providing instructions to execute them, + a [runtime expression](#runtimeExpression) is used for accessing values in an + operation and using them as parameters while invoking the linked operation. + """ + + operationRef: Optional[str] = None + """ + A relative or absolute URI reference to an OAS operation. + This field is mutually exclusive of the `operationId` field, + and MUST point to an [Operation Object](#operationObject). + Relative `operationRef` values MAY be used to locate an existing + [Operation Object](#operationObject) in the OpenAPI definition. See the rules for + resolving [Relative References](#relativeReferencesURI). + """ + + operationId: Optional[str] = None + """ + The name of an _existing_, resolvable OAS operation, as defined with a unique + `operationId`. + + This field is mutually exclusive of the `operationRef` field. + """ + + parameters: Optional[Dict[str, Any]] = None + """ + A map representing parameters to pass to an operation + as specified with `operationId` or identified via `operationRef`. + The key is the parameter name to be used, + whereas the value can be a constant or an expression to be evaluated and passed to + the linked operation. + + The parameter name can be qualified using the [parameter location](#parameterIn) + `[{in}.]{name}` for operations that use the same parameter name in different + locations (e.g. path.id). + """ + + requestBody: Optional[Any] = None + """ + A literal value or [{expression}](#runtimeExpression) to use as a request body when + calling the target operation. + """ + + description: Optional[str] = None + """ + A description of the link. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + server: Optional[Server] = None + """ + A server object to be used by the target operation. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/media_type.py b/openapi_pydantic/v3/v3_1/media_type.py new file mode 100644 index 0000000..0953596 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/media_type.py @@ -0,0 +1,97 @@ +from typing import Any, Dict, Optional, Union + +from pydantic import BaseModel, Field + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .encoding import Encoding +from .example import Example +from .reference import Reference +from .schema import Schema + +_examples = [ + { + "schema": {"$ref": "#/components/schemas/Pet"}, + "examples": { + "cat": { + "summary": "An example of a cat", + "value": { + "name": "Fluffy", + "petType": "Cat", + "color": "White", + "gender": "male", + "breed": "Persian", + }, + }, + "dog": { + "summary": "An example of a dog with a cat's name", + "value": { + "name": "Puma", + "petType": "Dog", + "color": "Black", + "gender": "Female", + "breed": "Mixed", + }, + }, + "frog": {"$ref": "#/components/examples/frog-example"}, + }, + } +] + + +class MediaType(BaseModel): + """Each Media Type Object provides schema and examples for the media type + identified by its key.""" + + media_type_schema: Optional[Union[Reference, Schema]] = Field( + default=None, alias="schema" + ) + """ + The schema defining the content of the request, response, or parameter. + """ + + example: Optional[Any] = None + """ + Example of the media type. + + The example object SHOULD be in the correct format as specified by the media type. + + The `example` field is mutually exclusive of the `examples` field. + + Furthermore, if referencing a `schema` which contains an example, + the `example` value SHALL _override_ the example provided by the schema. + """ + + examples: Optional[Dict[str, Union[Example, Reference]]] = None + """ + Examples of the media type. + + Each example object SHOULD match the media type and specified schema if present. + + The `examples` field is mutually exclusive of the `example` field. + + Furthermore, if referencing a `schema` which contains an example, + the `examples` value SHALL _override_ the example provided by the schema. + """ + + encoding: Optional[Dict[str, Encoding]] = None + """ + A map between a property name and its encoding information. + The key, being the property name, MUST exist in the schema as a property. + The encoding object SHALL only apply to `requestBody` objects + when the media type is `multipart` or `application/x-www-form-urlencoded`. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + allow_population_by_field_name = True + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/oauth_flow.py b/openapi_pydantic/v3/v3_1/oauth_flow.py new file mode 100644 index 0000000..acf741e --- /dev/null +++ b/openapi_pydantic/v3/v3_1/oauth_flow.py @@ -0,0 +1,80 @@ +from typing import Dict, Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [ + { + "authorizationUrl": "https://example.com/api/oauth/dialog", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + }, + { + "authorizationUrl": "https://example.com/api/oauth/dialog", + "tokenUrl": "https://example.com/api/oauth/token", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + }, + { + "authorizationUrl": "/api/oauth/dialog", + "tokenUrl": "/api/oauth/token", + "refreshUrl": "/api/oauth/token", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + }, +] + + +class OAuthFlow(BaseModel): + """ + Configuration details for a supported OAuth Flow + """ + + authorizationUrl: Optional[str] = None + """ + **REQUIRED** for `oauth2 ("implicit", "authorizationCode")`. + The authorization URL to be used for this flow. + This MUST be in the form of a URL. + The OAuth2 standard requires the use of TLS. + """ + + tokenUrl: Optional[str] = None + """ + **REQUIRED** for `oauth2 ("password", "clientCredentials", "authorizationCode")`. + The token URL to be used for this flow. + This MUST be in the form of a URL. + The OAuth2 standard requires the use of TLS. + """ + + refreshUrl: Optional[str] = None + """ + The URL to be used for obtaining refresh tokens. + This MUST be in the form of a URL. + The OAuth2 standard requires the use of TLS. + """ + + scopes: Optional[Dict[str, str]] = None + """ + **REQUIRED** for `oauth2`. The available scopes for the OAuth2 security scheme. + A map between the scope name and a short description for it. + The map MAY be empty. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/oauth_flows.py b/openapi_pydantic/v3/v3_1/oauth_flows.py new file mode 100644 index 0000000..c8a8e5a --- /dev/null +++ b/openapi_pydantic/v3/v3_1/oauth_flows.py @@ -0,0 +1,47 @@ +from typing import Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .oauth_flow import OAuthFlow + + +class OAuthFlows(BaseModel): + """ + Allows configuration of the supported OAuth Flows. + """ + + implicit: Optional[OAuthFlow] = None + """ + Configuration for the OAuth Implicit flow + """ + + password: Optional[OAuthFlow] = None + """ + Configuration for the OAuth Resource Owner Password flow + """ + + clientCredentials: Optional[OAuthFlow] = None + """ + Configuration for the OAuth Client Credentials flow. + + Previously called `application` in OpenAPI 2.0. + """ + + authorizationCode: Optional[OAuthFlow] = None + """ + Configuration for the OAuth Authorization Code flow. + + Previously called `accessCode` in OpenAPI 2.0. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + ) + + else: + + class Config: + extra = Extra.allow diff --git a/openapi_pydantic/v3/v3_1/open_api.py b/openapi_pydantic/v3/v3_1/open_api.py new file mode 100644 index 0000000..3427dc5 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/open_api.py @@ -0,0 +1,104 @@ +from typing import Dict, List, Literal, Optional, Union + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .components import Components +from .external_documentation import ExternalDocumentation +from .info import Info +from .path_item import PathItem +from .paths import Paths +from .reference import Reference +from .security_requirement import SecurityRequirement +from .server import Server +from .tag import Tag + + +class OpenAPI(BaseModel): + """This is the root document object of the OpenAPI document.""" + + openapi: Literal["3.1.1", "3.1.0"] = "3.1.1" + """ + **REQUIRED**. This string MUST be the [version number](#versions) + of the OpenAPI Specification that the OpenAPI document uses. + The `openapi` field SHOULD be used by tooling to interpret the OpenAPI document. + This is *not* related to the API [`info.version`](#infoVersion) string. + """ + + info: Info + """ + **REQUIRED**. Provides metadata about the API. The metadata MAY be used by tooling + as required. + """ + + jsonSchemaDialect: Optional[str] = None + """ + The default value for the `$schema` keyword within [Schema Objects](#schemaObject) + contained within this OAS document. This MUST be in the form of a URI. + """ + + servers: List[Server] = [Server(url="/")] + """ + An array of Server Objects, which provide connectivity information to a target + server. If the `servers` property is not provided, or is an empty array, + the default value would be a [Server Object](#serverObject) with a + [url](#serverUrl) value of `/`. + """ + + paths: Optional[Paths] = None + """ + The available paths and operations for the API. + """ + + webhooks: Optional[Dict[str, Union[PathItem, Reference]]] = None + """ + The incoming webhooks that MAY be received as part of this API and that the API + consumer MAY choose to implement. + Closely related to the `callbacks` feature, this section describes requests + initiated other than by an API call, + for example by an out of band registration. + The key name is a unique string to refer to each webhook, + while the (optionally referenced) Path Item Object describes a request + that may be initiated by the API provider and the expected responses. + An [example](../examples/v3.1/webhook-example.yaml) is available. + """ + + components: Optional[Components] = None + """ + An element to hold various schemas for the document. + """ + + security: Optional[List[SecurityRequirement]] = None + """ + A declaration of which security mechanisms can be used across the API. + The list of values includes alternative security requirement objects that can be + used. Only one of the security requirement objects need to be satisfied to + authorize a request. Individual operations can override this definition. + To make security optional, an empty security requirement (`{}`) can be included in + the array. + """ + + tags: Optional[List[Tag]] = None + """ + A list of tags used by the document with additional metadata. + The order of the tags can be used to reflect on their order by the parsing tools. + Not all tags that are used by the [Operation Object](#operationObject) must be + declared. The tags that are not declared MAY be organized randomly or based on the + tools' logic. Each tag name in the list MUST be unique. + """ + + externalDocs: Optional[ExternalDocumentation] = None + """ + Additional external documentation. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + ) + + else: + + class Config: + extra = Extra.allow diff --git a/openapi_pydantic/v3/v3_1/operation.py b/openapi_pydantic/v3/v3_1/operation.py new file mode 100644 index 0000000..5b721a7 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/operation.py @@ -0,0 +1,176 @@ +from typing import Dict, List, Optional, Union + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .callback import Callback +from .external_documentation import ExternalDocumentation +from .parameter import Parameter +from .reference import Reference +from .request_body import RequestBody +from .responses import Responses +from .security_requirement import SecurityRequirement +from .server import Server + +_examples = [ + { + "tags": ["pet"], + "summary": "Updates a pet in the store with form data", + "operationId": "updatePetWithForm", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": True, + "schema": {"type": "string"}, + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "name": { + "description": "Updated name of the pet", + "type": "string", + }, + "status": { + "description": "Updated status of the pet", + "type": "string", + }, + }, + "required": ["status"], + } + } + } + }, + "responses": { + "200": { + "description": "Pet updated.", + "content": {"application/json": {}, "application/xml": {}}, + }, + "405": { + "description": "Method Not Allowed", + "content": {"application/json": {}, "application/xml": {}}, + }, + }, + "security": [{"petstore_auth": ["write:pets", "read:pets"]}], + } +] + + +class Operation(BaseModel): + """Describes a single API operation on a path.""" + + tags: Optional[List[str]] = None + """ + A list of tags for API documentation control. + Tags can be used for logical grouping of operations by resources or any other + qualifier. + """ + + summary: Optional[str] = None + """ + A short summary of what the operation does. + """ + + description: Optional[str] = None + """ + A verbose explanation of the operation behavior. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + externalDocs: Optional[ExternalDocumentation] = None + """ + Additional external documentation for this operation. + """ + + operationId: Optional[str] = None + """ + Unique string used to identify the operation. + The id MUST be unique among all operations described in the API. + The operationId value is **case-sensitive**. + Tools and libraries MAY use the operationId to uniquely identify an operation, + therefore, it is RECOMMENDED to follow common programming naming conventions. + """ + + parameters: Optional[List[Union[Parameter, Reference]]] = None + """ + A list of parameters that are applicable for this operation. + If a parameter is already defined at the [Path Item](#pathItemParameters), + the new definition will override it but can never remove it. + The list MUST NOT include duplicated parameters. + A unique parameter is defined by a combination of a [name](#parameterName) and + [location](#parameterIn). The list can use the [Reference Object](#referenceObject) + to link to parameters that are defined at the + [OpenAPI Object's components/parameters](#componentsParameters). + """ + + requestBody: Optional[Union[RequestBody, Reference]] = None + """ + The request body applicable for this operation. + + The `requestBody` is fully supported in HTTP methods where the HTTP 1.1 + specification [RFC7231](https://tools.ietf.org/html/rfc7231#section-4.3.1) has + explicitly defined semantics for request bodies. + In other cases where the HTTP spec is vague (such as [GET](https://tools.ietf.org/html/rfc7231#section-4.3.1), + [HEAD](https://tools.ietf.org/html/rfc7231#section-4.3.2) + and [DELETE](https://tools.ietf.org/html/rfc7231#section-4.3.5)), + `requestBody` is permitted but does not have well-defined semantics and SHOULD be + avoided if possible. + """ + + responses: Optional[Responses] = None + """ + The list of possible responses as they are returned from executing this operation. + """ + + callbacks: Optional[Dict[str, Union[Callback, Reference]]] = None + """ + A map of possible out-of band callbacks related to the parent operation. + The key is a unique identifier for the Callback Object. + Each value in the map is a [Callback Object](#callbackObject) + that describes a request that may be initiated by the API provider and the expected + responses. + """ + + deprecated: bool = False + """ + Declares this operation to be deprecated. + Consumers SHOULD refrain from usage of the declared operation. + Default value is `false`. + """ + + security: Optional[List[SecurityRequirement]] = None + """ + A declaration of which security mechanisms can be used for this operation. + The list of values includes alternative security requirement objects that can be + used. Only one of the security requirement objects need to be satisfied to + authorize a request. To make security optional, an empty security requirement + (`{}`) can be included in the array. This definition overrides any declared + top-level [`security`](#oasSecurity). To remove a top-level security declaration, + an empty array can be used. + """ + + servers: Optional[List[Server]] = None + """ + An alternative `server` array to service this operation. + If an alternative `server` object is specified at the Path Item Object or Root + level, it will be overridden by this value. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/parameter.py b/openapi_pydantic/v3/v3_1/parameter.py new file mode 100644 index 0000000..1c7afb5 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/parameter.py @@ -0,0 +1,235 @@ +import enum +from typing import Any, Dict, Optional, Union + +from pydantic import BaseModel, Field + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .example import Example +from .media_type import MediaType +from .reference import Reference +from .schema import Schema + +_examples = [ + { + "name": "token", + "in": "header", + "description": "token to be passed as a header", + "required": True, + "schema": { + "type": "array", + "items": {"type": "integer", "format": "int64"}, + }, + "style": "simple", + }, + { + "name": "username", + "in": "path", + "description": "username to fetch", + "required": True, + "schema": {"type": "string"}, + }, + { + "name": "id", + "in": "query", + "description": "ID of the object to fetch", + "required": False, + "schema": {"type": "array", "items": {"type": "string"}}, + "style": "form", + "explode": True, + }, + { + "in": "query", + "name": "freeForm", + "schema": { + "type": "object", + "additionalProperties": {"type": "integer"}, + }, + "style": "form", + }, + { + "in": "query", + "name": "coordinates", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["lat", "long"], + "properties": { + "lat": {"type": "number"}, + "long": {"type": "number"}, + }, + } + } + }, + }, +] + + +class ParameterLocation(str, enum.Enum): + """The location of a given parameter.""" + + QUERY = "query" + HEADER = "header" + PATH = "path" + COOKIE = "cookie" + + +class ParameterBase(BaseModel): + """ + Base class for Parameter and Header. + + (Header is like Parameter, but has no `name` or `in` fields.) + """ + + description: Optional[str] = None + """ + A brief description of the parameter. + This could contain examples of use. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + required: bool = False + """ + Determines whether this parameter is mandatory. + If the [parameter location](#parameterIn) is `"path"`, this property is + **REQUIRED** and its value MUST be `true`. + Otherwise, the property MAY be included and its default value is `false`. + """ + + deprecated: bool = False + """ + Specifies that a parameter is deprecated and SHOULD be transitioned out of usage. + Default value is `false`. + """ + + style: Optional[str] = None + """ + Describes how the parameter value will be serialized depending on the type of the + parameter value. Default values (based on value of `in`): + + - for `query` - `form`; + - for `path` - `simple`; + - for `header` - `simple`; + - for `cookie` - `form`. + """ + + explode: Optional[bool] = None + """ + When this is true, parameter values of type `array` or `object` generate separate + parameters for each value of the array or key-value pair of the map. + For other types of parameters this property has no effect. + When [`style`](#parameterStyle) is `form`, the default value is `true`. + For all other styles, the default value is `false`. + """ + + param_schema: Optional[Union[Schema, Reference]] = Field( + default=None, alias="schema" + ) + """ + The schema defining the type used for the parameter. + """ + + example: Optional[Any] = None + """ + Example of the parameter's potential value. + The example SHOULD match the specified schema and encoding properties if present. + The `example` field is mutually exclusive of the `examples` field. + Furthermore, if referencing a `schema` that contains an example, + the `example` value SHALL _override_ the example provided by the schema. + To represent examples of media types that cannot naturally be represented in JSON + or YAML, a string value can contain the example with escaping where necessary. + """ + + examples: Optional[Dict[str, Union[Example, Reference]]] = None + """ + Examples of the parameter's potential value. + Each example SHOULD contain a value in the correct format as specified in the + parameter encoding. The `examples` field is mutually exclusive of the `example` + field. + Furthermore, if referencing a `schema` that contains an example, + the `examples` value SHALL _override_ the example provided by the schema. + """ + + """ + For more complex scenarios, the [`content`](#parameterContent) property + can define the media type and schema of the parameter. + A parameter MUST contain either a `schema` property, or a `content` property, but + not both. + When `example` or `examples` are provided in conjunction with the `schema` object, + the example MUST follow the prescribed serialization strategy for the parameter. + """ + + content: Optional[Dict[str, MediaType]] = None + """ + A map containing the representations for the parameter. + The key is the media type and the value describes it. + The map MUST only contain one entry. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + allow_population_by_field_name = True + schema_extra = {"examples": _examples} + + +class Parameter(ParameterBase): + """ + Describes a single operation parameter. + + A unique parameter is defined by a combination of a [name](#parameterName) and + [location](#parameterIn). + """ + + """Fixed Fields""" + + name: str + """ + **REQUIRED**. The name of the parameter. + Parameter names are *case sensitive*. + + - If [`in`](#parameterIn) is `"path"`, the `name` field MUST correspond to a + template expression occurring within the [path](#pathsPath) field in the + [Paths Object](#pathsObject). + See [Path Templating](#pathTemplating) for further information. + - If [`in`](#parameterIn) is `"header"` and the `name` field is `"Accept"`, + `"Content-Type"` or `"Authorization"`, the parameter definition SHALL be ignored. + - For all other cases, the `name` corresponds to the parameter name used by the + [`in`](#parameterIn) property. + """ + + param_in: ParameterLocation = Field(alias="in") + """ + **REQUIRED**. The location of the parameter. Possible values are `"query"`, + `"header"`, `"path"` or `"cookie"`. + """ + + allowEmptyValue: bool = False + """ + Sets the ability to pass empty-valued parameters. + This is valid only for `query` parameters and allows sending a parameter with an + empty value. Default value is `false`. + If [`style`](#parameterStyle) is used, and if behavior is `n/a` (cannot be + serialized), the value of `allowEmptyValue` SHALL be ignored. + Use of this property is NOT RECOMMENDED, as it is likely to be removed in a later + revision. + """ + + allowReserved: bool = False + """ + Determines whether the parameter value SHOULD allow reserved characters, + as defined by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.2) + `:/?#[]@!$&'()*+,;=` to be included without percent-encoding. + This property only applies to parameters with an `in` value of `query`. + The default value is `false`. + """ diff --git a/openapi_pydantic/v3/v3_1/path_item.py b/openapi_pydantic/v3/v3_1/path_item.py new file mode 100644 index 0000000..aa5b1d6 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/path_item.py @@ -0,0 +1,153 @@ +from typing import List, Optional, Union + +from pydantic import BaseModel, Field + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .operation import Operation +from .parameter import Parameter +from .reference import Reference +from .server import Server + +_examples = [ + { + "get": { + "description": "Returns pets based on ID", + "summary": "Find pets by ID", + "operationId": "getPetsById", + "responses": { + "200": { + "description": "pet response", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": {"$ref": "#/components/schemas/Pet"}, + } + } + }, + }, + "default": { + "description": "error payload", + "content": { + "text/html": { + "schema": {"$ref": "#/components/schemas/ErrorModel"} + } + }, + }, + }, + }, + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to use", + "required": True, + "schema": {"type": "array", "items": {"type": "string"}}, + "style": "simple", + } + ], + } +] + + +class PathItem(BaseModel): + """ + Describes the operations available on a single path. + A Path Item MAY be empty, due to [ACL constraints](#securityFiltering). + The path itself is still exposed to the documentation viewer + but they will not know which operations and parameters are available. + """ + + ref: Optional[str] = Field(default=None, alias="$ref") + """ + Allows for an external definition of this path item. + The referenced structure MUST be in the format of a + [Path Item Object](#pathItemObject). + + In case a Path Item Object field appears both in the defined object and the + referenced object, the behavior is undefined. + See the rules for resolving [Relative References](#relativeReferencesURI). + """ + + summary: Optional[str] = None + """ + An optional, string summary, intended to apply to all operations in this path. + """ + + description: Optional[str] = None + """ + An optional, string description, intended to apply to all operations in this path. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + get: Optional[Operation] = None + """ + A definition of a GET operation on this path. + """ + + put: Optional[Operation] = None + """ + A definition of a PUT operation on this path. + """ + + post: Optional[Operation] = None + """ + A definition of a POST operation on this path. + """ + + delete: Optional[Operation] = None + """ + A definition of a DELETE operation on this path. + """ + + options: Optional[Operation] = None + """ + A definition of a OPTIONS operation on this path. + """ + + head: Optional[Operation] = None + """ + A definition of a HEAD operation on this path. + """ + + patch: Optional[Operation] = None + """ + A definition of a PATCH operation on this path. + """ + + trace: Optional[Operation] = None + """ + A definition of a TRACE operation on this path. + """ + + servers: Optional[List[Server]] = None + """ + An alternative `server` array to service all operations in this path. + """ + + parameters: Optional[List[Union[Parameter, Reference]]] = None + """ + A list of parameters that are applicable for all the operations described under + this path. These parameters can be overridden at the operation level, but cannot be + removed there. The list MUST NOT include duplicated parameters. + A unique parameter is defined by a combination of a [name](#parameterName) and + [location](#parameterIn). The list can use the [Reference Object](#referenceObject) + to link to parameters that are defined at the + [OpenAPI Object's components/parameters](#componentsParameters). + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + allow_population_by_field_name = True + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/paths.py b/openapi_pydantic/v3/v3_1/paths.py new file mode 100644 index 0000000..8e2bbd9 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/paths.py @@ -0,0 +1,28 @@ +from typing import Dict + +from .path_item import PathItem + +Paths = Dict[str, PathItem] +""" +Holds the relative paths to the individual endpoints and their operations. +The path is appended to the URL from the [`Server Object`](#serverObject) in order to +construct the full URL. + +The Paths MAY be empty, due to +[Access Control List (ACL) constraints](#securityFiltering). +""" + +"""Patterned Fields""" + +# "/{path}" : PathItem +""" +A relative path to an individual endpoint. +The field name MUST begin with a forward slash (`/`). +The path is **appended** (no relative URL resolution) to the expanded URL +from the [`Server Object`](#serverObject)'s `url` field in order to construct the full +URL. [Path templating](#pathTemplating) is allowed. +When matching URLs, concrete (non-templated) paths would be matched before their +templated counterparts. Templated paths with the same hierarchy but different templated +names MUST NOT exist as they are identical. In case of ambiguous matching, it's up to +the tooling to decide which one to use. +""" diff --git a/openapi_pydantic/v3/v3_1/reference.py b/openapi_pydantic/v3/v3_1/reference.py new file mode 100644 index 0000000..9c75cab --- /dev/null +++ b/openapi_pydantic/v3/v3_1/reference.py @@ -0,0 +1,55 @@ +from typing import Optional + +from pydantic import BaseModel, Field + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [ + {"$ref": "#/components/schemas/Pet"}, + {"$ref": "Pet.json"}, + {"$ref": "definitions.json#/Pet"}, +] + + +class Reference(BaseModel): + """ + A simple object to allow referencing other components in the OpenAPI document. + + The `$ref` string value contains a URI [RFC3986](https://tools.ietf.org/html/rfc3986), + which identifies the location of the value being referenced. + + See the rules for resolving [Relative References](#relativeReferencesURI). + """ + + ref: str = Field(alias="$ref") + """**REQUIRED**. The reference identifier. This MUST be in the form of a URI.""" + + summary: Optional[str] = None + """ + A short summary which by default SHOULD override that of the referenced component. + If the referenced object-type does not allow a `summary` field, then this field has + no effect. + """ + + description: Optional[str] = None + """ + A description which by default SHOULD override that of the referenced component. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + If the referenced object-type does not allow a `description` field, then this field + has no effect. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + allow_population_by_field_name = True + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/request_body.py b/openapi_pydantic/v3/v3_1/request_body.py new file mode 100644 index 0000000..1741a5a --- /dev/null +++ b/openapi_pydantic/v3/v3_1/request_body.py @@ -0,0 +1,95 @@ +from typing import Dict, Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .media_type import MediaType + +_examples = [ + { + "description": "user to add to the system", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"}, + "examples": { + "user": { + "summary": "User Example", + "externalValue": "http://foo.bar/examples/user-example.json", + } + }, + }, + "application/xml": { + "schema": {"$ref": "#/components/schemas/User"}, + "examples": { + "user": { + "summary": "User example in XML", + "externalValue": "http://foo.bar/examples/user-example.xml", + } + }, + }, + "text/plain": { + "examples": { + "user": { + "summary": "User example in Plain text", + "externalValue": "http://foo.bar/examples/user-example.txt", + } + } + }, + "*/*": { + "examples": { + "user": { + "summary": "User example in other format", + "externalValue": "http://foo.bar/examples/user-example.whatever", + } + } + }, + }, + }, + { + "description": "user to add to the system", + "content": { + "text/plain": {"schema": {"type": "array", "items": {"type": "string"}}} + }, + }, +] + + +class RequestBody(BaseModel): + """Describes a single request body.""" + + description: Optional[str] = None + """ + A brief description of the request body. + This could contain examples of use. + + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + content: Dict[str, MediaType] + """ + **REQUIRED**. The content of the request body. + The key is a media type or [media type range](https://tools.ietf.org/html/rfc7231#appendix-D) + and the value describes it. + + For requests that match multiple keys, only the most specific key is applicable. + e.g. text/plain overrides text/* + """ + + required: bool = False + """ + Determines if the request body is required in the request. Defaults to `false`. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/response.py b/openapi_pydantic/v3/v3_1/response.py new file mode 100644 index 0000000..fa20933 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/response.py @@ -0,0 +1,100 @@ +from typing import Dict, Optional, Union + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .header import Header +from .link import Link +from .media_type import MediaType +from .reference import Reference + +_examples = [ + { + "description": "A complex object array response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": {"$ref": "#/components/schemas/VeryComplexType"}, + } + } + }, + }, + { + "description": "A simple string response", + "content": {"text/plain": {"schema": {"type": "string"}}}, + }, + { + "description": "A simple string response", + "content": {"text/plain": {"schema": {"type": "string", "example": "whoa!"}}}, + "headers": { + "X-Rate-Limit-Limit": { + "description": "The number of allowed requests in the " + "current period", + "schema": {"type": "integer"}, + }, + "X-Rate-Limit-Remaining": { + "description": "The number of remaining requests in the " + "current period", + "schema": {"type": "integer"}, + }, + "X-Rate-Limit-Reset": { + "description": "The number of seconds left in the current period", + "schema": {"type": "integer"}, + }, + }, + }, + {"description": "object created"}, +] + + +class Response(BaseModel): + """ + Describes a single response from an API Operation, including design-time, + static `links` to operations based on the response. + """ + + description: str + """ + **REQUIRED**. A short description of the response. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + headers: Optional[Dict[str, Union[Header, Reference]]] = None + """ + Maps a header name to its definition. + [RFC7230](https://tools.ietf.org/html/rfc7230#page-22) states header names are case + insensitive. + If a response header is defined with the name `"Content-Type"`, it SHALL be ignored. + """ + + content: Optional[Dict[str, MediaType]] = None + """ + A map containing descriptions of potential response payloads. + The key is a media type or [media type range](https://tools.ietf.org/html/rfc7231#appendix-D) + and the value describes it. + + For responses that match multiple keys, only the most specific key is applicable. + e.g. text/plain overrides text/* + """ + + links: Optional[Dict[str, Union[Link, Reference]]] = None + """ + A map of operations links that can be followed from the response. + The key of the map is a short name for the link, following the naming constraints + of the names for [Component Objects](#componentsObject). + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/responses.py b/openapi_pydantic/v3/v3_1/responses.py new file mode 100644 index 0000000..5c9a78a --- /dev/null +++ b/openapi_pydantic/v3/v3_1/responses.py @@ -0,0 +1,52 @@ +from typing import Dict, Union + +from .reference import Reference +from .response import Response + +Responses = Dict[str, Union[Response, Reference]] +""" +A container for the expected responses of an operation. +The container maps a HTTP response code to the expected response. + +The documentation is not necessarily expected to cover all possible HTTP response codes +because they may not be known in advance. +However, documentation is expected to cover a successful operation response and any +known errors. + +The `default` MAY be used as a default response object for all HTTP codes +that are not covered individually by the specification. + +The `Responses Object` MUST contain at least one response code, and it +SHOULD be the response for a successful operation call. +""" + +"""Fixed Fields""" + +# default: Optional[Union[Response, Reference]] +""" +The documentation of responses other than the ones declared for specific HTTP response +codes. +Use this field to cover undeclared responses. +A [Reference Object](#referenceObject) can link to a response +that the [OpenAPI Object's components/responses](#componentsResponses) section defines. +""" + +"""Patterned Fields""" +# {httpStatusCode}: Optional[Union[Response, Reference]] +""" +Any [HTTP status code](#httpCodes) can be used as the property name, +but only one property per code, to describe the expected response for that HTTP status +code. + +A [Reference Object](#referenceObject) can link to a response +that is defined in the [OpenAPI Object's components/responses](#componentsResponses) +section. +This field MUST be enclosed in quotation marks (for example, "200") for compatibility +between JSON and YAML. +To define a range of response codes, this field MAY contain the uppercase wildcard +character `X`. +For example, `2XX` represents all response codes between `[200-299]`. +Only the following range definitions are allowed: `1XX`, `2XX`, `3XX`, `4XX`, and `5XX`. +If a response is defined using an explicit code, +the explicit code definition takes precedence over the range definition for that code. +""" diff --git a/openapi_pydantic/v3/v3_1/schema.py b/openapi_pydantic/v3/v3_1/schema.py new file mode 100644 index 0000000..0d53e30 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/schema.py @@ -0,0 +1,971 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union + +from pydantic import BaseModel, Field + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra, min_length_arg + +from .datatype import DataType +from .discriminator import Discriminator +from .external_documentation import ExternalDocumentation +from .reference import Reference +from .xml import XML + +_examples = [ + {"type": "string", "format": "email"}, + { + "type": "object", + "required": ["name"], + "properties": { + "name": {"type": "string"}, + "address": {"$ref": "#/components/schemas/Address"}, + "age": {"type": "integer", "format": "int32", "minimum": 0}, + }, + }, + {"type": "object", "additionalProperties": {"type": "string"}}, + { + "type": "object", + "additionalProperties": {"$ref": "#/components/schemas/ComplexModel"}, + }, + { + "type": "object", + "properties": { + "id": {"type": "integer", "format": "int64"}, + "name": {"type": "string"}, + }, + "required": ["name"], + "example": {"name": "Puma", "id": 1}, + }, + { + "type": "object", + "required": ["message", "code"], + "properties": { + "message": {"type": "string"}, + "code": {"type": "integer", "minimum": 100, "maximum": 600}, + }, + }, + { + "allOf": [ + {"$ref": "#/components/schemas/ErrorModel"}, + { + "type": "object", + "required": ["rootCause"], + "properties": {"rootCause": {"type": "string"}}, + }, + ] + }, + { + "type": "object", + "discriminator": {"propertyName": "petType"}, + "properties": { + "name": {"type": "string"}, + "petType": {"type": "string"}, + }, + "required": ["name", "petType"], + }, + { + "description": "A representation of a cat. " + "Note that `Cat` will be used as the discriminator value.", + "allOf": [ + {"$ref": "#/components/schemas/Pet"}, + { + "type": "object", + "properties": { + "huntingSkill": { + "type": "string", + "description": "The measured skill for hunting", + "default": "lazy", + "enum": [ + "clueless", + "lazy", + "adventurous", + "aggressive", + ], + } + }, + "required": ["huntingSkill"], + }, + ], + }, + { + "description": "A representation of a dog. " + "Note that `Dog` will be used as the discriminator value.", + "allOf": [ + {"$ref": "#/components/schemas/Pet"}, + { + "type": "object", + "properties": { + "packSize": { + "type": "integer", + "format": "int32", + "description": "the size of the pack the dog is from", + "default": 0, + "minimum": 0, + } + }, + "required": ["packSize"], + }, + ], + }, +] + + +class Schema(BaseModel): + """ + The Schema Object allows the definition of input and output data types. + These types can be objects, but also primitives and arrays. + This object is a superset of + the [JSON Schema Specification Draft 2020-12](https://tools.ietf.org/html/draft-bhutton-json-schema-00). + + For more information about the properties, + see [JSON Schema Core](https://tools.ietf.org/html/draft-wright-json-schema-00) + and [JSON Schema Validation](https://tools.ietf.org/html/draft-wright-json-schema-validation-00). + + Unless stated otherwise, the property definitions follow those of JSON Schema + and do not add any additional semantics. + Where JSON Schema indicates that behavior is defined by the application (e.g. for + annotations), OAS also defers the definition of semantics to the application + consuming the OpenAPI document. + """ + + """ + The following properties are taken directly from the + [JSON Schema Core](https://tools.ietf.org/html/draft-wright-json-schema-00) + and follow the same specifications: + """ + + allOf: Optional[List[Union[Reference, "Schema"]]] = None + """ + This keyword's value MUST be a non-empty array. Each item of the + array MUST be a valid JSON Schema. + + An instance validates successfully against this keyword if it + validates successfully against all schemas defined by this keyword's + value. + """ + + anyOf: Optional[List[Union[Reference, "Schema"]]] = None + """ + This keyword's value MUST be a non-empty array. Each item of the + array MUST be a valid JSON Schema. + + An instance validates successfully against this keyword if it + validates successfully against at least one schema defined by this + keyword's value. Note that when annotations are being collected, all + subschemas MUST be examined so that annotations are collected from + each subschema that validates successfully. + """ + + oneOf: Optional[List[Union[Reference, "Schema"]]] = None + """ + This keyword's value MUST be a non-empty array. Each item of the + array MUST be a valid JSON Schema. + + An instance validates successfully against this keyword if it + validates successfully against exactly one schema defined by this + keyword's value. + """ + + schema_not: Optional[Union[Reference, "Schema"]] = Field(default=None, alias="not") + """ + This keyword's value MUST be a valid JSON Schema. + + An instance is valid against this keyword if it fails to validate + successfully against the schema defined by this keyword. + """ + + schema_if: Optional[Union[Reference, "Schema"]] = Field(default=None, alias="if") + """ + This keyword's value MUST be a valid JSON Schema. + + This validation outcome of this keyword's subschema has no direct + effect on the overall validation result. Rather, it controls which + of the "then" or "else" keywords are evaluated. + + Instances that successfully validate against this keyword's subschema + MUST also be valid against the subschema value of the "then" keyword, + if present. + + Instances that fail to validate against this keyword's subschema MUST + also be valid against the subschema value of the "else" keyword, if + present. + + If annotations (Section 7.7) are being collected, they are collected + from this keyword's subschema in the usual way, including when the + keyword is present without either "then" or "else". + """ + + then: Optional[Union[Reference, "Schema"]] = None + """ + This keyword's value MUST be a valid JSON Schema. + + When "if" is present, and the instance successfully validates against + its subschema, then validation succeeds against this keyword if the + instance also successfully validates against this keyword's + subschema. + + This keyword has no effect when "if" is absent, or when the instance + fails to validate against its subschema. Implementations MUST NOT + evaluate the instance against this keyword, for either validation or + annotation collection purposes, in such cases. + """ + + schema_else: Optional[Union[Reference, "Schema"]] = Field( + default=None, alias="else" + ) + """ + This keyword's value MUST be a valid JSON Schema. + + When "if" is present, and the instance fails to validate against its + subschema, then validation succeeds against this keyword if the + instance successfully validates against this keyword's subschema. + + This keyword has no effect when "if" is absent, or when the instance + successfully validates against its subschema. Implementations MUST + NOT evaluate the instance against this keyword, for either validation + or annotation collection purposes, in such cases. + """ + + dependentSchemas: Optional[Dict[str, Union[Reference, "Schema"]]] = None + """ + This keyword specifies subschemas that are evaluated if the instance + is an object and contains a certain property. + + This keyword's value MUST be an object. Each value in the object + MUST be a valid JSON Schema. + + If the object key is a property in the instance, the entire instance + must validate against the subschema. Its use is dependent on the + presence of the property. + + Omitting this keyword has the same behavior as an empty object. + """ + + prefixItems: Optional[List[Union[Reference, "Schema"]]] = None + """ + The value of "prefixItems" MUST be a non-empty array of valid JSON + Schemas. + + Validation succeeds if each element of the instance validates against + the schema at the same position, if any. This keyword does not + constrain the length of the array. If the array is longer than this + keyword's value, this keyword validates only the prefix of matching + length. + + This keyword produces an annotation value which is the largest index + to which this keyword applied a subschema. The value MAY be a + boolean true if a subschema was applied to every index of the + instance, such as is produced by the "items" keyword. This + annotation affects the behavior of "items" and "unevaluatedItems". + + Omitting this keyword has the same assertion behavior as an empty + array. + """ + + items: Optional[Union[Reference, "Schema"]] = None + """ + The value of "items" MUST be a valid JSON Schema. + + This keyword applies its subschema to all instance elements at + indexes greater than the length of the "prefixItems" array in the + same schema object, as reported by the annotation result of that + "prefixItems" keyword. If no such annotation result exists, "items" + applies its subschema to all instance array elements. [[CREF11: Note + that the behavior of "items" without "prefixItems" is identical to + that of the schema form of "items" in prior drafts. When + "prefixItems" is present, the behavior of "items" is identical to the + former "additionalItems" keyword. ]] + + If the "items" subschema is applied to any positions within the + instance array, it produces an annotation result of boolean true, + indicating that all remaining array elements have been evaluated + against this keyword's subschema. + + Omitting this keyword has the same assertion behavior as an empty + schema. + + Implementations MAY choose to implement or optimize this keyword in + another way that produces the same effect, such as by directly + checking for the presence and size of a "prefixItems" array. + Implementations that do not support annotation collection MUST do so. + """ + + contains: Optional[Union[Reference, "Schema"]] = None + """ + The value of this keyword MUST be a valid JSON Schema. + + An array instance is valid against "contains" if at least one of its + elements is valid against the given schema. The subschema MUST be + applied to every array element even after the first match has been + found, in order to collect annotations for use by other keywords. + This is to ensure that all possible annotations are collected. + + Logically, the validation result of applying the value subschema to + each item in the array MUST be ORed with "false", resulting in an + overall validation result. + + This keyword produces an annotation value which is an array of the + indexes to which this keyword validates successfully when applying + its subschema, in ascending order. The value MAY be a boolean "true" + if the subschema validates successfully when applied to every index + of the instance. The annotation MUST be present if the instance + array to which this keyword's schema applies is empty. + """ + + properties: Optional[Dict[str, Union[Reference, "Schema"]]] = None + """ + The value of "properties" MUST be an object. Each value of this + object MUST be a valid JSON Schema. + + Validation succeeds if, for each name that appears in both the + instance and as a name within this keyword's value, the child + instance for that name successfully validates against the + corresponding schema. + + The annotation result of this keyword is the set of instance property + names matched by this keyword. + + Omitting this keyword has the same assertion behavior as an empty + object. + """ + + patternProperties: Optional[Dict[str, Union[Reference, "Schema"]]] = None + """ + The value of "patternProperties" MUST be an object. Each property + name of this object SHOULD be a valid regular expression, according + to the ECMA-262 regular expression dialect. Each property value of + this object MUST be a valid JSON Schema. + + Validation succeeds if, for each instance name that matches any + regular expressions that appear as a property name in this keyword's + value, the child instance for that name successfully validates + against each schema that corresponds to a matching regular + expression. + + The annotation result of this keyword is the set of instance property + names matched by this keyword. + + Omitting this keyword has the same assertion behavior as an empty + object. + """ + + additionalProperties: Optional[Union[Reference, "Schema", bool]] = None + """ + The value of "additionalProperties" MUST be a valid JSON Schema. + + The behavior of this keyword depends on the presence and annotation + results of "properties" and "patternProperties" within the same + schema object. Validation with "additionalProperties" applies only + to the child values of instance names that do not appear in the + annotation results of either "properties" or "patternProperties". + + For all such properties, validation succeeds if the child instance + validates against the "additionalProperties" schema. + + The annotation result of this keyword is the set of instance property + names validated by this keyword's subschema. + + Omitting this keyword has the same assertion behavior as an empty + schema. + + Implementations MAY choose to implement or optimize this keyword in + another way that produces the same effect, such as by directly + checking the names in "properties" and the patterns in + "patternProperties" against the instance property set. + Implementations that do not support annotation collection MUST do so. + """ + + propertyNames: Optional[Union[Reference, "Schema"]] = None + """ + The value of "propertyNames" MUST be a valid JSON Schema. + + If the instance is an object, this keyword validates if every + property name in the instance validates against the provided schema. + Note the property name that the schema is testing will always be a + string. + + Omitting this keyword has the same behavior as an empty schema. + """ + + unevaluatedItems: Optional[Union[Reference, "Schema"]] = None + """ + The value of "unevaluatedItems" MUST be a valid JSON Schema. + + The behavior of this keyword depends on the annotation results of + adjacent keywords that apply to the instance location being + validated. Specifically, the annotations from "prefixItems", + "items", and "contains", which can come from those keywords when they + are adjacent to the "unevaluatedItems" keyword. Those three + annotations, as well as "unevaluatedItems", can also result from any + and all adjacent in-place applicator (Section 10.2) keywords. This + includes but is not limited to the in-place applicators defined in + this document. + + If no relevant annotations are present, the "unevaluatedItems" + subschema MUST be applied to all locations in the array. If a + boolean true value is present from any of the relevant annotations, + "unevaluatedItems" MUST be ignored. Otherwise, the subschema MUST be + applied to any index greater than the largest annotation value for + "prefixItems", which does not appear in any annotation value for + "contains". + + This means that "prefixItems", "items", "contains", and all in-place + applicators MUST be evaluated before this keyword can be evaluated. + Authors of extension keywords MUST NOT define an in-place applicator + that would need to be evaluated after this keyword. + + If the "unevaluatedItems" subschema is applied to any positions + within the instance array, it produces an annotation result of + boolean true, analogous to the behavior of "items". + + Omitting this keyword has the same assertion behavior as an empty + schema. + """ + + unevaluatedProperties: Optional[Union[Reference, "Schema"]] = None + """ + The value of "unevaluatedProperties" MUST be a valid JSON Schema. + + The behavior of this keyword depends on the annotation results of + adjacent keywords that apply to the instance location being + validated. Specifically, the annotations from "properties", + "patternProperties", and "additionalProperties", which can come from + those keywords when they are adjacent to the "unevaluatedProperties" + keyword. Those three annotations, as well as + "unevaluatedProperties", can also result from any and all adjacent + in-place applicator (Section 10.2) keywords. This includes but is + not limited to the in-place applicators defined in this document. + + Validation with "unevaluatedProperties" applies only to the child + values of instance names that do not appear in the "properties", + "patternProperties", "additionalProperties", or + "unevaluatedProperties" annotation results that apply to the instance + location being validated. + + For all such properties, validation succeeds if the child instance + validates against the "unevaluatedProperties" schema. + + This means that "properties", "patternProperties", + "additionalProperties", and all in-place applicators MUST be + evaluated before this keyword can be evaluated. Authors of extension + keywords MUST NOT define an in-place applicator that would need to be + evaluated after this keyword. + + The annotation result of this keyword is the set of instance property + names validated by this keyword's subschema. + + Omitting this keyword has the same assertion behavior as an empty + schema. + """ + + """ + The following properties are taken directly from the + [JSON Schema Validation](https://tools.ietf.org/html/draft-wright-json-schema-validation-00) + and follow the same specifications: + """ + + type: Optional[Union[DataType, List[DataType]]] = None + """ + The value of this keyword MUST be either a string or an array. If it + is an array, elements of the array MUST be strings and MUST be + unique. + + String values MUST be one of the six primitive types ("null", + "boolean", "object", "array", "number", or "string"), or "integer" + which matches any number with a zero fractional part. + + An instance validates if and only if the instance is in any of the + sets listed for this keyword. + """ + + enum: Optional[List[Any]] = Field(default=None, **min_length_arg(1)) + """ + The value of this keyword MUST be an array. This array SHOULD have + at least one element. Elements in the array SHOULD be unique. + + An instance validates successfully against this keyword if its value + is equal to one of the elements in this keyword's array value. + + Elements in the array might be of any type, including null. + """ + + const: Optional[Any] = None + """ + The value of this keyword MAY be of any type, including null. + + Use of this keyword is functionally equivalent to an "enum" + (Section 6.1.2) with a single value. + + An instance validates successfully against this keyword if its value + is equal to the value of the keyword. + """ + + multipleOf: Optional[float] = Field(default=None, gt=0.0) + """ + The value of "multipleOf" MUST be a number, strictly greater than 0. + + A numeric instance is only valid if division by this keyword's value + results in an integer. + """ + + maximum: Optional[float] = None + """ + The value of "maximum" MUST be a number, representing an inclusive + upper limit for a numeric instance. + + If the instance is a number, then this keyword validates only if the + instance is less than or exactly equal to "maximum". + """ + + exclusiveMaximum: Optional[float] = None + """ + The value of "exclusiveMaximum" MUST be a number, representing an + exclusive upper limit for a numeric instance. + + If the instance is a number, then the instance is valid only if it + has a value strictly less than (not equal to) "exclusiveMaximum". + """ + + minimum: Optional[float] = None + """ + The value of "minimum" MUST be a number, representing an inclusive + lower limit for a numeric instance. + + If the instance is a number, then this keyword validates only if the + instance is greater than or exactly equal to "minimum". + """ + + exclusiveMinimum: Optional[float] = None + """ + The value of "exclusiveMinimum" MUST be a number, representing an + exclusive lower limit for a numeric instance. + + If the instance is a number, then the instance is valid only if it + has a value strictly greater than (not equal to) "exclusiveMinimum". + """ + + maxLength: Optional[int] = Field(default=None, ge=0) + """ + The value of this keyword MUST be a non-negative integer. + + A string instance is valid against this keyword if its length is less + than, or equal to, the value of this keyword. + + The length of a string instance is defined as the number of its + characters as defined by RFC 8259 [RFC8259]. + """ + + minLength: Optional[int] = Field(default=None, ge=0) + """ + The value of this keyword MUST be a non-negative integer. + + A string instance is valid against this keyword if its length is + greater than, or equal to, the value of this keyword. + + The length of a string instance is defined as the number of its + characters as defined by RFC 8259 [RFC8259]. + + Omitting this keyword has the same behavior as a value of 0. + """ + + pattern: Optional[str] = None + """ + The value of this keyword MUST be a string. This string SHOULD be a + valid regular expression, according to the ECMA-262 regular + expression dialect. + + A string instance is considered valid if the regular expression + matches the instance successfully. Recall: regular expressions are + not implicitly anchored. + """ + + maxItems: Optional[int] = Field(default=None, ge=0) + """ + The value of this keyword MUST be a non-negative integer. + + An array instance is valid against "maxItems" if its size is less + than, or equal to, the value of this keyword. + """ + + minItems: Optional[int] = Field(default=None, ge=0) + """ + The value of this keyword MUST be a non-negative integer. + + An array instance is valid against "minItems" if its size is greater + than, or equal to, the value of this keyword. + + Omitting this keyword has the same behavior as a value of 0. + """ + + uniqueItems: Optional[bool] = None + """ + The value of this keyword MUST be a boolean. + + If this keyword has boolean value false, the instance validates + successfully. If it has boolean value true, the instance validates + successfully if all of its elements are unique. + + Omitting this keyword has the same behavior as a value of false. + """ + + maxContains: Optional[int] = Field(default=None, ge=0) + """ + The value of this keyword MUST be a non-negative integer. + + If "contains" is not present within the same schema object, then this + keyword has no effect. + + An instance array is valid against "maxContains" in two ways, + depending on the form of the annotation result of an adjacent + "contains" [json-schema] keyword. The first way is if the annotation + result is an array and the length of that array is less than or equal + to the "maxContains" value. The second way is if the annotation + result is a boolean "true" and the instance array length is less than + or equal to the "maxContains" value. + """ + + minContains: Optional[int] = Field(default=None, ge=0) + """ + The value of this keyword MUST be a non-negative integer. + + If "contains" is not present within the same schema object, then this + keyword has no effect. + + An instance array is valid against "minContains" in two ways, + depending on the form of the annotation result of an adjacent + "contains" [json-schema] keyword. The first way is if the annotation + result is an array and the length of that array is greater than or + equal to the "minContains" value. The second way is if the + annotation result is a boolean "true" and the instance array length + is greater than or equal to the "minContains" value. + + A value of 0 is allowed, but is only useful for setting a range of + occurrences from 0 to the value of "maxContains". A value of 0 with + no "maxContains" causes "contains" to always pass validation. + + Omitting this keyword has the same behavior as a value of 1. + """ + + maxProperties: Optional[int] = Field(default=None, ge=0) + """ + The value of this keyword MUST be a non-negative integer. + + An object instance is valid against "maxProperties" if its number of + properties is less than, or equal to, the value of this keyword. + """ + + minProperties: Optional[int] = Field(default=None, ge=0) + """ + The value of this keyword MUST be a non-negative integer. + + An object instance is valid against "minProperties" if its number of + properties is greater than, or equal to, the value of this keyword. + + Omitting this keyword has the same behavior as a value of 0. + """ + + required: Optional[List[str]] = None + """ + The value of this keyword MUST be an array. Elements of this array, + if any, MUST be strings, and MUST be unique. + + An object instance is valid against this keyword if every item in the + array is the name of a property in the instance. + + Omitting this keyword has the same behavior as an empty array. + """ + + dependentRequired: Optional[Dict[str, List[str]]] = None + """ + The value of this keyword MUST be an object. Properties in this + object, if any, MUST be arrays. Elements in each array, if any, MUST + be strings, and MUST be unique. + + This keyword specifies properties that are required if a specific + other property is present. Their requirement is dependent on the + presence of the other property. + + Validation succeeds if, for each name that appears in both the + instance and as a name within this keyword's value, every item in the + corresponding array is also the name of a property in the instance. + + Omitting this keyword has the same behavior as an empty object. + """ + + schema_format: Optional[str] = Field(default=None, alias="format") + """ + From OpenAPI: + See [Data Type Formats](#dataTypeFormat) for further details. + While relying on JSON Schema's defined formats, the OAS offers a few additional + predefined formats. + + From JSON Schema: + Structural validation alone may be insufficient to allow an + application to correctly utilize certain values. The "format" + annotation keyword is defined to allow schema authors to convey + semantic information for a fixed subset of values which are + accurately described by authoritative resources, be they RFCs or + other external specifications. + + The value of this keyword is called a format attribute. It MUST be a + string. A format attribute can generally only validate a given set + of instance types. If the type of the instance to validate is not in + this set, validation for this format attribute and instance SHOULD + succeed. All format attributes defined in this section apply to + strings, but a format attribute can be specified to apply to any + instance types defined in the data model defined in the core JSON + Schema. [json-schema] [[CREF1: Note that the "type" keyword in this + specification defines an "integer" type which is not part of the data + model. Therefore a format attribute can be limited to numbers, but + not specifically to integers. However, a numeric format can be used + alongside the "type" keyword with a value of "integer", or could be + explicitly defined to always pass if the number is not an integer, + which produces essentially the same behavior as only applying to + integers. ]] + """ + + contentEncoding: Optional[str] = None + """ + If the instance value is a string, this property defines that the + string SHOULD be interpreted as binary data and decoded using the + encoding named by this property. + + Possible values indicating base 16, 32, and 64 encodings with several + variations are listed in RFC 4648 [RFC4648]. Additionally, sections + 6.7 and 6.8 of RFC 2045 [RFC2045] provide encodings used in MIME. As + "base64" is defined in both RFCs, the definition from RFC 4648 SHOULD + be assumed unless the string is specifically intended for use in a + MIME context. Note that all of these encodings result in strings + consisting only of 7-bit ASCII characters. Therefore, this keyword + has no meaning for strings containing characters outside of that + range. + + If this keyword is absent, but "contentMediaType" is present, this + indicates that the encoding is the identity encoding, meaning that no + transformation was needed in order to represent the content in a + UTF-8 string. + """ + + contentMediaType: Optional[str] = None + """ + If the instance is a string, this property indicates the media type + of the contents of the string. If "contentEncoding" is present, this + property describes the decoded string. + + The value of this property MUST be a string, which MUST be a media + type, as defined by RFC 2046 [RFC2046]. + """ + + contentSchema: Optional[Union[Reference, "Schema"]] = None + """ + If the instance is a string, and if "contentMediaType" is present, + this property contains a schema which describes the structure of the + string. + + This keyword MAY be used with any media type that can be mapped into + JSON Schema's data model. + + The value of this property MUST be a valid JSON schema. It SHOULD be + ignored if "contentMediaType" is not present. + """ + + title: Optional[str] = None + """ + The value of "title" MUST be a string. + + The title can be used to decorate a user interface with + information about the data produced by this user interface. + A title will preferably be short. + """ + + description: Optional[str] = None + """ + From OpenAPI: + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + + From JSON Schema: + The value "description" MUST be a string. + + The description can be used to decorate a user interface with + information about the data produced by this user interface. + A description will provide explanation about the purpose of + the instance described by this schema. + """ + + default: Optional[Any] = None + """ + There are no restrictions placed on the value of this keyword. When + multiple occurrences of this keyword are applicable to a single sub- + instance, implementations SHOULD remove duplicates. + + This keyword can be used to supply a default JSON value associated + with a particular schema. It is RECOMMENDED that a default value be + valid against the associated schema. + """ + + deprecated: Optional[bool] = None + """ + The value of this keyword MUST be a boolean. When multiple + occurrences of this keyword are applicable to a single sub-instance, + applications SHOULD consider the instance location to be deprecated + if any occurrence specifies a true value. + + If "deprecated" has a value of boolean true, it indicates that + applications SHOULD refrain from usage of the declared property. It + MAY mean the property is going to be removed in the future. + + A root schema containing "deprecated" with a value of true indicates + that the entire resource being described MAY be removed in the + future. + + The "deprecated" keyword applies to each instance location to which + the schema object containing the keyword successfully applies. This + can result in scenarios where every array item or object property is + deprecated even though the containing array or object is not. + + Omitting this keyword has the same behavior as a value of false. + """ + + readOnly: Optional[bool] = None + """ + The value of "readOnly" MUST be a boolean. When multiple + occurrences of this keyword are applicable to a single sub-instance, + the resulting behavior SHOULD be as for a true value if any + occurrence specifies a true value, and SHOULD be as for a false value + otherwise. + + If "readOnly" has a value of boolean true, it indicates that the + value of the instance is managed exclusively by the owning authority, + and attempts by an application to modify the value of this property + are expected to be ignored or rejected by that owning authority. + + An instance document that is marked as "readOnly" for the entire + document MAY be ignored if sent to the owning authority, or MAY + result in an error, at the authority's discretion. + + For example, "readOnly" would be used to mark a database-generated + serial number as read-only, while "writeOnly" would be used to mark a + password input field. + + This keyword can be used to assist in user interface instance + generation. In particular, an application MAY choose to use a widget + that hides input values as they are typed for write-only fields. + + Omitting these keywords has the same behavior as values of false. + """ + + writeOnly: Optional[bool] = None + """ + The value of "writeOnly" MUST be a boolean. When multiple + occurrences of this keyword are applicable to a single sub-instance, + the resulting behavior SHOULD be as for a true value if any + occurrence specifies a true value, and SHOULD be as for a false value + otherwise. + + If "writeOnly" has a value of boolean true, it indicates that the + value is never present when the instance is retrieved from the owning + authority. It can be present when sent to the owning authority to + update or create the document (or the resource it represents), but it + will not be included in any updated or newly created version of the + instance. + + An instance document that is marked as "writeOnly" for the entire + document MAY be returned as a blank document of some sort, or MAY + produce an error upon retrieval, or have the retrieval request + ignored, at the authority's discretion. + + For example, "readOnly" would be used to mark a database-generated + serial number as read-only, while "writeOnly" would be used to mark a + password input field. + + This keyword can be used to assist in user interface instance + generation. In particular, an application MAY choose to use a widget + that hides input values as they are typed for write-only fields. + + Omitting these keywords has the same behavior as values of false. + """ + + examples: Optional[List[Any]] = None + """ + The value of this keyword MUST be an array. There are no + restrictions placed on the values within the array. When multiple + occurrences of this keyword are applicable to a single sub-instance, + implementations MUST provide a flat array of all values rather than + an array of arrays. + + This keyword can be used to provide sample JSON values associated + with a particular schema, for the purpose of illustrating usage. It + is RECOMMENDED that these values be valid against the associated + schema. + + Implementations MAY use the value(s) of "default", if present, as an + additional example. If "examples" is absent, "default" MAY still be + used in this manner. + """ + + """ + The OpenAPI Specification's base vocabulary is comprised of the following keywords: + """ + + discriminator: Optional[Discriminator] = None + """ + Adds support for polymorphism. + The discriminator is an object name that is used to differentiate between other + schemas which may satisfy the payload description. + See [Composition and Inheritance](#schemaComposition) for more details. + """ + + xml: Optional[XML] = None + """ + This MAY be used only on properties schemas. + It has no effect on root schemas. + Adds additional metadata to describe the XML representation of this property. + """ + + externalDocs: Optional[ExternalDocumentation] = None + """ + Additional external documentation for this schema. + """ + + example: Optional[Any] = None + """ + A free-form property to include an example of an instance for this schema. + To represent examples that cannot be naturally represented in JSON or YAML, + a string value can be used to contain the example with escaping where necessary. + + Deprecated: The example property has been deprecated in favor of the JSON Schema + examples keyword. + Use of example is discouraged, and later versions of this specification may remove + it. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + allow_population_by_field_name = True + schema_extra = {"examples": _examples} + + +if TYPE_CHECKING: + + def schema_validate( + obj: Any, + *, + strict: Optional[bool] = None, + from_attributes: Optional[bool] = None, + context: Optional[Dict[str, Any]] = None + ) -> Schema: ... + +elif PYDANTIC_V2: + schema_validate = Schema.model_validate + +else: + schema_validate = Schema.parse_obj diff --git a/openapi_pydantic/v3/v3_1/security_requirement.py b/openapi_pydantic/v3/v3_1/security_requirement.py new file mode 100644 index 0000000..f8e2733 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/security_requirement.py @@ -0,0 +1,33 @@ +from typing import Dict, List + +SecurityRequirement = Dict[str, List[str]] +""" +Lists the required security schemes to execute this operation. +The name used for each property MUST correspond to a security scheme declared in the +[Security Schemes](#componentsSecuritySchemes) under the +[Components Object](#componentsObject). + +Security Requirement Objects that contain multiple schemes require that +all schemes MUST be satisfied for a request to be authorized. +This enables support for scenarios where multiple query parameters or HTTP headers +are required to convey security information. + +When a list of Security Requirement Objects is defined on the +[OpenAPI Object](#oasObject) or [Operation Object](#operationObject), +only one of the Security Requirement Objects in the list needs to be satisfied to +authorize the request. +""" + +"""Patterned Fields""" + +# {name}: List[str] +""" +Each name MUST correspond to a security scheme which is declared +in the [Security Schemes](#componentsSecuritySchemes) under the +[Components Object](#componentsObject). +If the security scheme is of type `"oauth2"` or `"openIdConnect"`, +then the value is a list of scope names required for the execution, +and the list MAY be empty if authorization does not require a specified scope. +For other security scheme types, the array MAY contain a list of role names which are +required for the execution, but are not otherwise defined or exchanged in-band. +""" diff --git a/openapi_pydantic/v3/v3_1/security_scheme.py b/openapi_pydantic/v3/v3_1/security_scheme.py new file mode 100644 index 0000000..88a447c --- /dev/null +++ b/openapi_pydantic/v3/v3_1/security_scheme.py @@ -0,0 +1,119 @@ +from typing import Optional + +from pydantic import BaseModel, Field + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .oauth_flows import OAuthFlows + +_examples = [ + {"type": "http", "scheme": "basic"}, + {"type": "apiKey", "name": "api_key", "in": "header"}, + {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}, + { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://example.com/api/oauth/dialog", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + } + }, + }, + { + "type": "openIdConnect", + "openIdConnectUrl": "https://example.com/openIdConnect", + }, + { + "type": "openIdConnect", + "openIdConnectUrl": "openIdConnect", + }, # issue #5: allow relative path +] + + +class SecurityScheme(BaseModel): + """ + Defines a security scheme that can be used by the operations. + + Supported schemes are HTTP authentication, + an API key (either as a header, a cookie parameter or as a query parameter), + mutual TLS (use of a client certificate), + OAuth2's common flows (implicit, password, client credentials and authorization + code) as defined in [RFC6749](https://tools.ietf.org/html/rfc6749), + and [OpenID Connect Discovery](https://tools.ietf.org/html/draft-ietf-oauth-discovery-06). + + Please note that as of 2020, the implicit flow is about to be deprecated by + [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics). + Recommended for most use case is Authorization Code Grant flow with PKCE. + """ + + type: str + """ + **REQUIRED**. The type of the security scheme. + Valid values are `"apiKey"`, `"http"`, "mutualTLS", `"oauth2"`, `"openIdConnect"`. + """ + + description: Optional[str] = None + """ + A description for security scheme. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + name: Optional[str] = None + """ + **REQUIRED** for `apiKey`. The name of the header, query or cookie parameter to be + used. + """ + + security_scheme_in: Optional[str] = Field(alias="in", default=None) + """ + **REQUIRED** for `apiKey`. The location of the API key. Valid values are `"query"`, + `"header"` or `"cookie"`. + """ + + scheme: Optional[str] = None + """ + **REQUIRED** for `http`. The name of the HTTP Authorization scheme to be used in the + [Authorization header as defined in RFC7235](https://tools.ietf.org/html/rfc7235#section-5.1). + + The values used SHOULD be registered in the + [IANA Authentication Scheme registry](https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml). + """ + + bearerFormat: Optional[str] = None + """ + A hint to the client to identify how the bearer token is formatted. + + Bearer tokens are usually generated by an authorization server, + so this information is primarily for documentation purposes. + """ + + flows: Optional[OAuthFlows] = None + """ + **REQUIRED** for `oauth2`. An object containing configuration information for the + flow types supported. + """ + + openIdConnectUrl: Optional[str] = None + """ + **REQUIRED** for `openIdConnect`. OpenId Connect URL to discover OAuth2 + configuration values. This MUST be in the form of a URL. The OpenID Connect + standard requires the use of TLS. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + allow_population_by_field_name = True + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/server.py b/openapi_pydantic/v3/v3_1/server.py new file mode 100644 index 0000000..0b98b75 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/server.py @@ -0,0 +1,67 @@ +from typing import Dict, Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .server_variable import ServerVariable + +_examples = [ + { + "url": "https://development.gigantic-server.com/v1", + "description": "Development server", + }, + { + "url": "https://{username}.gigantic-server.com:{port}/{basePath}", + "description": "The production API server", + "variables": { + "username": { + "default": "demo", + "description": "this value is assigned by the service " + "provider, in this example `gigantic-server.com`", + }, + "port": {"enum": ["8443", "443"], "default": "8443"}, + "basePath": {"default": "v2"}, + }, + }, +] + + +class Server(BaseModel): + """An object representing a Server.""" + + url: str + """ + **REQUIRED**. A URL to the target host. + + This URL supports Server Variables and MAY be relative, + to indicate that the host location is relative to the location where the OpenAPI + document is being served. + Variable substitutions will be made when a variable is named in `{`brackets`}`. + """ + + description: Optional[str] = None + """ + An optional string describing the host designated by the URL. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + variables: Optional[Dict[str, ServerVariable]] = None + """ + A map between a variable name and its value. + + The value is used for substitution in the server's URL template. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/server_variable.py b/openapi_pydantic/v3/v3_1/server_variable.py new file mode 100644 index 0000000..3de5734 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/server_variable.py @@ -0,0 +1,42 @@ +from typing import List, Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + + +class ServerVariable(BaseModel): + """An object representing a Server Variable for server URL template substitution.""" + + enum: Optional[List[str]] = None + """ + An enumeration of string values to be used if the substitution options are from a + limited set. The array SHOULD NOT be empty. + """ + + default: str + """ + **REQUIRED**. The default value to use for substitution, + which SHALL be sent if an alternate value is _not_ supplied. + Note this behavior is different than the [Schema Object's](#schemaObject) treatment + of default values, because in those cases parameter values are optional. + If the [`enum`](#serverVariableEnum) is defined, the value MUST exist in the enum's + values. + """ + + description: Optional[str] = None + """ + An optional description for the server variable. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + ) + + else: + + class Config: + extra = Extra.allow diff --git a/openapi_pydantic/v3/v3_1/tag.py b/openapi_pydantic/v3/v3_1/tag.py new file mode 100644 index 0000000..9ec0d03 --- /dev/null +++ b/openapi_pydantic/v3/v3_1/tag.py @@ -0,0 +1,47 @@ +from typing import Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +from .external_documentation import ExternalDocumentation + +_examples = [{"name": "pet", "description": "Pets operations"}] + + +class Tag(BaseModel): + """ + Adds metadata to a single tag that is used by the + [Operation Object](#operationObject). + It is not mandatory to have a Tag Object per tag defined in the Operation Object + instances. + """ + + name: str + """ + **REQUIRED**. The name of the tag. + """ + + description: Optional[str] = None + """ + A short description for the tag. + [CommonMark syntax](https://spec.commonmark.org/) MAY be used for rich text + representation. + """ + + externalDocs: Optional[ExternalDocumentation] = None + """ + Additional external documentation for this tag. + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/openapi_pydantic/v3/v3_1/xml.py b/openapi_pydantic/v3/v3_1/xml.py new file mode 100644 index 0000000..4972fca --- /dev/null +++ b/openapi_pydantic/v3/v3_1/xml.py @@ -0,0 +1,72 @@ +from typing import Optional + +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict, Extra + +_examples = [ + {"name": "animal"}, + {"attribute": True}, + {"wrapped": True}, + {"namespace": "http://example.com/schema/sample", "prefix": "sample"}, + {"name": "aliens", "wrapped": True}, +] + + +class XML(BaseModel): + """ + A metadata object that allows for more fine-tuned XML model definitions. + + When using arrays, XML element names are *not* inferred (for singular/plural forms) + and the `name` property SHOULD be used to add that information. + See examples for expected behavior. + """ + + name: Optional[str] = None + """ + Replaces the name of the element/attribute used for the described schema property. + When defined within `items`, it will affect the name of the individual XML elements + within the list. + When defined alongside `type` being `array` (outside the `items`), + it will affect the wrapping element and only if `wrapped` is `true`. + If `wrapped` is `false`, it will be ignored. + """ + + namespace: Optional[str] = None + """ + The URI of the namespace definition. + Value MUST be in the form of an absolute URI. + """ + + prefix: Optional[str] = None + """ + The prefix to be used for the [name](#xmlName). + """ + + attribute: bool = False + """ + Declares whether the property definition translates to an attribute instead of an + element. Default value is `false`. + """ + + wrapped: bool = False + """ + MAY be used only for an array definition. + Signifies whether the array is wrapped + (for example, ``) or unwrapped (``). + Default value is `false`. + The definition takes effect only when defined alongside `type` being `array` + (outside the `items`). + """ + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="allow", + json_schema_extra={"examples": _examples}, + ) + + else: + + class Config: + extra = Extra.allow + schema_extra = {"examples": _examples} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..6f37c80 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1250 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} + +[[package]] +name = "attrs" +version = "24.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "black" +version = "24.8.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.6.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] + +[[package]] +name = "identify" +version = "2.6.1" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, + {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "importlib-resources" +version = "6.4.5" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, + {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] +type = ["pytest-mypy"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, + {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} +jsonschema-specifications = ">=2023.03.6" +pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-path" +version = "0.3.3" +description = "JSONSchema Spec with object-oriented paths" +optional = false +python-versions = "<4.0.0,>=3.8.0" +files = [ + {file = "jsonschema_path-0.3.3-py3-none-any.whl", hash = "sha256:203aff257f8038cd3c67be614fe6b2001043408cb1b4e36576bc4921e09d83c4"}, + {file = "jsonschema_path-0.3.3.tar.gz", hash = "sha256:f02e5481a4288ec062f8e68c808569e427d905bedfecb7f2e4c69ef77957c382"}, +] + +[package.dependencies] +pathable = ">=0.4.1,<0.5.0" +PyYAML = ">=5.1" +referencing = ">=0.28.0,<0.36.0" +requests = ">=2.31.0,<3.0.0" + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, + {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, +] + +[package.dependencies] +importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} +referencing = ">=0.31.0" + +[[package]] +name = "lazy-object-proxy" +version = "1.10.0" +description = "A fast and thorough lazy object proxy." +optional = false +python-versions = ">=3.8" +files = [ + {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab7004cf2e59f7c2e4345604a3e6ea0d92ac44e1c2375527d56492014e690c3"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0d2fc424e54c70c4bc06787e4072c4f3b1aa2f897dfdc34ce1013cf3ceef05"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e2adb09778797da09d2b5ebdbceebf7dd32e2c96f79da9052b2e87b6ea495895"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1f711e2c6dcd4edd372cf5dec5c5a30d23bba06ee012093267b3376c079ec83"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-win32.whl", hash = "sha256:76a095cfe6045c7d0ca77db9934e8f7b71b14645f0094ffcd842349ada5c5fb9"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4f87d4ed9064b2628da63830986c3d2dca7501e6018347798313fcf028e2fd4"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fec03caabbc6b59ea4a638bee5fce7117be8e99a4103d9d5ad77f15d6f81020c"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c83f957782cbbe8136bee26416686a6ae998c7b6191711a04da776dc9e47d4"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009e6bb1f1935a62889ddc8541514b6a9e1fcf302667dcb049a0be5c8f613e56"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75fc59fc450050b1b3c203c35020bc41bd2695ed692a392924c6ce180c6f1dc9"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:782e2c9b2aab1708ffb07d4bf377d12901d7a1d99e5e410d648d892f8967ab1f"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-win32.whl", hash = "sha256:edb45bb8278574710e68a6b021599a10ce730d156e5b254941754a9cc0b17d03"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:e271058822765ad5e3bca7f05f2ace0de58a3f4e62045a8c90a0dfd2f8ad8cc6"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e98c8af98d5707dcdecc9ab0863c0ea6e88545d42ca7c3feffb6b4d1e370c7ba"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:952c81d415b9b80ea261d2372d2a4a2332a3890c2b83e0535f263ddfe43f0d43"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b39d3a151309efc8cc48675918891b865bdf742a8616a337cb0090791a0de9"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e221060b701e2aa2ea991542900dd13907a5c90fa80e199dbf5a03359019e7a3"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92f09ff65ecff3108e56526f9e2481b8116c0b9e1425325e13245abfd79bdb1b"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-win32.whl", hash = "sha256:3ad54b9ddbe20ae9f7c1b29e52f123120772b06dbb18ec6be9101369d63a4074"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:127a789c75151db6af398b8972178afe6bda7d6f68730c057fbbc2e96b08d282"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4ed0518a14dd26092614412936920ad081a424bdcb54cc13349a8e2c6d106a"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ad9e6ed739285919aa9661a5bbed0aaf410aa60231373c5579c6b4801bd883c"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc0a92c02fa1ca1e84fc60fa258458e5bf89d90a1ddaeb8ed9cc3147f417255"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0aefc7591920bbd360d57ea03c995cebc204b424524a5bd78406f6e1b8b2a5d8"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5faf03a7d8942bb4476e3b62fd0f4cf94eaf4618e304a19865abf89a35c0bbee"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-win32.whl", hash = "sha256:e333e2324307a7b5d86adfa835bb500ee70bfcd1447384a822e96495796b0ca4"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:cb73507defd385b7705c599a94474b1d5222a508e502553ef94114a143ec6696"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:366c32fe5355ef5fc8a232c5436f4cc66e9d3e8967c01fb2e6302fd6627e3d94"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2297f08f08a2bb0d32a4265e98a006643cd7233fb7983032bd61ac7a02956b3b"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18dd842b49456aaa9a7cf535b04ca4571a302ff72ed8740d06b5adcd41fe0757"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:217138197c170a2a74ca0e05bddcd5f1796c735c37d0eee33e43259b192aa424"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a3a87cf1e133e5b1994144c12ca4aa3d9698517fe1e2ca82977781b16955658"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-win32.whl", hash = "sha256:30b339b2a743c5288405aa79a69e706a06e02958eab31859f7f3c04980853b70"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:a899b10e17743683b293a729d3a11f2f399e8a90c73b089e29f5d0fe3509f0dd"}, + {file = "lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d"}, +] + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "openapi-schema-validator" +version = "0.6.2" +description = "OpenAPI schema validation for Python" +optional = false +python-versions = ">=3.8.0,<4.0.0" +files = [ + {file = "openapi_schema_validator-0.6.2-py3-none-any.whl", hash = "sha256:c4887c1347c669eb7cded9090f4438b710845cd0f90d1fb9e1b3303fb37339f8"}, + {file = "openapi_schema_validator-0.6.2.tar.gz", hash = "sha256:11a95c9c9017912964e3e5f2545a5b11c3814880681fcacfb73b1759bb4f2804"}, +] + +[package.dependencies] +jsonschema = ">=4.19.1,<5.0.0" +jsonschema-specifications = ">=2023.5.2,<2024.0.0" +rfc3339-validator = "*" + +[[package]] +name = "openapi-spec-validator" +version = "0.7.1" +description = "OpenAPI 2.0 (aka Swagger) and OpenAPI 3 spec validator" +optional = false +python-versions = ">=3.8.0,<4.0.0" +files = [ + {file = "openapi_spec_validator-0.7.1-py3-none-any.whl", hash = "sha256:3c81825043f24ccbcd2f4b149b11e8231abce5ba84f37065e14ec947d8f4e959"}, + {file = "openapi_spec_validator-0.7.1.tar.gz", hash = "sha256:8577b85a8268685da6f8aa30990b83b7960d4d1117e901d451b5d572605e5ec7"}, +] + +[package.dependencies] +importlib-resources = {version = ">=5.8,<7.0", markers = "python_version < \"3.9\""} +jsonschema = ">=4.18.0,<5.0.0" +jsonschema-path = ">=0.3.1,<0.4.0" +lazy-object-proxy = ">=1.7.1,<2.0.0" +openapi-schema-validator = ">=0.6.0,<0.7.0" + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pathable" +version = "0.4.3" +description = "Object-oriented paths" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "pathable-0.4.3-py3-none-any.whl", hash = "sha256:cdd7b1f9d7d5c8b8d3315dbf5a86b2596053ae845f056f57d97c0eefff84da14"}, + {file = "pathable-0.4.3.tar.gz", hash = "sha256:5c869d315be50776cc8a993f3af43e0c60dc01506b399643f919034ebf4cdcab"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pkgutil-resolve-name" +version = "1.3.10" +description = "Resolve a name to an object." +optional = false +python-versions = ">=3.6" +files = [ + {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, + {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "2.21.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pydantic" +version = "2.10.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e"}, + {file = "pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.27.1" +typing-extensions = ">=4.12.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.27.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, + {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, + {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, + {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, + {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, + {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, + {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, + {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, + {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, + {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, + {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, + {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"}, + {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"}, + {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"}, + {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"}, + {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"}, + {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "referencing" +version = "0.35.1" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, + {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +description = "A pure python RFC3339 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, + {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "rpds-py" +version = "0.20.1" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a649dfd735fff086e8a9d0503a9f0c7d01b7912a333c7ae77e1515c08c146dad"}, + {file = "rpds_py-0.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f16bc1334853e91ddaaa1217045dd7be166170beec337576818461268a3de67f"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14511a539afee6f9ab492b543060c7491c99924314977a55c98bfa2ee29ce78c"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ccb8ac2d3c71cda472b75af42818981bdacf48d2e21c36331b50b4f16930163"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c142b88039b92e7e0cb2552e8967077e3179b22359e945574f5e2764c3953dcf"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f19169781dddae7478a32301b499b2858bc52fc45a112955e798ee307e294977"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13c56de6518e14b9bf6edde23c4c39dac5b48dcf04160ea7bce8fca8397cdf86"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:925d176a549f4832c6f69fa6026071294ab5910e82a0fe6c6228fce17b0706bd"}, + {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:78f0b6877bfce7a3d1ff150391354a410c55d3cdce386f862926a4958ad5ab7e"}, + {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3dd645e2b0dcb0fd05bf58e2e54c13875847687d0b71941ad2e757e5d89d4356"}, + {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4f676e21db2f8c72ff0936f895271e7a700aa1f8d31b40e4e43442ba94973899"}, + {file = "rpds_py-0.20.1-cp310-none-win32.whl", hash = "sha256:648386ddd1e19b4a6abab69139b002bc49ebf065b596119f8f37c38e9ecee8ff"}, + {file = "rpds_py-0.20.1-cp310-none-win_amd64.whl", hash = "sha256:d9ecb51120de61e4604650666d1f2b68444d46ae18fd492245a08f53ad2b7711"}, + {file = "rpds_py-0.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:762703bdd2b30983c1d9e62b4c88664df4a8a4d5ec0e9253b0231171f18f6d75"}, + {file = "rpds_py-0.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0b581f47257a9fce535c4567782a8976002d6b8afa2c39ff616edf87cbeff712"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:842c19a6ce894493563c3bd00d81d5100e8e57d70209e84d5491940fdb8b9e3a"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42cbde7789f5c0bcd6816cb29808e36c01b960fb5d29f11e052215aa85497c93"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c8e9340ce5a52f95fa7d3b552b35c7e8f3874d74a03a8a69279fd5fca5dc751"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ba6f89cac95c0900d932c9efb7f0fb6ca47f6687feec41abcb1bd5e2bd45535"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a916087371afd9648e1962e67403c53f9c49ca47b9680adbeef79da3a7811b0"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:200a23239781f46149e6a415f1e870c5ef1e712939fe8fa63035cd053ac2638e"}, + {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58b1d5dd591973d426cbb2da5e27ba0339209832b2f3315928c9790e13f159e8"}, + {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6b73c67850ca7cae0f6c56f71e356d7e9fa25958d3e18a64927c2d930859b8e4"}, + {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d8761c3c891cc51e90bc9926d6d2f59b27beaf86c74622c8979380a29cc23ac3"}, + {file = "rpds_py-0.20.1-cp311-none-win32.whl", hash = "sha256:cd945871335a639275eee904caef90041568ce3b42f402c6959b460d25ae8732"}, + {file = "rpds_py-0.20.1-cp311-none-win_amd64.whl", hash = "sha256:7e21b7031e17c6b0e445f42ccc77f79a97e2687023c5746bfb7a9e45e0921b84"}, + {file = "rpds_py-0.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:36785be22066966a27348444b40389f8444671630063edfb1a2eb04318721e17"}, + {file = "rpds_py-0.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:142c0a5124d9bd0e2976089484af5c74f47bd3298f2ed651ef54ea728d2ea42c"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbddc10776ca7ebf2a299c41a4dde8ea0d8e3547bfd731cb87af2e8f5bf8962d"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15a842bb369e00295392e7ce192de9dcbf136954614124a667f9f9f17d6a216f"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be5ef2f1fc586a7372bfc355986226484e06d1dc4f9402539872c8bb99e34b01"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbcf360c9e3399b056a238523146ea77eeb2a596ce263b8814c900263e46031a"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd27a66740ffd621d20b9a2f2b5ee4129a56e27bfb9458a3bcc2e45794c96cb"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0b937b2a1988f184a3e9e577adaa8aede21ec0b38320d6009e02bd026db04fa"}, + {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6889469bfdc1eddf489729b471303739bf04555bb151fe8875931f8564309afc"}, + {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:19b73643c802f4eaf13d97f7855d0fb527fbc92ab7013c4ad0e13a6ae0ed23bd"}, + {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c6afcf2338e7f374e8edc765c79fbcb4061d02b15dd5f8f314a4af2bdc7feb5"}, + {file = "rpds_py-0.20.1-cp312-none-win32.whl", hash = "sha256:dc73505153798c6f74854aba69cc75953888cf9866465196889c7cdd351e720c"}, + {file = "rpds_py-0.20.1-cp312-none-win_amd64.whl", hash = "sha256:8bbe951244a838a51289ee53a6bae3a07f26d4e179b96fc7ddd3301caf0518eb"}, + {file = "rpds_py-0.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6ca91093a4a8da4afae7fe6a222c3b53ee4eef433ebfee4d54978a103435159e"}, + {file = "rpds_py-0.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b9c2fe36d1f758b28121bef29ed1dee9b7a2453e997528e7d1ac99b94892527c"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f009c69bc8c53db5dfab72ac760895dc1f2bc1b62ab7408b253c8d1ec52459fc"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6740a3e8d43a32629bb9b009017ea5b9e713b7210ba48ac8d4cb6d99d86c8ee8"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32b922e13d4c0080d03e7b62991ad7f5007d9cd74e239c4b16bc85ae8b70252d"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe00a9057d100e69b4ae4a094203a708d65b0f345ed546fdef86498bf5390982"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fe9b04b6fa685bd39237d45fad89ba19e9163a1ccaa16611a812e682913496"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa7ac11e294304e615b43f8c441fee5d40094275ed7311f3420d805fde9b07b4"}, + {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aa97af1558a9bef4025f8f5d8c60d712e0a3b13a2fe875511defc6ee77a1ab7"}, + {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:483b29f6f7ffa6af845107d4efe2e3fa8fb2693de8657bc1849f674296ff6a5a"}, + {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37fe0f12aebb6a0e3e17bb4cd356b1286d2d18d2e93b2d39fe647138458b4bcb"}, + {file = "rpds_py-0.20.1-cp313-none-win32.whl", hash = "sha256:a624cc00ef2158e04188df5e3016385b9353638139a06fb77057b3498f794782"}, + {file = "rpds_py-0.20.1-cp313-none-win_amd64.whl", hash = "sha256:b71b8666eeea69d6363248822078c075bac6ed135faa9216aa85f295ff009b1e"}, + {file = "rpds_py-0.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5b48e790e0355865197ad0aca8cde3d8ede347831e1959e158369eb3493d2191"}, + {file = "rpds_py-0.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3e310838a5801795207c66c73ea903deda321e6146d6f282e85fa7e3e4854804"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249280b870e6a42c0d972339e9cc22ee98730a99cd7f2f727549af80dd5a963"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e79059d67bea28b53d255c1437b25391653263f0e69cd7dec170d778fdbca95e"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b431c777c9653e569986ecf69ff4a5dba281cded16043d348bf9ba505486f36"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da584ff96ec95e97925174eb8237e32f626e7a1a97888cdd27ee2f1f24dd0ad8"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a0629ec053fc013808a85178524e3cb63a61dbc35b22499870194a63578fb9"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fbf15aff64a163db29a91ed0868af181d6f68ec1a3a7d5afcfe4501252840bad"}, + {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:07924c1b938798797d60c6308fa8ad3b3f0201802f82e4a2c41bb3fafb44cc28"}, + {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4a5a844f68776a7715ecb30843b453f07ac89bad393431efbf7accca3ef599c1"}, + {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:518d2ca43c358929bf08f9079b617f1c2ca6e8848f83c1225c88caeac46e6cbc"}, + {file = "rpds_py-0.20.1-cp38-none-win32.whl", hash = "sha256:3aea7eed3e55119635a74bbeb80b35e776bafccb70d97e8ff838816c124539f1"}, + {file = "rpds_py-0.20.1-cp38-none-win_amd64.whl", hash = "sha256:7dca7081e9a0c3b6490a145593f6fe3173a94197f2cb9891183ef75e9d64c425"}, + {file = "rpds_py-0.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b41b6321805c472f66990c2849e152aff7bc359eb92f781e3f606609eac877ad"}, + {file = "rpds_py-0.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a90c373ea2975519b58dece25853dbcb9779b05cc46b4819cb1917e3b3215b6"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d4477bcb9fbbd7b5b0e4a5d9b493e42026c0bf1f06f723a9353f5153e75d30"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84b8382a90539910b53a6307f7c35697bc7e6ffb25d9c1d4e998a13e842a5e83"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4888e117dd41b9d34194d9e31631af70d3d526efc363085e3089ab1a62c32ed1"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5265505b3d61a0f56618c9b941dc54dc334dc6e660f1592d112cd103d914a6db"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e75ba609dba23f2c95b776efb9dd3f0b78a76a151e96f96cc5b6b1b0004de66f"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1791ff70bc975b098fe6ecf04356a10e9e2bd7dc21fa7351c1742fdeb9b4966f"}, + {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d126b52e4a473d40232ec2052a8b232270ed1f8c9571aaf33f73a14cc298c24f"}, + {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c14937af98c4cc362a1d4374806204dd51b1e12dded1ae30645c298e5a5c4cb1"}, + {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3d089d0b88996df627693639d123c8158cff41c0651f646cd8fd292c7da90eaf"}, + {file = "rpds_py-0.20.1-cp39-none-win32.whl", hash = "sha256:653647b8838cf83b2e7e6a0364f49af96deec64d2a6578324db58380cff82aca"}, + {file = "rpds_py-0.20.1-cp39-none-win_amd64.whl", hash = "sha256:fa41a64ac5b08b292906e248549ab48b69c5428f3987b09689ab2441f267d04d"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a07ced2b22f0cf0b55a6a510078174c31b6d8544f3bc00c2bcee52b3d613f74"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:68cb0a499f2c4a088fd2f521453e22ed3527154136a855c62e148b7883b99f9a"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa3060d885657abc549b2a0f8e1b79699290e5d83845141717c6c90c2df38311"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95f3b65d2392e1c5cec27cff08fdc0080270d5a1a4b2ea1d51d5f4a2620ff08d"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cc3712a4b0b76a1d45a9302dd2f53ff339614b1c29603a911318f2357b04dd2"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d4eea0761e37485c9b81400437adb11c40e13ef513375bbd6973e34100aeb06"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f5179583d7a6cdb981151dd349786cbc318bab54963a192692d945dd3f6435d"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fbb0ffc754490aff6dabbf28064be47f0f9ca0b9755976f945214965b3ace7e"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a94e52537a0e0a85429eda9e49f272ada715506d3b2431f64b8a3e34eb5f3e75"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:92b68b79c0da2a980b1c4197e56ac3dd0c8a149b4603747c4378914a68706979"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:93da1d3db08a827eda74356f9f58884adb254e59b6664f64cc04cdff2cc19b0d"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:754bbed1a4ca48479e9d4182a561d001bbf81543876cdded6f695ec3d465846b"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ca449520e7484534a2a44faf629362cae62b660601432d04c482283c47eaebab"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9c4cb04a16b0f199a8c9bf807269b2f63b7b5b11425e4a6bd44bd6961d28282c"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63804105143c7e24cee7db89e37cb3f3941f8e80c4379a0b355c52a52b6780"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:55cd1fa4ecfa6d9f14fbd97ac24803e6f73e897c738f771a9fe038f2f11ff07c"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f8f741b6292c86059ed175d80eefa80997125b7c478fb8769fd9ac8943a16c0"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fc212779bf8411667234b3cdd34d53de6c2b8b8b958e1e12cb473a5f367c338"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ad56edabcdb428c2e33bbf24f255fe2b43253b7d13a2cdbf05de955217313e6"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a3a1e9ee9728b2c1734f65d6a1d376c6f2f6fdcc13bb007a08cc4b1ff576dc5"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e13de156137b7095442b288e72f33503a469aa1980ed856b43c353ac86390519"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:07f59760ef99f31422c49038964b31c4dfcfeb5d2384ebfc71058a7c9adae2d2"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:59240685e7da61fb78f65a9f07f8108e36a83317c53f7b276b4175dc44151684"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:83cba698cfb3c2c5a7c3c6bac12fe6c6a51aae69513726be6411076185a8b24a"}, + {file = "rpds_py-0.20.1.tar.gz", hash = "sha256:e1791c4aabd117653530dccd24108fa03cc6baf21f58b950d0a73c3b3b29a350"}, +] + +[[package]] +name = "ruff" +version = "0.7.2" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.7.2-py3-none-linux_armv6l.whl", hash = "sha256:b73f873b5f52092e63ed540adefc3c36f1f803790ecf2590e1df8bf0a9f72cb8"}, + {file = "ruff-0.7.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5b813ef26db1015953daf476202585512afd6a6862a02cde63f3bafb53d0b2d4"}, + {file = "ruff-0.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:853277dbd9675810c6826dad7a428d52a11760744508340e66bf46f8be9701d9"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21aae53ab1490a52bf4e3bf520c10ce120987b047c494cacf4edad0ba0888da2"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccc7e0fc6e0cb3168443eeadb6445285abaae75142ee22b2b72c27d790ab60ba"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd77877a4e43b3a98e5ef4715ba3862105e299af0c48942cc6d51ba3d97dc859"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e00163fb897d35523c70d71a46fbaa43bf7bf9af0f4534c53ea5b96b2e03397b"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3c54b538633482dc342e9b634d91168fe8cc56b30a4b4f99287f4e339103e88"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b792468e9804a204be221b14257566669d1db5c00d6bb335996e5cd7004ba80"}, + {file = "ruff-0.7.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dba53ed84ac19ae4bfb4ea4bf0172550a2285fa27fbb13e3746f04c80f7fa088"}, + {file = "ruff-0.7.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b19fafe261bf741bca2764c14cbb4ee1819b67adb63ebc2db6401dcd652e3748"}, + {file = "ruff-0.7.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28bd8220f4d8f79d590db9e2f6a0674f75ddbc3847277dd44ac1f8d30684b828"}, + {file = "ruff-0.7.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9fd67094e77efbea932e62b5d2483006154794040abb3a5072e659096415ae1e"}, + {file = "ruff-0.7.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:576305393998b7bd6c46018f8104ea3a9cb3fa7908c21d8580e3274a3b04b691"}, + {file = "ruff-0.7.2-py3-none-win32.whl", hash = "sha256:fa993cfc9f0ff11187e82de874dfc3611df80852540331bc85c75809c93253a8"}, + {file = "ruff-0.7.2-py3-none-win_amd64.whl", hash = "sha256:dd8800cbe0254e06b8fec585e97554047fb82c894973f7ff18558eee33d1cb88"}, + {file = "ruff-0.7.2-py3-none-win_arm64.whl", hash = "sha256:bb8368cd45bba3f57bb29cbb8d64b4a33f8415d0149d2655c5c8539452ce7760"}, + {file = "ruff-0.7.2.tar.gz", hash = "sha256:2b14e77293380e475b4e3a7a368e14549288ed2931fce259a6f99978669e844f"}, +] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "tomli" +version = "2.0.2" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.27.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +files = [ + {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, + {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "zipp" +version = "3.20.2" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, + {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "834d62056bc88495edf2a6475aaef19829eda09adbc9ac377f9ba9c6a65c872d" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5c06584 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,57 @@ +[tool.poetry] +name = "openapi-pydantic" +version = "0.5.1" +description = "Pydantic OpenAPI schema implementation" +authors = ["Mike Oakley "] +readme = "README.md" +repository = "https://github.com/mike-oakley/openapi-pydantic" +license = "MIT" +keywords = [ + "openapi", + "schema", + "parser", + "pydantic", + "validation", +] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Framework :: Pydantic", +] +include = ["openapi_pydantic/py.typed"] + +[tool.poetry.urls] +changelog = "https://github.com/mike-oakley/openapi-pydantic/releases" + +[tool.poetry.dependencies] +python = "^3.8" +pydantic = ">=1.8" + +[tool.poetry.group.test.dependencies] +pytest = "^8.2.2" +pytest-cov = "^5.0.0" +openapi-spec-validator = "^0.7.0" + +[tool.poetry.group.dev.dependencies] +black = "^24.4.2" +mypy = "^1.8.0" +pre-commit = "^2.16.0" +ruff = "^0.7.2" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.mypy] +warn_unused_ignores = true +warn_redundant_casts = true +warn_unused_configs = true +warn_unreachable = true +warn_return_any = true +strict = true +disallow_any_generics = false +implicit_reexport = false +show_error_codes = true +files = ["openapi_pydantic/", "tests/"] +plugins = ["pydantic.mypy"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/swagger_openapi_v3.0.1.json b/tests/data/swagger_openapi_v3.0.1.json new file mode 100644 index 0000000..5047064 --- /dev/null +++ b/tests/data/swagger_openapi_v3.0.1.json @@ -0,0 +1,1103 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Swagger Petstore", + "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "apiteam@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.0" + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + }, + "servers": [ + { + "url": "https://petstore.swagger.io/v2" + }, + { + "url": "http://petstore.swagger.io/v2" + } + ], + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Access to Petstore orders" + }, + { + "name": "user", + "description": "Operations about user", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + } + } + ], + "paths": { + "/pet": { + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "operationId": "updatePet", + "requestBody": { + "description": "Pet object that needs to be added to the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "responses": { + "400": { + "description": "Invalid ID supplied", + "content": {} + }, + "404": { + "description": "Pet not found", + "content": {} + }, + "405": { + "description": "Validation exception", + "content": {} + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ], + "x-codegen-request-body-name": "body" + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "operationId": "addPet", + "requestBody": { + "description": "Pet object that needs to be added to the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "responses": { + "405": { + "description": "Invalid input", + "content": {} + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ], + "x-codegen-request-body-name": "body" + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": true, + "style": "form", + "explode": true, + "schema": { + "type": "array", + "items": { + "type": "string", + "default": "available", + "enum": [ + "available", + "pending", + "sold" + ] + } + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid status value", + "content": {} + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByTags": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "description": "Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": true, + "style": "form", + "explode": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid tag value", + "content": {} + } + }, + "deprecated": true, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied", + "content": {} + }, + "404": { + "description": "Pet not found", + "content": {} + } + }, + "security": [ + { + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "operationId": "updatePetWithForm", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "properties": { + "name": { + "type": "string", + "description": "Updated name of the pet" + }, + "status": { + "type": "string", + "description": "Updated status of the pet" + } + } + } + } + } + }, + "responses": { + "405": { + "description": "Invalid input", + "content": {} + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "operationId": "deletePet", + "parameters": [ + { + "name": "api_key", + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied", + "content": {} + }, + "404": { + "description": "Pet not found", + "content": {} + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": [ + "pet" + ], + "summary": "uploads an image", + "operationId": "uploadFile", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "additionalMetadata": { + "type": "string", + "description": "Additional data to pass to server" + }, + "file": { + "type": "string", + "description": "file to upload", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/store/inventory": { + "get": { + "tags": [ + "store" + ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/store/order": { + "post": { + "tags": [ + "store" + ], + "summary": "Place an order for a pet", + "operationId": "placeOrder", + "requestBody": { + "description": "order placed for purchasing the pet", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "400": { + "description": "Invalid Order", + "content": {} + } + }, + "x-codegen-request-body-name": "body" + } + }, + "/store/order/{orderId}": { + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", + "operationId": "getOrderById", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of pet that needs to be fetched", + "required": true, + "schema": { + "maximum": 10, + "minimum": 1, + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "400": { + "description": "Invalid ID supplied", + "content": {} + }, + "404": { + "description": "Order not found", + "content": {} + } + } + }, + "delete": { + "tags": [ + "store" + ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", + "operationId": "deleteOrder", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "schema": { + "minimum": 1, + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied", + "content": {} + }, + "404": { + "description": "Order not found", + "content": {} + } + } + } + }, + "/user": { + "post": { + "tags": [ + "user" + ], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "requestBody": { + "description": "Created user object", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "required": true + }, + "responses": { + "default": { + "description": "successful operation", + "content": {} + } + }, + "x-codegen-request-body-name": "body" + } + }, + "/user/createWithArray": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "operationId": "createUsersWithArrayInput", + "requestBody": { + "description": "List of user object", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "required": true + }, + "responses": { + "default": { + "description": "successful operation", + "content": {} + } + }, + "x-codegen-request-body-name": "body" + } + }, + "/user/createWithList": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "operationId": "createUsersWithListInput", + "requestBody": { + "description": "List of user object", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "required": true + }, + "responses": { + "default": { + "description": "successful operation", + "content": {} + } + }, + "x-codegen-request-body-name": "body" + } + }, + "/user/login": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs user into the system", + "operationId": "loginUser", + "parameters": [ + { + "name": "username", + "in": "query", + "description": "The user name for login", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "headers": { + "X-Rate-Limit": { + "description": "calls per hour allowed by the user", + "schema": { + "type": "integer", + "format": "int32" + } + }, + "X-Expires-After": { + "description": "date in UTC when token expires", + "schema": { + "type": "string", + "format": "date-time" + } + } + }, + "content": { + "application/xml": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid username/password supplied", + "content": {} + } + } + } + }, + "/user/logout": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs out current logged in user session", + "operationId": "logoutUser", + "responses": { + "default": { + "description": "successful operation", + "content": {} + } + } + } + }, + "/user/{username}": { + "get": { + "tags": [ + "user" + ], + "summary": "Get user by user name", + "operationId": "getUserByName", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "description": "Invalid username supplied", + "content": {} + }, + "404": { + "description": "User not found", + "content": {} + } + } + }, + "put": { + "tags": [ + "user" + ], + "summary": "Updated user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "name that need to be updated", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Updated user object", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "required": true + }, + "responses": { + "400": { + "description": "Invalid user supplied", + "content": {} + }, + "404": { + "description": "User not found", + "content": {} + } + }, + "x-codegen-request-body-name": "body" + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Invalid username supplied", + "content": {} + }, + "404": { + "description": "User not found", + "content": {} + } + } + } + } + }, + "components": { + "schemas": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "petId": { + "type": "integer", + "format": "int64" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean", + "default": false + } + }, + "xml": { + "name": "Order" + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Category" + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "integer", + "description": "User Status", + "format": "int32" + } + }, + "xml": { + "name": "User" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Tag" + } + }, + "Pet": { + "required": [ + "name", + "photoUrls" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/components/schemas/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "Pet" + } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + }, + "securitySchemes": { + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "http://petstore.swagger.io/oauth/dialog", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + } + } +} \ No newline at end of file diff --git a/tests/schema_classes/__init__.py b/tests/schema_classes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/schema_classes/test_schema.py b/tests/schema_classes/test_schema.py new file mode 100644 index 0000000..ecbe96f --- /dev/null +++ b/tests/schema_classes/test_schema.py @@ -0,0 +1,64 @@ +import logging + +from pydantic import BaseModel + +from openapi_pydantic import Reference, schema_validate +from openapi_pydantic.compat import ( + DEFS_KEY, + PYDANTIC_V2, + ConfigDict, + Extra, + models_json_schema, + v1_schema, +) + + +def test_schema() -> None: + schema = schema_validate( + { + "title": "reference list", + "description": "schema for list of reference type", + "allOf": [{"$ref": "#/definitions/TestType"}], + } + ) + logging.debug(f"schema.allOf={schema.allOf}") + assert schema.allOf + assert isinstance(schema.allOf, list) + assert isinstance(schema.allOf[0], Reference) + assert schema.allOf[0].ref == "#/definitions/TestType" + + +def test_additional_properties_is_bool() -> None: + class TestModel(BaseModel): + test_field: str + + if PYDANTIC_V2: + model_config = ConfigDict( + extra="forbid", + ) + + else: + + class Config: + extra = Extra.forbid + + if PYDANTIC_V2: + _key_map, schema_definition = models_json_schema([(TestModel, "validation")]) + else: + schema_definition = v1_schema([TestModel]) + + assert schema_definition == { + DEFS_KEY: { + "TestModel": { + "title": "TestModel", + "type": "object", + "properties": {"test_field": {"title": "Test Field", "type": "string"}}, + "required": ["test_field"], + "additionalProperties": False, + } + } + } + + # allow "additionalProperties" to have boolean value + result = schema_validate(schema_definition[DEFS_KEY]["TestModel"]) + assert result.additionalProperties is False diff --git a/tests/schema_classes/test_security_scheme.py b/tests/schema_classes/test_security_scheme.py new file mode 100644 index 0000000..b395af2 --- /dev/null +++ b/tests/schema_classes/test_security_scheme.py @@ -0,0 +1,46 @@ +from openapi_pydantic import SecurityScheme +from openapi_pydantic.compat import PYDANTIC_V2 + + +def test_oidc_parsing() -> None: + security_scheme_1 = SecurityScheme( + type="openIdConnect", openIdConnectUrl="https://example.com/openIdConnect" + ) + assert isinstance(security_scheme_1.openIdConnectUrl, str) + dump1 = getattr(security_scheme_1, "model_dump_json" if PYDANTIC_V2 else "json") + if PYDANTIC_V2: + assert dump1(by_alias=True, exclude_none=True) == ( + '{"type":"openIdConnect","openIdConnectUrl":"https://example.com/openIdConnect"}' + ) + else: + assert dump1(by_alias=True, exclude_none=True) == ( + '{"type": "openIdConnect", "openIdConnectUrl": "https://example.com/openIdConnect"}' + ) + + security_scheme_2 = SecurityScheme( + type="openIdConnect", openIdConnectUrl="/openIdConnect" + ) + assert isinstance(security_scheme_2.openIdConnectUrl, str) + dump2 = getattr(security_scheme_2, "model_dump_json" if PYDANTIC_V2 else "json") + if PYDANTIC_V2: + assert dump2(by_alias=True, exclude_none=True) == ( + '{"type":"openIdConnect","openIdConnectUrl":"/openIdConnect"}' + ) + else: + assert dump2(by_alias=True, exclude_none=True) == ( + '{"type": "openIdConnect", "openIdConnectUrl": "/openIdConnect"}' + ) + + security_scheme_3 = SecurityScheme( + type="openIdConnect", openIdConnectUrl="openIdConnect" + ) + assert isinstance(security_scheme_3.openIdConnectUrl, str) + dump3 = getattr(security_scheme_3, "model_dump_json" if PYDANTIC_V2 else "json") + if PYDANTIC_V2: + assert dump3(by_alias=True, exclude_none=True) == ( + '{"type":"openIdConnect","openIdConnectUrl":"openIdConnect"}' + ) + else: + assert dump3(by_alias=True, exclude_none=True) == ( + '{"type": "openIdConnect", "openIdConnectUrl": "openIdConnect"}' + ) diff --git a/tests/test_alias.py b/tests/test_alias.py new file mode 100644 index 0000000..04238d6 --- /dev/null +++ b/tests/test_alias.py @@ -0,0 +1,78 @@ +from typing import Callable + +from openapi_pydantic import ( + MediaType, + Parameter, + PathItem, + Reference, + Schema, + SecurityScheme, +) +from openapi_pydantic.compat import PYDANTIC_V2 + +validate_func_name = "model_validate" if PYDANTIC_V2 else "parse_obj" + + +def test_media_type_alias() -> None: + media_type_1 = MediaType(media_type_schema=Schema()) + media_type_2 = MediaType(schema=Schema()) + model_validate: Callable[[dict], MediaType] = getattr(MediaType, validate_func_name) + media_type_3 = model_validate({"media_type_schema": Schema()}) + media_type_4 = model_validate({"schema": Schema()}) + assert media_type_1 == media_type_2 == media_type_3 == media_type_4 + + +def test_parameter_alias() -> None: + parameter_1 = Parameter( # type: ignore + name="test", + param_in="path", + param_schema=Schema(), + ) + parameter_2 = Parameter( # type: ignore + name="test", + param_in="path", + schema=Schema(), + ) + model_validate: Callable[[dict], Parameter] = getattr(Parameter, validate_func_name) + parameter_3 = model_validate( + {"name": "test", "param_in": "path", "param_schema": Schema()} + ) + parameter_4 = model_validate({"name": "test", "in": "path", "schema": Schema()}) + assert parameter_1 == parameter_2 == parameter_3 == parameter_4 + + +def test_path_item_alias() -> None: + path_item_1 = PathItem(ref="#/dummy") + model_validate: Callable[[dict], PathItem] = getattr(PathItem, validate_func_name) + path_item_2 = model_validate({"ref": "#/dummy"}) + path_item_3 = model_validate({"$ref": "#/dummy"}) + assert path_item_1 == path_item_2 == path_item_3 + + +def test_reference_alias() -> None: + reference_1 = Reference(ref="#/dummy") # type: ignore + reference_2 = Reference(**{"$ref": "#/dummy"}) + model_validate: Callable[[dict], Reference] = getattr(Reference, validate_func_name) + reference_3 = model_validate({"ref": "#/dummy"}) + reference_4 = model_validate({"$ref": "#/dummy"}) + assert reference_1 == reference_2 == reference_3 == reference_4 + + +def test_security_scheme() -> None: + security_scheme_1 = SecurityScheme(type="apiKey", security_scheme_in="header") + model_validate: Callable[[dict], SecurityScheme] = getattr( + SecurityScheme, validate_func_name + ) + security_scheme_2 = model_validate( + {"type": "apiKey", "security_scheme_in": "header"} + ) + security_scheme_3 = model_validate({"type": "apiKey", "in": "header"}) + assert security_scheme_1 == security_scheme_2 == security_scheme_3 + + +def test_schema() -> None: + schema_1 = Schema(schema_not=Schema(), schema_format="email") + model_validate: Callable[[dict], Schema] = getattr(Schema, validate_func_name) + schema_2 = model_validate({"schema_not": Schema(), "schema_format": "email"}) + schema_3 = model_validate({"not": Schema(), "format": "email"}) + assert schema_1 == schema_2 == schema_3 diff --git a/tests/test_config_example.py b/tests/test_config_example.py new file mode 100644 index 0000000..5247f79 --- /dev/null +++ b/tests/test_config_example.py @@ -0,0 +1,95 @@ +from typing import Any + +from openapi_pydantic import ( + XML, + Callback, + Components, + Contact, + Discriminator, + Encoding, + Example, + ExternalDocumentation, + Header, + Info, + License, + Link, + MediaType, + OAuthFlow, + OAuthFlows, + OpenAPI, + Operation, + Parameter, + PathItem, + Paths, + Reference, + RequestBody, + Response, + Responses, + Schema, + SecurityRequirement, + SecurityScheme, + Server, + ServerVariable, + Tag, +) +from openapi_pydantic.compat import PYDANTIC_V2 + + +def test_config_example() -> None: + all_types = [ + OpenAPI, + Info, + Contact, + License, + Server, + ServerVariable, + Components, + Paths, + PathItem, + Operation, + ExternalDocumentation, + Parameter, + RequestBody, + MediaType, + Encoding, + Responses, + Response, + Callback, + Example, + Link, + Header, + Tag, + Reference, + Schema, + Discriminator, + XML, + SecurityScheme, + OAuthFlows, + OAuthFlow, + SecurityRequirement, + ] + for schema_type in all_types: + _assert_config_examples(schema_type) + + +def _assert_config_examples(schema_type: Any) -> None: + if PYDANTIC_V2: + if not hasattr(schema_type, "model_config"): + return + extra = schema_type.model_config.get("json_schema_extra") + if extra is not None: + examples = extra["examples"] + if examples is None: + breakpoint() + for example_dict in examples: + obj = schema_type.model_validate(example_dict) + assert obj.model_fields_set + + else: + Config = getattr(schema_type, "Config", None) + schema_extra = getattr(Config, "schema_extra", None) + if schema_extra is not None: + examples = schema_extra["examples"] + for example_dict in examples: + obj = schema_type(**example_dict) + assert obj.__fields_set__ diff --git a/tests/test_example.py b/tests/test_example.py new file mode 100644 index 0000000..1f3c8b7 --- /dev/null +++ b/tests/test_example.py @@ -0,0 +1,67 @@ +import logging +from typing import Callable + +from openapi_pydantic import Info, OpenAPI, Operation, PathItem, Response +from openapi_pydantic.compat import PYDANTIC_V2 + + +def test_readme_example() -> None: + open_api_1 = readme_example_1() + assert open_api_1 + dump_json = getattr(open_api_1, "model_dump_json" if PYDANTIC_V2 else "json") + open_api_json_1 = dump_json(by_alias=True, exclude_none=True, indent=2) + logging.debug(open_api_json_1) + assert open_api_json_1 + + open_api_2 = readme_example_2() + assert open_api_1 == open_api_2 + + open_api_3 = readme_example_3() + assert open_api_1 == open_api_3 + + +def readme_example_1() -> OpenAPI: + """Construct OpenAPI using data class""" + return OpenAPI( + info=Info( + title="My own API", + version="v0.0.1", + ), + paths={ + "/ping": PathItem( + get=Operation(responses={"200": Response(description="pong")}) + ) + }, + ) + + +def readme_example_2() -> OpenAPI: + """Construct OpenAPI from raw data object""" + openapi_validate: Callable[[dict], OpenAPI] = getattr( + OpenAPI, "model_validate" if PYDANTIC_V2 else "parse_obj" + ) + return openapi_validate( + { + "info": {"title": "My own API", "version": "v0.0.1"}, + "paths": { + "/ping": {"get": {"responses": {"200": {"description": "pong"}}}} + }, + } + ) + + +def readme_example_3() -> OpenAPI: + """Construct OpenAPI from mixed object""" + openapi_validate: Callable[[dict], OpenAPI] = getattr( + OpenAPI, "model_validate" if PYDANTIC_V2 else "parse_obj" + ) + return openapi_validate( + { + "info": {"title": "My own API", "version": "v0.0.1"}, + "paths": { + "/ping": PathItem( + get={"responses": {"200": Response(description="pong")}} + ) + }, + } + ) diff --git a/tests/test_openapi.py b/tests/test_openapi.py new file mode 100644 index 0000000..536e34a --- /dev/null +++ b/tests/test_openapi.py @@ -0,0 +1,81 @@ +from typing import Callable + +import pytest + +from openapi_pydantic.compat import PYDANTIC_V2 +from openapi_pydantic.v3 import v3_0, v3_1 + + +@pytest.mark.parametrize("version", ["3.0.4", "3.1.1"]) +def test_parse_with_callback(version: str) -> None: + data = { + "openapi": version, + "info": {"title": "API with Callback", "version": ""}, + "paths": { + "/create": { + "post": { + "responses": {"200": {"description": "Success"}}, + "callbacks": { + "event": { + "callback": { + "post": { + "responses": {"200": {"description": "Success"}} + } + } + } + }, + } + } + }, + } + + if version == "3.0.4": + model_validate_3_0: Callable[[dict], v3_0.OpenAPI] = getattr( + v3_0.OpenAPI, "model_validate" if PYDANTIC_V2 else "parse_obj" + ) + assert model_validate_3_0(data) == v3_0.OpenAPI( + info=v3_0.Info(title="API with Callback", version=""), + paths={ + "/create": v3_0.PathItem( + post=v3_0.Operation( + responses={"200": v3_0.Response(description="Success")}, + callbacks={ + "event": { + "callback": v3_0.PathItem( + post=v3_0.Operation( + responses={ + "200": v3_0.Response(description="Success") + } + ) + ) + } + }, + ) + ) + }, + ) + else: + model_validate_3_1: Callable[[dict], v3_1.OpenAPI] = getattr( + v3_1.OpenAPI, "model_validate" if PYDANTIC_V2 else "parse_obj" + ) + assert model_validate_3_1(data) == v3_1.OpenAPI( + info=v3_1.Info(title="API with Callback", version=""), + paths={ + "/create": v3_1.PathItem( + post=v3_1.Operation( + responses={"200": v3_1.Response(description="Success")}, + callbacks={ + "event": { + "callback": v3_1.PathItem( + post=v3_1.Operation( + responses={ + "200": v3_1.Response(description="Success") + } + ) + ) + } + }, + ) + ) + }, + ) diff --git a/tests/test_parse.py b/tests/test_parse.py new file mode 100644 index 0000000..0a48140 --- /dev/null +++ b/tests/test_parse.py @@ -0,0 +1,42 @@ +from typing import Literal + +import pytest + +from openapi_pydantic import parse_obj +from openapi_pydantic.v3 import v3_0, v3_1 + + +@pytest.mark.parametrize("version", ["3.0.4", "3.0.3", "3.0.2", "3.0.1", "3.0.0"]) +def test_parse_obj_3_0( + version: Literal["3.0.4", "3.0.3", "3.0.2", "3.0.1", "3.0.0"] +) -> None: + result = parse_obj( + { + "openapi": version, + "info": {"title": "foo", "version": "0.1.0"}, + "paths": {"/": {}}, + } + ) + + assert result == v3_0.OpenAPI( + openapi=version, + info=v3_0.Info(title="foo", version="0.1.0"), + paths={"/": v3_0.PathItem()}, + ) + + +@pytest.mark.parametrize("version", ["3.1.1", "3.1.0"]) +def test_parse_obj_3_1(version: Literal["3.1.1", "3.1.0"]) -> None: + result = parse_obj( + { + "openapi": version, + "info": {"title": "foo", "version": "0.1.0"}, + "paths": {"/": {}}, + } + ) + + assert result == v3_1.OpenAPI( + openapi=version, + info=v3_1.Info(title="foo", version="0.1.0"), + paths={"/": v3_1.PathItem()}, + ) diff --git a/tests/test_swagger_openapi_v3.py b/tests/test_swagger_openapi_v3.py new file mode 100644 index 0000000..a6214e3 --- /dev/null +++ b/tests/test_swagger_openapi_v3.py @@ -0,0 +1,47 @@ +from typing import Dict, Optional + +from pydantic import Field + +from openapi_pydantic.compat import PYDANTIC_V2, ConfigDict +from openapi_pydantic.v3.v3_0 import OpenAPI, Operation, PathItem + + +def test_swagger_openapi_v3() -> None: + with open("tests/data/swagger_openapi_v3.0.1.json") as f: + if PYDANTIC_V2: + validate = getattr(ExtendedOpenAPI, "model_validate_json") + else: + validate = getattr(ExtendedOpenAPI, "parse_raw") + open_api = validate(f.read()) + assert open_api + + +class ExtendedOperation(Operation): + """Override classes to use "x-codegen-request-body-name" in Operation""" + + xCodegenRequestBodyName: Optional[str] = Field( + default=None, alias="x-codegen-request-body-name" + ) + + if PYDANTIC_V2: + model_config = ConfigDict(populate_by_name=True) + + else: + + class Config: + allow_population_by_field_name = True + + +class ExtendedPathItem(PathItem): + get: Optional[ExtendedOperation] = None + put: Optional[ExtendedOperation] = None + post: Optional[ExtendedOperation] = None + delete: Optional[ExtendedOperation] = None + options: Optional[ExtendedOperation] = None + head: Optional[ExtendedOperation] = None + patch: Optional[ExtendedOperation] = None + trace: Optional[ExtendedOperation] = None + + +class ExtendedOpenAPI(OpenAPI): + paths: Dict[str, ExtendedPathItem] # type: ignore[assignment] diff --git a/tests/util/__init__.py b/tests/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/util/test_optional_and_computed.py b/tests/util/test_optional_and_computed.py new file mode 100644 index 0000000..d8b95f8 --- /dev/null +++ b/tests/util/test_optional_and_computed.py @@ -0,0 +1,114 @@ +# mypy: ignore-errors + +from typing import Optional + +import pytest + +from openapi_pydantic import ( + Info, + MediaType, + OpenAPI, + Operation, + PathItem, + RequestBody, + Response, + Schema, +) +from openapi_pydantic.compat import PYDANTIC_V2 +from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="computed fields require Pydantic V2") +def test_optional_and_computed_fields() -> None: + api = construct_sample_api() + + result = construct_open_api_with_schema_class(api) + assert result.components is not None + assert result.components.schemas is not None + + req_schema = result.components.schemas["SampleRequest"] + assert isinstance(req_schema, Schema) + assert req_schema.properties is not None + assert req_schema.required is not None + + resp_schema = result.components.schemas["SampleResponse"] + assert isinstance(resp_schema, Schema) + assert resp_schema.properties is not None + assert resp_schema.required is not None + + # When validating: + # - required fields are still required + # - optional fields are still optional + # - computed fields don't exist + assert "req" in req_schema.properties + assert "opt" in req_schema.properties + assert "comp" not in req_schema.properties + assert set(req_schema.required) == {"req"} + + # When serializing: + # - required fields are still required + # - optional fields are still optional + # (except when json_schema_serialization_defaults_required is enabled) + # - computed fields are required + assert "req" in resp_schema.properties + assert "opt" in resp_schema.properties + assert "comp" in resp_schema.properties + assert set(resp_schema.required) == {"req", "comp"} + + +def construct_sample_api() -> OpenAPI: + from typing import TYPE_CHECKING, Callable + + from pydantic import BaseModel + + if TYPE_CHECKING: + + def computed_field(x: Callable) -> Callable: ... + + else: + from pydantic import computed_field + + class SampleModel(BaseModel): + req: bool + opt: Optional[bool] = None + + @computed_field + @property + def comp(self) -> bool: + return True + + class SampleRequest(SampleModel): + model_config = {"json_schema_mode": "validation"} + + class SampleResponse(SampleModel): + model_config = {"json_schema_mode": "serialization"} + + return OpenAPI( + info=Info( + title="Sample API", + version="v0.0.1", + ), + paths={ + "/callme": PathItem( + post=Operation( + requestBody=RequestBody( + content={ + "application/json": MediaType( + schema=PydanticSchema(schema_class=SampleRequest) + ) + } + ), + responses={ + "200": Response( + description="resp", + content={ + "application/json": MediaType( + schema=PydanticSchema(schema_class=SampleResponse) + ) + }, + ) + }, + ) + ) + }, + ) diff --git a/tests/util/test_pydantic_field.py b/tests/util/test_pydantic_field.py new file mode 100644 index 0000000..2d9e583 --- /dev/null +++ b/tests/util/test_pydantic_field.py @@ -0,0 +1,163 @@ +from typing import Dict, List, Union + +from pydantic import BaseModel, Field +from typing_extensions import Literal + +from openapi_pydantic import ( + Discriminator, + Info, + MediaType, + OpenAPI, + Operation, + PathItem, + Reference, + RequestBody, + Response, + Schema, +) +from openapi_pydantic.compat import ( + DEFS_KEY, + PYDANTIC_MINOR_VERSION, + PYDANTIC_V2, + models_json_schema, + v1_schema, +) +from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class + + +class DataAModel(BaseModel): + kind: Literal["a"] + + +class DataBModel(BaseModel): + kind: Literal["b"] + + +class RequestModel(BaseModel): + data: Union[DataAModel, DataBModel] = Field(discriminator="kind") + + +def construct_base_open_api() -> OpenAPI: + return OpenAPI( + info=Info( + title="My own API", + version="v0.0.1", + ), + paths={ + "/ping": PathItem( + post=Operation( + requestBody=RequestBody( + content={ + "application/json": MediaType( + media_type_schema=PydanticSchema( + schema_class=RequestModel + ) + ) + } + ), + responses={"200": Response(description="pong")}, + ) + ) + }, + ) + + +def test_pydantic_discriminator_schema_generation() -> None: + """https://github.com/kuimono/openapi-schema-pydantic/issues/8""" + + a_kind: Dict[str, Union[str, List[str]]] = {"title": "Kind", "type": "string"} + b_kind: Dict[str, Union[str, List[str]]] = {"title": "Kind", "type": "string"} + + if PYDANTIC_V2: + _key_map, json_schema = models_json_schema([(RequestModel, "validation")]) + # In Pydantic v2, string literal types are mapped to consts. + a_kind["const"] = "a" + b_kind["const"] = "b" + if PYDANTIC_MINOR_VERSION < 10: + # Prior to 2.10, string literal types are also mapped to enums with a single entry. + a_kind["enum"] = ["a"] + b_kind["enum"] = ["b"] + else: + json_schema = v1_schema([RequestModel]) + # In Pydantic v1, string literal types are mapped to enums with a single entry. + a_kind["enum"] = ["a"] + b_kind["enum"] = ["b"] + + assert json_schema == { + DEFS_KEY: { + "DataAModel": { + "properties": { + "kind": a_kind, + }, + "required": ["kind"], + "title": "DataAModel", + "type": "object", + }, + "DataBModel": { + "properties": { + "kind": b_kind, + }, + "required": ["kind"], + "title": "DataBModel", + "type": "object", + }, + "RequestModel": { + "properties": { + "data": { + "oneOf": [ + {"$ref": f"#/{DEFS_KEY}/DataAModel"}, + {"$ref": f"#/{DEFS_KEY}/DataBModel"}, + ], + "discriminator": { + "mapping": { + "a": f"#/{DEFS_KEY}/DataAModel", + "b": f"#/{DEFS_KEY}/DataBModel", + }, + "propertyName": "kind", + }, + "title": "Data", + } + }, + "required": ["data"], + "title": "RequestModel", + "type": "object", + }, + } + } + + +def test_pydantic_discriminator_openapi_generation() -> None: + """https://github.com/kuimono/openapi-schema-pydantic/issues/8""" + + open_api = construct_open_api_with_schema_class(construct_base_open_api()) + assert open_api.components is not None + assert open_api.components.schemas is not None + json_schema = open_api.components.schemas["RequestModel"] + assert json_schema.properties == { + "data": Schema( + oneOf=[ + Reference( + **{ + "$ref": "#/components/schemas/DataAModel", + "summary": None, + "description": None, + } + ), + Reference( + **{ + "$ref": "#/components/schemas/DataBModel", + "summary": None, + "description": None, + } + ), + ], + title="Data", + discriminator=Discriminator( + propertyName="kind", + mapping={ + "a": "#/components/schemas/DataAModel", + "b": "#/components/schemas/DataBModel", + }, + ), + ) + } diff --git a/tests/util/test_util.py b/tests/util/test_util.py new file mode 100644 index 0000000..bed4270 --- /dev/null +++ b/tests/util/test_util.py @@ -0,0 +1,271 @@ +import logging +from typing import Callable, Generic, TypeVar + +import pytest +from pydantic import BaseModel, Field + +from openapi_pydantic import ( + Info, + MediaType, + OpenAPI, + Operation, + PathItem, + Reference, + RequestBody, + Response, +) +from openapi_pydantic.compat import PYDANTIC_V2 +from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class + + +def test_construct_open_api_with_schema_class_1() -> None: + open_api = construct_base_open_api_1() + result_open_api_1 = construct_open_api_with_schema_class(open_api) + result_open_api_2 = construct_open_api_with_schema_class( + open_api, [PingRequest, PingResponse] + ) + assert result_open_api_1.components == result_open_api_2.components + assert result_open_api_1 == result_open_api_2 + + dump_json = getattr(result_open_api_1, "model_dump_json" if PYDANTIC_V2 else "json") + open_api_json = dump_json(by_alias=True, exclude_none=True, indent=2) + logging.debug(open_api_json) + + +def test_construct_open_api_with_schema_class_2() -> None: + open_api_1 = construct_base_open_api_1() + open_api_2 = construct_base_open_api_2() + result_open_api_1 = construct_open_api_with_schema_class(open_api_1) + result_open_api_2 = construct_open_api_with_schema_class( + open_api_2, [PingRequest, PingResponse] + ) + assert result_open_api_1 == result_open_api_2 + + +def test_construct_open_api_with_schema_class_3() -> None: + open_api_3 = construct_base_open_api_3() + + result_with_alias_1 = construct_open_api_with_schema_class(open_api_3) + assert result_with_alias_1.components is not None + assert result_with_alias_1.components.schemas is not None + schema_with_alias = result_with_alias_1.components.schemas["PongResponse"] + assert schema_with_alias.properties is not None + assert "pong_foo" in schema_with_alias.properties + assert "pong_bar" in schema_with_alias.properties + + result_with_alias_2 = construct_open_api_with_schema_class( + open_api_3, by_alias=True + ) + assert result_with_alias_1 == result_with_alias_2 + + result_without_alias = construct_open_api_with_schema_class( + open_api_3, by_alias=False + ) + assert result_without_alias.components is not None + assert result_without_alias.components.schemas is not None + schema_without_alias = result_without_alias.components.schemas["PongResponse"] + assert schema_without_alias.properties is not None + assert "resp_foo" in schema_without_alias.properties + assert "resp_bar" in schema_without_alias.properties + + +@pytest.mark.skipif(PYDANTIC_V2, reason="generic type for Pydantic V1") +def test_construct_open_api_with_schema_class_4_generic_response_v1() -> None: + DataT = TypeVar("DataT") + from pydantic.v1.generics import GenericModel + + class GenericResponse(GenericModel, Generic[DataT]): + msg: str = Field(description="message of the generic response") + data: DataT = Field(description="data value of the generic response") + + open_api_4 = construct_base_open_api_4_generic_response( + GenericResponse[PongResponse] + ) + + result = construct_open_api_with_schema_class(open_api_4) + assert result.components is not None + assert result.components.schemas is not None + assert "GenericResponse_PongResponse_" in result.components.schemas + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="generic type for Pydantic V2") +def test_construct_open_api_with_schema_class_4_generic_response_v2() -> None: + DataT = TypeVar("DataT") + + class GenericResponse(BaseModel, Generic[DataT]): + msg: str = Field(description="message of the generic response") + data: DataT = Field(description="data value of the generic response") + + open_api_4 = construct_base_open_api_4_generic_response( + GenericResponse[PongResponse] + ) + + result = construct_open_api_with_schema_class(open_api_4) + assert result.components is not None + assert result.components.schemas is not None + assert "GenericResponse_PongResponse_" in result.components.schemas + + +def construct_base_open_api_1() -> OpenAPI: + model_validate: Callable[[dict], OpenAPI] = getattr( + OpenAPI, "model_validate" if PYDANTIC_V2 else "parse_obj" + ) + return model_validate( + { + "info": {"title": "My own API", "version": "v0.0.1"}, + "paths": { + "/ping": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": PydanticSchema(schema_class=PingRequest) + } + } + }, + "responses": { + "200": { + "description": "pong", + "content": { + "application/json": { + "schema": PydanticSchema( + schema_class=PingResponse + ) + } + }, + } + }, + } + } + }, + } + ) + + +def construct_base_open_api_2() -> OpenAPI: + return OpenAPI( + info=Info( + title="My own API", + version="v0.0.1", + ), + paths={ + "/ping": PathItem( + post=Operation( + requestBody=RequestBody( + content={ + "application/json": MediaType( + media_type_schema=Reference( + **{"$ref": "#/components/schemas/PingRequest"} + ) + ) + } + ), + responses={ + "200": Response( + description="pong", + content={ + "application/json": MediaType( + media_type_schema=Reference( + **{"$ref": "#/components/schemas/PingResponse"} + ) + ) + }, + ) + }, + ) + ) + }, + ) + + +def construct_base_open_api_3() -> OpenAPI: + return OpenAPI( + info=Info( + title="My own API", + version="v0.0.1", + ), + paths={ + "/ping": PathItem( + post=Operation( + requestBody=RequestBody( + content={ + "application/json": MediaType( + media_type_schema=PydanticSchema( + schema_class=PingRequest + ) + ) + } + ), + responses={ + "200": Response( + description="pong", + content={ + "application/json": MediaType( + media_type_schema=PydanticSchema( + schema_class=PongResponse + ) + ) + }, + ) + }, + ) + ) + }, + ) + + +def construct_base_open_api_4_generic_response(response_schema: type) -> OpenAPI: + return OpenAPI( + info=Info( + title="My own API", + version="v0.0.1", + ), + paths={ + "/ping": PathItem( + post=Operation( + requestBody=RequestBody( + content={ + "application/json": MediaType( + media_type_schema=PydanticSchema( + schema_class=PingRequest + ) + ) + } + ), + responses={ + "200": Response( + description="pong", + content={ + "application/json": MediaType( + media_type_schema=PydanticSchema( + schema_class=response_schema + ) + ) + }, + ) + }, + ) + ) + }, + ) + + +class PingRequest(BaseModel): + """Ping Request""" + + req_foo: str = Field(description="foo value of the request") + req_bar: str = Field(description="bar value of the request") + + +class PingResponse(BaseModel): + """Ping response""" + + resp_foo: str = Field(description="foo value of the response") + resp_bar: str = Field(description="bar value of the response") + + +class PongResponse(BaseModel): + """Pong response""" + + resp_foo: str = Field(alias="pong_foo", description="foo value of the response") + resp_bar: str = Field(alias="pong_bar", description="bar value of the response") diff --git a/tests/util/test_validated_schema.py b/tests/util/test_validated_schema.py new file mode 100644 index 0000000..b4ab0ed --- /dev/null +++ b/tests/util/test_validated_schema.py @@ -0,0 +1,117 @@ +# mypy: ignore-errors + +import sys +from typing import Any, Optional + +import pytest +from openapi_spec_validator import validate +from pydantic import BaseModel + +from openapi_pydantic import ( + DataType, + Header, + Info, + MediaType, + OpenAPI, + Operation, + PathItem, + RequestBody, + Response, + Schema, +) +from openapi_pydantic.compat import PYDANTIC_V2 +from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class + +if sys.version_info < (3, 9): + from typing_extensions import Annotated, Literal +else: + from typing import Annotated, Literal + + +def test_basic_schema() -> None: + class SampleModel(BaseModel): + required: bool + optional: Optional[bool] = None + one_literal_choice: Literal["only_choice"] + multiple_literal_choices: Literal["choice1", "choice2"] + + part_api = construct_sample_api(SampleModel) + + api = construct_open_api_with_schema_class(part_api) + assert api.components is not None + assert api.components.schemas is not None + + if PYDANTIC_V2: + json_api: Any = api.model_dump(mode="json", by_alias=True, exclude_none=True) + else: + json_api: Any = api.dict(by_alias=True, exclude_none=True) + validate(json_api) + + +@pytest.mark.skipif( + not PYDANTIC_V2, + reason="Field-level JSON examples not supported in Pydantic V1", +) +def test_field_json_schema_example() -> None: + from pydantic import WithJsonSchema + + Thing = Annotated[str, WithJsonSchema({"examples": ["thing1"]})] + + class SampleModel(BaseModel): + a: Thing + + part_api = construct_sample_api(SampleModel) + + api = construct_open_api_with_schema_class(part_api) + assert api.components is not None + assert api.components.schemas is not None + + json_api: Any = api.model_dump(mode="json", by_alias=True, exclude_none=True) + props = json_api["components"]["schemas"]["SampleRequest"]["properties"] + assert props["a"]["examples"] == ["thing1"] + + validate(json_api) + + +def construct_sample_api(SampleModel) -> OpenAPI: + class SampleRequest(SampleModel): + model_config = {"json_schema_mode": "validation"} + + class SampleResponse(SampleModel): + model_config = {"json_schema_mode": "serialization"} + + return OpenAPI( + info=Info( + title="Sample API", + version="v0.0.1", + ), + paths={ + "/callme": PathItem( + post=Operation( + requestBody=RequestBody( + content={ + "application/json": MediaType( + schema=PydanticSchema(schema_class=SampleRequest) + ) + } + ), + responses={ + "200": Response( + description="resp", + headers={ + "WWW-Authenticate": Header( + description="Indicate how to authenticate", + schema=Schema(type=DataType.STRING), + ) + }, + content={ + "application/json": MediaType( + schema=PydanticSchema(schema_class=SampleResponse) + ) + }, + ) + }, + ) + ) + }, + ) diff --git a/tests/v3_0/__init__.py b/tests/v3_0/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/v3_0/test_config_example.py b/tests/v3_0/test_config_example.py new file mode 100644 index 0000000..7f37594 --- /dev/null +++ b/tests/v3_0/test_config_example.py @@ -0,0 +1,93 @@ +from typing import Any + +from openapi_pydantic.compat import PYDANTIC_V2 +from openapi_pydantic.v3.v3_0 import ( + XML, + Callback, + Components, + Contact, + Discriminator, + Encoding, + Example, + ExternalDocumentation, + Header, + Info, + License, + Link, + MediaType, + OAuthFlow, + OAuthFlows, + OpenAPI, + Operation, + Parameter, + PathItem, + Paths, + Reference, + RequestBody, + Response, + Responses, + Schema, + SecurityRequirement, + SecurityScheme, + Server, + ServerVariable, + Tag, +) + + +def test_config_example() -> None: + all_types = [ + OpenAPI, + Info, + Contact, + License, + Server, + ServerVariable, + Components, + Paths, + PathItem, + Operation, + ExternalDocumentation, + Parameter, + RequestBody, + MediaType, + Encoding, + Responses, + Response, + Callback, + Example, + Link, + Header, + Tag, + Reference, + Schema, + Discriminator, + XML, + SecurityScheme, + OAuthFlows, + OAuthFlow, + SecurityRequirement, + ] + for schema_type in all_types: + _assert_config_examples(schema_type) + + +def _assert_config_examples(schema_type: Any) -> None: + if PYDANTIC_V2: + if not hasattr(schema_type, "model_config"): + return + extra = schema_type.model_config.get("json_schema_extra") + if extra is not None: + examples = extra["examples"] + for example_dict in examples: + obj = schema_type.model_validate(example_dict) + assert obj.model_fields_set + + else: + Config = getattr(schema_type, "Config", None) + schema_extra = getattr(Config, "schema_extra", None) + if schema_extra is not None: + examples = schema_extra["examples"] + for example_dict in examples: + obj = schema_type(**example_dict) + assert obj.__fields_set__ diff --git a/tests/v3_0/test_datatype.py b/tests/v3_0/test_datatype.py new file mode 100644 index 0000000..c5afe97 --- /dev/null +++ b/tests/v3_0/test_datatype.py @@ -0,0 +1,34 @@ +import pytest +from pydantic import ValidationError + +from openapi_pydantic.v3.v3_0 import Schema + + +@pytest.mark.parametrize( + "datatype", + ( + "string", + "number", + "integer", + "boolean", + "array", + "object", + ), +) +def test_good_types_parse_and_equate(datatype: str) -> None: + assert Schema(type=datatype).type == datatype + + +def test_bad_types_raise_validation_errors() -> None: + with pytest.raises(ValidationError): + Schema(type="invalid") + + with pytest.raises(ValidationError): + Schema(anyOf=[{"type": "invalid"}]) + + with pytest.raises(ValidationError): + Schema( + properties={ + "a": Schema(type="invalid"), + }, + ) diff --git a/tests/v3_0/test_optional_and_computed.py b/tests/v3_0/test_optional_and_computed.py new file mode 100644 index 0000000..0518929 --- /dev/null +++ b/tests/v3_0/test_optional_and_computed.py @@ -0,0 +1,117 @@ +# mypy: ignore-errors + +from typing import Optional + +import pytest + +from openapi_pydantic.compat import PYDANTIC_V2 +from openapi_pydantic.v3.v3_0 import ( + Info, + MediaType, + OpenAPI, + Operation, + PathItem, + RequestBody, + Response, + Schema, +) +from openapi_pydantic.v3.v3_0.util import ( + PydanticSchema, + construct_open_api_with_schema_class, +) + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="computed fields require Pydantic V2") +def test_optional_and_computed_fields() -> None: + api = construct_sample_api() + + result = construct_open_api_with_schema_class(api) + assert result.components is not None + assert result.components.schemas is not None + + req_schema = result.components.schemas["SampleRequest"] + assert isinstance(req_schema, Schema) + assert req_schema.properties is not None + assert req_schema.required is not None + + resp_schema = result.components.schemas["SampleResponse"] + assert isinstance(resp_schema, Schema) + assert resp_schema.properties is not None + assert resp_schema.required is not None + + # When validating: + # - required fields are still required + # - optional fields are still optional + # - computed fields don't exist + assert "req" in req_schema.properties + assert "opt" in req_schema.properties + assert "comp" not in req_schema.properties + assert set(req_schema.required) == {"req"} + + # When serializing: + # - required fields are still required + # - optional fields are still optional + # (except when json_schema_serialization_defaults_required is enabled) + # - computed fields are required + assert "req" in resp_schema.properties + assert "opt" in resp_schema.properties + assert "comp" in resp_schema.properties + assert set(resp_schema.required) == {"req", "comp"} + + +def construct_sample_api() -> OpenAPI: + from typing import TYPE_CHECKING, Callable + + from pydantic import BaseModel + + if TYPE_CHECKING: + + def computed_field(x: Callable) -> Callable: ... + + else: + from pydantic import computed_field + + class SampleModel(BaseModel): + req: bool + opt: Optional[bool] = None + + @computed_field # type: ignore + @property + def comp(self) -> bool: + return True + + class SampleRequest(SampleModel): + model_config = {"json_schema_mode": "validation"} + + class SampleResponse(SampleModel): + model_config = {"json_schema_mode": "serialization"} + + return OpenAPI( + info=Info( + title="Sample API", + version="v0.0.1", + ), + paths={ + "/callme": PathItem( + post=Operation( + requestBody=RequestBody( + content={ + "application/json": MediaType( + schema=PydanticSchema(schema_class=SampleRequest) + ) + } + ), + responses={ + "200": Response( + description="resp", + content={ + "application/json": MediaType( + schema=PydanticSchema(schema_class=SampleResponse) + ) + }, + ) + }, + ) + ) + }, + ) diff --git a/tests/v3_0/test_util.py b/tests/v3_0/test_util.py new file mode 100644 index 0000000..e752dc2 --- /dev/null +++ b/tests/v3_0/test_util.py @@ -0,0 +1,330 @@ +import logging +from typing import Callable, Generic, Literal, TypeVar + +import pytest +from pydantic import BaseModel, Field + +from openapi_pydantic.compat import PYDANTIC_V2 +from openapi_pydantic.v3.v3_0 import ( + Info, + MediaType, + OpenAPI, + Operation, + PathItem, + Reference, + RequestBody, + Response, + Schema, +) +from openapi_pydantic.v3.v3_0.util import ( + PydanticSchema, + construct_open_api_with_schema_class, +) + + +def test_construct_open_api_with_schema_class_1() -> None: + open_api = construct_base_open_api_1() + result_open_api_1 = construct_open_api_with_schema_class(open_api) + result_open_api_2 = construct_open_api_with_schema_class( + open_api, [PingRequest, PingResponse] + ) + assert result_open_api_1.components == result_open_api_2.components + assert result_open_api_1 == result_open_api_2 + + dump_json = getattr(result_open_api_1, "model_dump_json" if PYDANTIC_V2 else "json") + open_api_json = dump_json(by_alias=True, exclude_none=True, indent=2) + logging.debug(open_api_json) + + +def test_construct_open_api_with_schema_class_2() -> None: + open_api_1 = construct_base_open_api_1() + open_api_2 = construct_base_open_api_2() + result_open_api_1 = construct_open_api_with_schema_class(open_api_1) + result_open_api_2 = construct_open_api_with_schema_class( + open_api_2, [PingRequest, PingResponse] + ) + assert result_open_api_1 == result_open_api_2 + + +def test_construct_open_api_with_schema_class_3() -> None: + open_api_3 = construct_base_open_api_3() + + result_with_alias_1 = construct_open_api_with_schema_class(open_api_3) + assert result_with_alias_1.components is not None + assert result_with_alias_1.components.schemas is not None + schema_with_alias = result_with_alias_1.components.schemas["PongResponse"] + assert isinstance(schema_with_alias, Schema) + assert schema_with_alias.properties is not None + assert "pong_foo" in schema_with_alias.properties + assert "pong_bar" in schema_with_alias.properties + + result_with_alias_2 = construct_open_api_with_schema_class( + open_api_3, by_alias=True + ) + assert result_with_alias_1 == result_with_alias_2 + + result_without_alias = construct_open_api_with_schema_class( + open_api_3, by_alias=False + ) + assert result_without_alias.components is not None + assert result_without_alias.components.schemas is not None + schema_without_alias = result_without_alias.components.schemas["PongResponse"] + assert isinstance(schema_without_alias, Schema) + assert schema_without_alias.properties is not None + assert "resp_foo" in schema_without_alias.properties + assert "resp_bar" in schema_without_alias.properties + + +@pytest.mark.skipif(PYDANTIC_V2, reason="generic type for Pydantic V1") +def test_construct_open_api_with_schema_class_4_generic_response_v1() -> None: + DataT = TypeVar("DataT") + from pydantic.v1.generics import GenericModel + + class GenericResponse(GenericModel, Generic[DataT]): + msg: str = Field(description="message of the generic response") + data: DataT = Field(description="data value of the generic response") + + open_api_4 = construct_base_open_api_4_generic_response( + GenericResponse[PongResponse] + ) + + result = construct_open_api_with_schema_class(open_api_4) + assert result.components is not None + assert result.components.schemas is not None + assert "GenericResponse_PongResponse_" in result.components.schemas + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="generic type for Pydantic V2") +def test_construct_open_api_with_schema_class_4_generic_response() -> None: + DataT = TypeVar("DataT") + + class GenericResponse(BaseModel, Generic[DataT]): + msg: str = Field(description="message of the generic response") + data: DataT = Field(description="data value of the generic response") + + open_api_4 = construct_base_open_api_4_generic_response( + GenericResponse[PongResponse] + ) + + result = construct_open_api_with_schema_class(open_api_4) + assert result.components is not None + assert result.components.schemas is not None + assert "GenericResponse_PongResponse_" in result.components.schemas + + +def construct_base_open_api_1() -> OpenAPI: + model_validate: Callable[[dict], OpenAPI] = getattr( + OpenAPI, "model_validate" if PYDANTIC_V2 else "parse_obj" + ) + return model_validate( + { + "info": {"title": "My own API", "version": "v0.0.1"}, + "paths": { + "/ping": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": PydanticSchema(schema_class=PingRequest) + } + } + }, + "responses": { + "200": { + "description": "pong", + "content": { + "application/json": { + "schema": PydanticSchema( + schema_class=PingResponse + ) + } + }, + } + }, + } + } + }, + } + ) + + +def construct_base_open_api_2() -> OpenAPI: + return OpenAPI( + info=Info(title="My own API", version="v0.0.1"), + paths={ + "/ping": PathItem( + post=Operation( + requestBody=RequestBody( + content={ + "application/json": MediaType( + media_type_schema=Reference( + **{"$ref": "#/components/schemas/PingRequest"} + ) + ) + } + ), + responses={ + "200": Response( + description="pong", + content={ + "application/json": MediaType( + media_type_schema=Reference( + **{"$ref": "#/components/schemas/PingResponse"} + ) + ) + }, + ) + }, + ) + ) + }, + ) + + +def construct_base_open_api_3() -> OpenAPI: + return OpenAPI( + info=Info( + title="My own API", + version="v0.0.1", + ), + paths={ + "/ping": PathItem( + post=Operation( + requestBody=RequestBody( + content={ + "application/json": MediaType( + media_type_schema=PydanticSchema( + schema_class=PingRequest + ) + ) + } + ), + responses={ + "200": Response( + description="pong", + content={ + "application/json": MediaType( + media_type_schema=PydanticSchema( + schema_class=PongResponse + ) + ) + }, + ) + }, + ) + ) + }, + ) + + +def construct_base_open_api_3_plus() -> OpenAPI: + return OpenAPI( + info=Info( + title="My own API", + version="v0.0.1", + ), + paths={ + "/ping": PathItem( + post=Operation( + requestBody=RequestBody( + content={ + "application/json": MediaType( + media_type_schema=PydanticSchema( + schema_class=PingPlusRequest + ) + ) + } + ), + responses={ + "200": Response( + description="pong", + content={ + "application/json": MediaType( + media_type_schema=PydanticSchema( + schema_class=PongResponse + ) + ) + }, + ) + }, + ) + ) + }, + ) + + +def construct_base_open_api_4_generic_response(response_schema: type) -> OpenAPI: + return OpenAPI( + info=Info( + title="My own API", + version="v0.0.1", + ), + paths={ + "/ping": PathItem( + post=Operation( + requestBody=RequestBody( + content={ + "application/json": MediaType( + media_type_schema=PydanticSchema( + schema_class=PingRequest + ) + ) + } + ), + responses={ + "200": Response( + description="pong", + content={ + "application/json": MediaType( + media_type_schema=PydanticSchema( + schema_class=response_schema + ) + ) + }, + ) + }, + ) + ) + }, + ) + + +class PingRequest(BaseModel): + """Ping Request""" + + req_foo: str = Field(description="foo value of the request") + req_bar: str = Field(description="bar value of the request") + + +class PingResponse(BaseModel): + """Ping response""" + + resp_foo: str = Field(description="foo value of the response") + resp_bar: str = Field(description="bar value of the response") + + +class PongResponse(BaseModel): + """Pong response""" + + resp_foo: str = Field(alias="pong_foo", description="foo value of the response") + resp_bar: str = Field(alias="pong_bar", description="bar value of the response") + + +class PingPlusRequest(BaseModel): + """Ping Request with extra""" + + req_foo: str + req_bar: str + req_single_choice: Literal["one"] + + +def test_enum_with_single_choice() -> None: + api_obj = construct_open_api_with_schema_class(construct_base_open_api_3_plus()) + model_dump = getattr(api_obj, "model_dump" if PYDANTIC_V2 else "dict") + api = model_dump(by_alias=True, exclude_none=True) + schema = api["components"]["schemas"]["PingPlusRequest"] + prop = schema["properties"]["req_single_choice"] + # OpenAPI 3.0 does not support "const", so make sure the enum is not + # rendered that way. + assert not prop.get("const") + assert prop["enum"] == ["one"] diff --git a/tests/v3_0/test_validated_schema.py b/tests/v3_0/test_validated_schema.py new file mode 100644 index 0000000..113248f --- /dev/null +++ b/tests/v3_0/test_validated_schema.py @@ -0,0 +1,97 @@ +# mypy: ignore-errors + +import sys +from typing import Any, Optional + +from openapi_spec_validator import validate +from pydantic import BaseModel + +from openapi_pydantic.compat import PYDANTIC_V2 +from openapi_pydantic.v3.v3_0 import ( + Components, + DataType, + Example, + Header, + Info, + MediaType, + OpenAPI, + Operation, + PathItem, + RequestBody, + Response, + Schema, +) +from openapi_pydantic.v3.v3_0.util import ( + PydanticSchema, + construct_open_api_with_schema_class, +) + +if sys.version_info < (3, 9): + from typing_extensions import Literal +else: + from typing import Literal + + +def test_basic_schema() -> None: + class SampleModel(BaseModel): + required: bool + optional: Optional[bool] = None + one_literal_choice: Literal["only_choice"] + multiple_literal_choices: Literal["choice1", "choice2"] + + part_api = construct_sample_api(SampleModel) + + api = construct_open_api_with_schema_class(part_api) + assert api.components is not None + assert api.components.schemas is not None + + if PYDANTIC_V2: + json_api: Any = api.model_dump(mode="json", by_alias=True, exclude_none=True) + else: + json_api: Any = api.dict(by_alias=True, exclude_none=True) + validate(json_api) + + +def construct_sample_api(SampleModel) -> OpenAPI: + class SampleRequest(SampleModel): + model_config = {"json_schema_mode": "validation"} + + class SampleResponse(SampleModel): + model_config = {"json_schema_mode": "serialization"} + + return OpenAPI( + info=Info( + title="Sample API", + version="v0.0.1", + ), + paths={ + "/callme": PathItem( + post=Operation( + requestBody=RequestBody( + content={ + "application/json": MediaType( + schema=PydanticSchema(schema_class=SampleRequest) + ) + } + ), + responses={ + "200": Response( + description="resp", + headers={ + "WWW-Authenticate": Header( + description="Indicate how to authenticate", + schema=Schema(type=DataType.STRING), + ) + }, + content={ + "application/json": MediaType( + schema=PydanticSchema(schema_class=SampleResponse) + ) + }, + ) + }, + ) + ) + }, + components=Components(examples={"thing-example": Example(value="thing1")}), + ) diff --git a/tests/v3_1/__init__.py b/tests/v3_1/__init__.py new file mode 100644 index 0000000..0699444 --- /dev/null +++ b/tests/v3_1/__init__.py @@ -0,0 +1,6 @@ +from openapi_pydantic.v3.v3_1.schema import Schema, schema_validate + + +def test_empty_schema() -> None: + schema = schema_validate({}) + assert schema == Schema() diff --git a/tests/v3_1/test_datatype.py b/tests/v3_1/test_datatype.py new file mode 100644 index 0000000..242d05a --- /dev/null +++ b/tests/v3_1/test_datatype.py @@ -0,0 +1,35 @@ +import pytest +from pydantic import ValidationError + +from openapi_pydantic.v3.v3_1 import Schema + + +@pytest.mark.parametrize( + "datatype", + ( + "string", + "number", + "integer", + "boolean", + "array", + "object", + "null", + ), +) +def test_good_types_parse_and_equate(datatype: str) -> None: + assert Schema(type=datatype).type == datatype + + +def test_bad_types_raise_validation_errors() -> None: + with pytest.raises(ValidationError): + Schema(type="invalid") + + with pytest.raises(ValidationError): + Schema(anyOf=[{"type": "invalid"}]) + + with pytest.raises(ValidationError): + Schema( + properties={ + "a": Schema(type="invalid"), + }, + ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..14d85c1 --- /dev/null +++ b/tox.ini @@ -0,0 +1,38 @@ +[tox] +min_version = 4.0 +env_list = format, lint, py{38,39,310,311,312}-pydantic{1,2}-{test,type} + +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: format, lint, py312 + +[testenv] +labels = core +allowlist_externals = poetry +# The "pydanticX:", "test:", and "type:" prefixes are Tox factor-conditional settings. +# https://tox.wiki/en/3.4.0/config.html?highlight=conditional#factors-and-factor-conditional-settings +# Note that "poetry add" changes pyproject.toml, but at least we +# change it back when the tests finish. +commands_pre = + pydantic1: poetry add --lock pydantic<2 + pydantic2: poetry add --lock pydantic>=1.8 + poetry install --no-root --all-extras +commands = + test: poetry run pytest -vv tests + type: poetry run mypy openapi_pydantic tests + +[testenv:format] +allowlist_externals = poetry +commands_pre = poetry install --only dev --no-root +commands = + poetry run black --check openapi_pydantic tests + +[testenv:lint] +allowlist_externals = poetry +commands_pre = poetry install --only dev --no-root +commands = + poetry run ruff check openapi_pydantic tests