├── src └── aiopenapi3 │ ├── me.py │ ├── version.py │ ├── v31 │ ├── tag.py │ ├── xml.py │ ├── example.py │ ├── media.py │ ├── components.py │ ├── __init__.py │ ├── root.py │ ├── info.py │ ├── parameter.py │ ├── general.py │ └── servers.py │ ├── __main__.py │ ├── extra │ └── __init__.py │ ├── v30 │ ├── xml.py │ ├── tag.py │ ├── example.py │ ├── root.py │ ├── info.py │ ├── components.py │ ├── media.py │ ├── __init__.py │ ├── general.py │ ├── servers.py │ └── security.py │ ├── v20 │ ├── xml.py │ ├── tag.py │ ├── __init__.py │ ├── info.py │ ├── root.py │ ├── general.py │ ├── security.py │ ├── schemas.py │ └── paths.py │ ├── __init__.py │ ├── json.py │ ├── debug.py │ ├── _types.py │ └── log.py ├── .pep8 ├── docs ├── requirements.txt ├── source │ ├── links.rst │ ├── install.rst │ ├── toc.rst │ ├── extra.rst │ ├── _ext │ │ └── aioai3.py │ ├── _static │ │ └── my_custom.js │ └── index.rst ├── Makefile └── make.bat ├── tests ├── data │ └── dog.png ├── fixtures │ ├── parsing-paths-invalid.yaml │ ├── schema-type-list.yaml │ ├── schema-string-pattern.yaml │ ├── schema-enum-array.yaml │ ├── parsing-schema-properties-name-empty.yaml │ ├── schema-baseurl-v20.yaml │ ├── schema-properties-default.yaml │ ├── parsing-paths-response-ref-invalid.yaml │ ├── schema-enum-object.yaml │ ├── schema-type-missing.yaml │ ├── schema-date-types.yaml │ ├── schema-self-recursion.yaml │ ├── schema-constraints.yaml │ ├── schema-type-string-format-byte-base64.yaml │ ├── schema-property-name-is-type.yaml │ ├── schema-boolean-v20.yaml │ ├── plugin-base.yaml │ ├── schema-array.yaml │ ├── schema-patternProperties.yaml │ ├── schema-title-name-collision.yaml │ ├── schema-type-validators.yaml │ ├── schema-create-update-read.yaml │ ├── schema-oneOf-mixed.yaml │ ├── schema-regex-engine.yaml │ ├── parsing-paths-parameter-name-with-underscores.yaml │ ├── schema-enum.yaml │ ├── schema-discriminated-union-invalid-array.yaml │ ├── paths-response-content-empty.yaml │ ├── schema-oneOf-properties.yaml │ ├── schema-discriminated-union-discriminator-name.yaml │ ├── parsing-paths-operationid-duplicate.yaml │ ├── schema-additionalProperties-and-named-properties.yaml │ ├── schema-yaml12-tags.yaml │ ├── schema-oneOf-nullable-v31.yaml │ ├── paths-content-schema-property-without-properties.yaml │ ├── paths-response-content-empty-v20.yaml │ ├── parsing-paths-parameter-name-mismatch.yaml │ ├── parsing-paths-content-schema-float-validation.yaml │ ├── schema-oneOf-nullable-v30.yaml │ ├── paths-parameter-name-invalid.yaml │ ├── README.md │ ├── paths-response-header-v20.yaml │ ├── paths-tags.yaml │ ├── parsing-paths-content-schema-object.yaml │ ├── parsing-schema-names.yaml │ ├── paths-parameter-missing.yaml │ ├── extra-cookie.yaml │ ├── parsing-paths-links-invalid.yaml │ ├── paths-servers.yaml │ ├── schema-empty.yaml │ ├── paths-response-content-type-octet.yaml │ ├── schema-oneOf.yaml │ ├── schema-discriminated-union-extends.yaml │ ├── paths-parameter-format-complex.yaml │ ├── schema-allof-string.yaml │ ├── schema-pathitems.yaml │ ├── paths-response-error-v20.yaml │ ├── schema-nullable-v31.yaml │ ├── schema-nullable-v30.yaml │ ├── schema-discriminated-union.yaml │ ├── paths-response-status-pattern-default.yaml │ ├── schema-discriminated-union-merge.yaml │ ├── paths-parameter-default.yaml │ ├── paths-response-error.yaml │ ├── schema-anyOf.yaml │ ├── paths-parameters-oneOf.yaml │ ├── paths-server-variables.yaml │ ├── parsing-paths-content-nested-array-ref.yaml │ ├── paths-parameters.yaml │ ├── paths-response-header.yaml │ ├── schema-discriminated-union-warning.yaml │ ├── schema-Of-parent-properties.yaml │ ├── parsing-paths-links.yaml │ ├── schema-ref-nesting.yaml │ ├── schema-recursion.yaml │ ├── schema-additionalProperties.yaml │ ├── schema-boolean.yaml │ ├── schema-additionalProperties-v20.yaml │ ├── schema-extensions.yaml │ ├── extra-reduced.yaml │ ├── paths-callbacks.yaml │ ├── schema-allof-oneof-combined.yaml │ ├── schema-discriminated-union-deep.yaml │ ├── schema-allof-discriminator.yaml │ ├── paths-parameter-format-v20.yaml │ ├── paths-security-v20.yaml │ └── paths-security.yaml ├── api │ ├── v1 │ │ ├── schema.py │ │ └── main.py │ ├── main.py │ └── v2 │ │ ├── schema.py │ │ └── main.py ├── clone_test.py ├── pickle_test.py ├── parse_data_test.py ├── debug_test.py ├── error_test.py ├── cli_test.py ├── loader_test.py ├── formdata_test.py ├── content_length_test.py ├── apiv1_test.py └── plugin_test.py ├── .gitignore ├── renovate.json ├── .github ├── SECURITY.md ├── dependabot.yml ├── workflows │ ├── publish.yml │ ├── codecov.yml │ └── codeql-analysis.yml └── ISSUE_TEMPLATE │ ├── feature_request.yml │ └── bug_report.yml ├── .readthedocs.yaml ├── requirements.txt ├── LICENSE ├── .pre-commit-config.yaml └── README.md /src/aiopenapi3/me.py: -------------------------------------------------------------------------------- 1 | __all__: list[str] = [] 2 | -------------------------------------------------------------------------------- /.pep8: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | ignore = E221,E501,E731 3 | -------------------------------------------------------------------------------- /src/aiopenapi3/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.9.0" 2 | -------------------------------------------------------------------------------- /src/aiopenapi3/v31/tag.py: -------------------------------------------------------------------------------- 1 | from ..v30.tag import Tag 2 | -------------------------------------------------------------------------------- /src/aiopenapi3/v31/xml.py: -------------------------------------------------------------------------------- 1 | from ..v30.xml import XML 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | docutils<0.22 2 | sphinx-rtd-theme 3 | sphinx_autodoc_typehints 4 | -------------------------------------------------------------------------------- /tests/data/dog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/commonism/aiopenapi3/HEAD/tests/data/dog.png -------------------------------------------------------------------------------- /tests/fixtures/parsing-paths-invalid.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: This is broken on purpose 4 | paths: 5 | -------------------------------------------------------------------------------- /src/aiopenapi3/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from .cli import main 3 | 4 | if __name__ == "__main__": 5 | main(sys.argv[1:]) 6 | -------------------------------------------------------------------------------- /src/aiopenapi3/extra/__init__.py: -------------------------------------------------------------------------------- 1 | from .reduce import Cull, Reduce 2 | from .cookies import Cookies 3 | 4 | __all__ = ["Cull", "Reduce", "Cookies"] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | openapi.yaml 2 | openapi.json 3 | *.swp 4 | *.pyc 5 | build/* 6 | dist/* 7 | .idea 8 | tests/data/ 9 | tests/my_*.py 10 | docs/build/ 11 | .pdm-python 12 | *.egg-info 13 | *.ipynb 14 | -------------------------------------------------------------------------------- /tests/fixtures/schema-type-list.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: OpenAPI schema where the type is a list 5 | 6 | components: 7 | schemas: 8 | Any: 9 | type: ["string", "number"] 10 | -------------------------------------------------------------------------------- /tests/fixtures/schema-string-pattern.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: string pattern test 5 | 6 | components: 7 | schemas: 8 | GUID: 9 | type: string 10 | pattern: "^[0-9A-Fa-f]{4}([0-9A-Fa-f]{4}-){4}[0-9A-Fa-f]{12}$" 11 | -------------------------------------------------------------------------------- /docs/source/links.rst: -------------------------------------------------------------------------------- 1 | .. |aiopenapi3| replace:: **aiopenapi3** 2 | .. _OpenAPI: https://github.com/OAI/OpenAPI-Specification/ 3 | .. _pydantic: https://github.com/pydantic/pydantic 4 | .. _httpx: https://github.com/encode/httpx 5 | .. _httpx-auth: https://github.com/Colin-b/httpx_auth 6 | -------------------------------------------------------------------------------- /tests/fixtures/schema-enum-array.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: enum with complex type - not supported 5 | 6 | components: 7 | schemas: 8 | obj: 9 | type: array 10 | items: 11 | type: integer 12 | enum: 13 | - [1, 2, 3] 14 | -------------------------------------------------------------------------------- /tests/fixtures/parsing-schema-properties-name-empty.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: with properties 5 | 6 | paths: {} 7 | 8 | components: 9 | schemas: 10 | shared-user: 11 | type: object 12 | properties: 13 | "": 14 | type: string 15 | -------------------------------------------------------------------------------- /tests/fixtures/schema-baseurl-v20.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: baseurl with port 4 | description: baseurl with port 5 | version: 1.0.0 6 | host: api.example.com:81 7 | basePath: /v1 8 | schemes: 9 | - https 10 | 11 | consumes: [] 12 | produces: [] 13 | paths: {} 14 | definitions: {} 15 | -------------------------------------------------------------------------------- /tests/fixtures/schema-properties-default.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: test 5 | 6 | paths: {} 7 | 8 | components: 9 | schemas: 10 | Number: 11 | type: object 12 | properties: 13 | code: 14 | type: number 15 | default: 1 16 | -------------------------------------------------------------------------------- /tests/fixtures/parsing-paths-response-ref-invalid.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: This has a broken $ref in it 5 | paths: 6 | /example: 7 | get: 8 | operationId: hasBrokenRef 9 | responses: 10 | '200': 11 | $ref: '#/components/responses/Missing' 12 | -------------------------------------------------------------------------------- /tests/fixtures/schema-enum-object.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: enum with complex type - not supported 5 | 6 | components: 7 | schemas: 8 | obj: 9 | type: object 10 | properties: 11 | id: 12 | type: integer 13 | enum: 14 | - {id: 1} 15 | - [1, 2, 3] 16 | -------------------------------------------------------------------------------- /tests/fixtures/schema-type-missing.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: OpenAPI schema where the type is missing 5 | 6 | components: 7 | schemas: 8 | Any: 9 | properties: 10 | id: 11 | type: integer 12 | Ref: 13 | type: object 14 | allOf: 15 | - $ref: "#/components/schemas/Any" 16 | -------------------------------------------------------------------------------- /tests/fixtures/schema-date-types.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: date-time 5 | 6 | components: 7 | schemas: 8 | Integer: 9 | type: integer 10 | format: date-time 11 | 12 | Number: 13 | type: number 14 | format: date-time 15 | 16 | String: 17 | type: string 18 | format: date-time 19 | -------------------------------------------------------------------------------- /tests/fixtures/schema-self-recursion.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: OpenAPI schema with Self recursion 5 | 6 | components: 7 | schemas: 8 | Any: 9 | type: ["string", "number", "object"] 10 | $ref: '#/components/schemas/Any' 11 | 12 | Self: 13 | type: "object" 14 | $ref: '#/components/schemas/Self' 15 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "enabledManagers": ["pep621"], 4 | "extends": [ 5 | "config:recommended", 6 | "group:allNonMajor" 7 | ], 8 | "lockFileMaintenance": { 9 | "enabled": true 10 | }, 11 | "packageRules": [ 12 | { 13 | "matchManagers": ["pep621"] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /docs/source/install.rst: -------------------------------------------------------------------------------- 1 | .. include:: links.rst 2 | 3 | ************ 4 | Installation 5 | ************ 6 | 7 | .. code:: bash 8 | 9 | $ pip install aiopenapi3 10 | 11 | 12 | * aiopenapi3[auth] will install httpx-auth_ which is required to authenticate using oauth2/azuread/. Currently httpx-auth is `limited to Sync `_ operations. 13 | -------------------------------------------------------------------------------- /tests/fixtures/schema-constraints.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: enum test 5 | 6 | components: 7 | schemas: 8 | A: 9 | type: string 10 | minLength: 5 11 | maxLength: 10 12 | B: 13 | type: number 14 | exclusiveMinimum: 0 15 | exclusiveMaximum: 10 16 | C: 17 | type: number 18 | multipleOf: 2 19 | -------------------------------------------------------------------------------- /tests/fixtures/schema-type-string-format-byte-base64.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: OpenAPI schema where the type is base64 5 | 6 | components: 7 | schemas: 8 | Base64Root: 9 | type: string 10 | format: byte 11 | Base64Property: 12 | type: object 13 | properties: 14 | data: 15 | type: string 16 | format: byte 17 | -------------------------------------------------------------------------------- /tests/fixtures/schema-property-name-is-type.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Any OpenAPI schema where a property name matches a schema 5 | 6 | components: 7 | schemas: 8 | A: 9 | type: object 10 | properties: 11 | B: 12 | $ref: '#/components/schemas/B' 13 | 14 | B: 15 | type: object 16 | properties: 17 | data: 18 | type: string 19 | -------------------------------------------------------------------------------- /tests/fixtures/schema-boolean-v20.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: baseurl with port 4 | description: baseurl with port 5 | version: 1.0.0 6 | host: api.example.com:81 7 | basePath: /v1 8 | schemes: 9 | - https 10 | 11 | consumes: [] 12 | produces: [] 13 | paths: {} 14 | definitions: 15 | A: 16 | type: object 17 | additionalProperties: true 18 | 19 | B: 20 | type: object 21 | additionalProperties: false 22 | -------------------------------------------------------------------------------- /docs/source/toc.rst: -------------------------------------------------------------------------------- 1 | ################# 2 | Table of contents 3 | ################# 4 | 5 | .. toctree:: 6 | :maxdepth: 3 7 | :caption: Index 8 | 9 | index 10 | self 11 | install 12 | use 13 | advanced 14 | plugin 15 | extra 16 | api 17 | 18 | .. toctree:: 19 | :hidden: 20 | 21 | 22 | .. comment out 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | -------------------------------------------------------------------------------- /tests/api/v1/schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, RootModel, Field 2 | 3 | 4 | class PetBase(BaseModel): 5 | name: str 6 | tag: str | None = Field(default=None) 7 | 8 | 9 | class PetCreate(PetBase): 10 | pass 11 | 12 | 13 | class Pet(PetBase): 14 | id: int 15 | 16 | 17 | class Pets(RootModel): 18 | root: list[Pet] = Field(..., description="list of pet") 19 | 20 | 21 | class Error(BaseModel): 22 | code: int 23 | message: str 24 | -------------------------------------------------------------------------------- /tests/fixtures/plugin-base.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | paths: 6 | /pets: 7 | get: 8 | description: '' 9 | operationId: xPets 10 | responses: 11 | '200': 12 | description: pet response 13 | content: 14 | application/json: 15 | schema: 16 | type: array 17 | items: 18 | $ref: '#/components/schemas/Pet' 19 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Reporting issues affecting the confidentiality, integrity or availability. 4 | 5 | ## Supported Versions 6 | 7 | | Version | Supported | 8 | |---------| ------------------ | 9 | | 0.8.x | :white_check_mark: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Depending on your own severity rating of the issue, you can either just report a bug or send me an email to the mail address most frequently used in the commits. 14 | -------------------------------------------------------------------------------- /tests/fixtures/schema-array.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: enum test 5 | 6 | components: 7 | schemas: 8 | Bool: 9 | type: boolean 10 | 11 | Array: 12 | type: array 13 | items: 14 | $ref: "#/components/schemas/Bool" 15 | 16 | ListArray: 17 | type: array 18 | items: 19 | - type: integer 20 | - type: string 21 | 22 | ArbitraryArray: 23 | type: array 24 | items: {} 25 | -------------------------------------------------------------------------------- /tests/fixtures/schema-patternProperties.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: with patternProperties 5 | 6 | components: 7 | schemas: 8 | A: 9 | type: object 10 | additionalProperties: false 11 | patternProperties: 12 | "^S_": 13 | type: "string" 14 | "^I_": 15 | type: "integer" 16 | O: 17 | type: object 18 | additionalProperties: false 19 | patternProperties: 20 | "^O_": {} 21 | -------------------------------------------------------------------------------- /src/aiopenapi3/v30/xml.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | 3 | from .general import ObjectExtended 4 | 5 | 6 | class XML(ObjectExtended): 7 | """ 8 | 9 | .. XML Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xml-object 10 | """ 11 | 12 | name: str = Field(default=None) 13 | namespace: str = Field(default=None) 14 | prefix: str = Field(default=None) 15 | attribute: bool = Field(default=False) 16 | wrapped: bool = Field(default=False) 17 | -------------------------------------------------------------------------------- /tests/fixtures/schema-title-name-collision.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | 3 | info: 4 | title: title collision 5 | version: 2.2.7 6 | 7 | servers: 8 | - url: / 9 | 10 | paths: {} 11 | 12 | 13 | components: 14 | schemas: 15 | B: 16 | type: object 17 | title: B 18 | properties: 19 | a: 20 | title: A 21 | type: string 22 | A: 23 | type: object 24 | properties: 25 | id: 26 | type: integer 27 | 28 | C: 29 | $ref: '#/components/schemas/A' 30 | -------------------------------------------------------------------------------- /tests/fixtures/schema-type-validators.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: OpenAPI schemas with validators 5 | 6 | components: 7 | schemas: 8 | Integer: 9 | type: integer 10 | maximum: 10 11 | minimum: 10 12 | Number: 13 | type: number 14 | maximum: 10 15 | minimum: 10 16 | String: 17 | type: string 18 | maxLength: 5 19 | minLength: 5 20 | Any: 21 | maxLength: 5 22 | minLength: 5 23 | maximum: 10 24 | minimum: 10 25 | -------------------------------------------------------------------------------- /tests/api/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from fastapi import FastAPI 4 | from fastapi_versioning import VersionedFastAPI, version 5 | 6 | from api.v1.main import router as v1 7 | from api.v2.main import router as v2 8 | 9 | app = FastAPI( 10 | version="1.0.0", title="Dorthu's Petstore", servers=[{"url": "/", "description": "Default, relative server"}] 11 | ) 12 | 13 | 14 | app.include_router(v1) 15 | app.include_router(v2) 16 | 17 | app = VersionedFastAPI(app, version_format="{major}", prefix_format="/v{major}") 18 | -------------------------------------------------------------------------------- /tests/fixtures/schema-create-update-read.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: enum test 5 | 6 | components: 7 | schemas: 8 | A: 9 | type: object 10 | additionalProperties: false 11 | required: [a] 12 | properties: 13 | a: 14 | type: string 15 | AB: 16 | type: object 17 | additionalProperties: false 18 | required: [b] 19 | properties: 20 | b: 21 | type: string 22 | allOf: 23 | - $ref: "#/components/schemas/A" 24 | -------------------------------------------------------------------------------- /tests/fixtures/schema-oneOf-mixed.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: {} 12 | 13 | components: 14 | schemas: 15 | object: 16 | type: object 17 | additionalProperties: false 18 | properties: 19 | typed: 20 | oneOf: 21 | - type: string 22 | enum: 23 | - "5" 24 | - type: integer 25 | enum: 26 | - 4 27 | -------------------------------------------------------------------------------- /tests/fixtures/schema-regex-engine.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: | 5 | string pattern test with pattern invalid with pydantic-core/rust regex 6 | https://github.com/pydantic/pydantic-core/issues/1374 7 | 8 | components: 9 | schemas: 10 | Root: 11 | type: string 12 | pattern: ^Passphrase:[ ^[ !#-~]+$ 13 | 14 | Object: 15 | type: object 16 | additionalProperties: false 17 | properties: 18 | v: 19 | type: string 20 | pattern: ^Passphrase:[ ^[ !#-~]+$ 21 | -------------------------------------------------------------------------------- /src/aiopenapi3/v20/xml.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | 3 | from .general import ObjectExtended 4 | 5 | 6 | class XML(ObjectExtended): 7 | """ 8 | A metadata object that allows for more fine-tuned XML model definitions. 9 | 10 | https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#xml-object 11 | """ 12 | 13 | name: str = Field(default=None) 14 | namespace: str = Field(default=None) 15 | prefix: str = Field(default=None) 16 | attribute: bool = Field(default=False) 17 | wrapped: bool = Field(default=False) 18 | -------------------------------------------------------------------------------- /tests/clone_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from aiopenapi3 import OpenAPI 4 | 5 | import pytest 6 | 7 | """ 8 | https://github.com/pydantic/pydantic/issues/6010 9 | """ 10 | # pytest.skip(allow_module_level=True) 11 | 12 | 13 | def test_clone(petstore_expanded): 14 | api = OpenAPI("/", petstore_expanded) 15 | _ = api.clone("/v2") 16 | 17 | 18 | def test_cache(petstore_expanded): 19 | api = OpenAPI("/", petstore_expanded) 20 | 21 | p = Path("tests/data/cache-test.pickle") 22 | api.cache_store(p) 23 | _ = OpenAPI.cache_load(p) 24 | -------------------------------------------------------------------------------- /src/aiopenapi3/v30/tag.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | 3 | from ..base import ObjectExtended 4 | from .general import ExternalDocumentation 5 | 6 | 7 | class Tag(ObjectExtended): 8 | """ 9 | A `Tag Object`_ holds a reusable set of different aspects of the OAS 10 | spec. 11 | 12 | .. _Tag Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tag-object 13 | """ 14 | 15 | name: str = Field(...) 16 | description: str | None = Field(default=None) 17 | externalDocs: ExternalDocumentation | None = Field(default=None) 18 | -------------------------------------------------------------------------------- /tests/fixtures/parsing-paths-parameter-name-with-underscores.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | version: 1.0.0 4 | title: test spec 5 | paths: 6 | /test/{parameter_with_underscores}: 7 | parameters: 8 | - name: parameter_with_underscores 9 | in: path 10 | required: true 11 | schema: 12 | type: string 13 | get: 14 | operationId: test 15 | responses: 16 | '200': 17 | description: test 18 | content: 19 | application/json: 20 | schema: 21 | type: object 22 | -------------------------------------------------------------------------------- /tests/fixtures/schema-enum.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: enum test 5 | 6 | components: 7 | schemas: 8 | Integer: 9 | type: integer 10 | enum: 11 | - 1 12 | - 2 13 | - 3 14 | 15 | String: 16 | type: string 17 | enum: 18 | - "a" 19 | - "b" 20 | 21 | Nullable: 22 | type: [string, "null"] 23 | enum: 24 | - "a" 25 | - 26 | 27 | Mixed: 28 | enum: 29 | - 1 30 | - good 31 | - "yes" 32 | - true 33 | - 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | enable-beta-ecosystems: true 8 | updates: 9 | - package-ecosystem: "uv" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | groups: 14 | all: 15 | patterns: 16 | - "*" 17 | -------------------------------------------------------------------------------- /tests/fixtures/schema-discriminated-union-invalid-array.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: aliased discriminator key 5 | 6 | components: 7 | schemas: 8 | A: 9 | title: A 10 | type: array 11 | additionalProperties: false 12 | items: 13 | type: string 14 | 15 | AB: 16 | title: AB 17 | type: object 18 | additionalProperties: false 19 | oneOf: 20 | - $ref: '#/components/schemas/A' 21 | discriminator: 22 | propertyName: t-d 23 | mapping: 24 | a: '#/components/schemas/A' 25 | -------------------------------------------------------------------------------- /src/aiopenapi3/v20/tag.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | 3 | from ..base import ObjectExtended 4 | from .general import ExternalDocumentation 5 | 6 | 7 | class Tag(ObjectExtended): 8 | """ 9 | Allows adding meta data to a single tag that is used by the Operation Object. It is not mandatory to have a Tag Object per tag used there. 10 | 11 | .. _Tag Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#tag-object 12 | """ 13 | 14 | name: str = Field(...) 15 | description: str | None = Field(default=None) 16 | externalDocs: ExternalDocumentation | None = Field(default=None) 17 | -------------------------------------------------------------------------------- /src/aiopenapi3/v31/example.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from pydantic import Field 4 | 5 | from ..base import ObjectExtended 6 | 7 | 8 | class Example(ObjectExtended): 9 | """ 10 | A `Example Object`_ holds a reusable set of different aspects of the OAS 11 | spec. 12 | 13 | .. _Example Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#exampleObject 14 | """ 15 | 16 | summary: str | None = Field(default=None) 17 | description: str | None = Field(default=None) 18 | value: Any | None = Field(default=None) 19 | externalValue: str | None = Field(default=None) 20 | -------------------------------------------------------------------------------- /tests/fixtures/paths-response-content-empty.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: 'with empty response' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: 12 | /empty: 13 | get: 14 | operationId: empty 15 | responses: 16 | "200": 17 | description: "ok" 18 | /headers: 19 | get: 20 | operationId: headers 21 | responses: 22 | "200": 23 | description: "ok" 24 | headers: 25 | X-required: 26 | schema: 27 | type: string 28 | required: true 29 | -------------------------------------------------------------------------------- /tests/fixtures/schema-oneOf-properties.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Any OpenAPI validator that properly validates discriminator against data? 5 | 6 | components: 7 | schemas: 8 | AB: 9 | type: object 10 | oneOf: 11 | - $ref: "#/components/schemas/A" 12 | - $ref: "#/components/schemas/B" 13 | properties: 14 | id: 15 | type: integer 16 | 17 | B: 18 | type: object 19 | properties: 20 | ofB: 21 | type: string 22 | 23 | A: 24 | type: object 25 | properties: 26 | ofA: 27 | type: integer 28 | -------------------------------------------------------------------------------- /tests/fixtures/schema-discriminated-union-discriminator-name.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: aliased discriminator key 5 | 6 | components: 7 | schemas: 8 | A: 9 | title: A 10 | type: object 11 | additionalProperties: false 12 | properties: 13 | t-d: 14 | type: string 15 | enum: ["a"] 16 | 17 | AB: 18 | title: AB 19 | type: object 20 | additionalProperties: false 21 | oneOf: 22 | - $ref: '#/components/schemas/A' 23 | discriminator: 24 | propertyName: t-d 25 | mapping: 26 | a: '#/components/schemas/A' 27 | -------------------------------------------------------------------------------- /src/aiopenapi3/v30/example.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from pydantic import Field 4 | 5 | from ..base import ObjectExtended 6 | 7 | from .general import Reference 8 | 9 | 10 | class Example(ObjectExtended): 11 | """ 12 | A `Example Object`_ holds a reusable set of different aspects of the OAS 13 | spec. 14 | 15 | .. _Example Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#example-object 16 | """ 17 | 18 | summary: str | None = Field(default=None) 19 | description: str | None = Field(default=None) 20 | value: Any | None = Field(default=None) 21 | externalValue: str | None = Field(default=None) 22 | -------------------------------------------------------------------------------- /tests/fixtures/parsing-paths-operationid-duplicate.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: This has a duplicate operationId 5 | paths: 6 | /example: 7 | get: 8 | operationId: dupe 9 | responses: 10 | '200': 11 | description: The response 12 | content: 13 | application/json: 14 | schema: 15 | type: object 16 | /example2: 17 | get: 18 | operationId: dupe 19 | responses: 20 | '200': 21 | description: The response 22 | content: 23 | application/json: 24 | schema: 25 | type: object 26 | -------------------------------------------------------------------------------- /tests/fixtures/schema-additionalProperties-and-named-properties.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: with additionalProperties and named properties 5 | 6 | components: 7 | schemas: 8 | A: 9 | title: A 10 | type: object 11 | additionalProperties: 12 | type: integer 13 | format: int32 14 | properties: 15 | B: 16 | type: string 17 | 18 | B: 19 | type: object 20 | additionalProperties: true 21 | properties: 22 | data: 23 | type: object 24 | properties: 25 | b0: 26 | type: string 27 | b1: 28 | type: integer 29 | -------------------------------------------------------------------------------- /tests/fixtures/schema-yaml12-tags.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: "yes" 4 | version: "1.0" 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: {} 12 | 13 | components: 14 | schemas: 15 | LongviewClient: 16 | type: object 17 | description: > 18 | A LongviewClient is a single monitor set up to track statistics about 19 | one of your servers. 20 | properties: 21 | updated: 22 | type: string 23 | format: date-time 24 | description: > 25 | When this Longview Client was last updated. 26 | example: 2018-01-01T00:01:01 27 | readOnly: true 28 | -------------------------------------------------------------------------------- /tests/fixtures/schema-oneOf-nullable-v31.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: {} 12 | 13 | components: 14 | schemas: 15 | object: 16 | type: object 17 | additionalProperties: false 18 | properties: 19 | typed: 20 | oneOf: 21 | - type: string 22 | enum: 23 | - "5" 24 | - type: string 25 | enum: 26 | - "4" 27 | - type: "null" 28 | 29 | enumed: 30 | oneOf: 31 | - type: string 32 | enum: ["5"] 33 | - enum: [null] 34 | -------------------------------------------------------------------------------- /tests/fixtures/paths-content-schema-property-without-properties.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Schema with no properties 5 | servers: 6 | - url: http://localhost 7 | paths: 8 | /no-props: 9 | get: 10 | operationId: noProps 11 | responses: 12 | '200': 13 | description: Response object with no properties 14 | content: 15 | 'application/json': 16 | schema: 17 | type: object 18 | properties: 19 | example: 20 | type: string 21 | no_properties: 22 | title: has_no_properties 23 | type: object 24 | -------------------------------------------------------------------------------- /tests/fixtures/paths-response-content-empty-v20.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: with empty response 4 | description: with empty response 5 | version: 1.0.0 6 | host: api.example.com 7 | basePath: /v1 8 | schemes: 9 | - https 10 | 11 | consumes: 12 | - application/json 13 | produces: 14 | - application/json 15 | 16 | definitions: {} 17 | 18 | paths: 19 | /empty: 20 | get: 21 | operationId: empty 22 | responses: 23 | "200": 24 | description: OK 25 | /headers: 26 | get: 27 | operationId: headers 28 | responses: 29 | "200": 30 | description: OK 31 | headers: 32 | X-required: 33 | type: string 34 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /tests/fixtures/parsing-paths-parameter-name-mismatch.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: This has a mismatched parameter name. 5 | paths: 6 | /example/{name}: 7 | parameters: 8 | - in: path 9 | name: name 10 | required: true 11 | schema: 12 | type: string 13 | get: 14 | parameters: 15 | - in: path 16 | name: different 17 | required: true 18 | schema: 19 | type: string 20 | operationId: example 21 | responses: 22 | '200': 23 | description: Success! 24 | content: 25 | application/json: 26 | schema: 27 | type: object 28 | -------------------------------------------------------------------------------- /src/aiopenapi3/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ 2 | from .openapi import OpenAPI 3 | from .loader import FileSystemLoader 4 | from .errors import ( 5 | SpecError, 6 | ReferenceResolutionError, 7 | HTTPError, 8 | ResponseError, 9 | HTTPStatusError, 10 | ContentTypeError, 11 | ResponseDecodingError, 12 | ResponseSchemaError, 13 | RequestError, 14 | ) 15 | 16 | 17 | __all__ = [ 18 | "__version__", 19 | "OpenAPI", 20 | "FileSystemLoader", 21 | "SpecError", 22 | "ReferenceResolutionError", 23 | "HTTPError", 24 | "ResponseError", 25 | "HTTPStatusError", 26 | "ContentTypeError", 27 | "ResponseDecodingError", 28 | "ResponseSchemaError", 29 | "RequestError", 30 | ] 31 | -------------------------------------------------------------------------------- /tests/fixtures/parsing-paths-content-schema-float-validation.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Testing float validations 5 | paths: 6 | /foo: 7 | get: 8 | responses: 9 | '200': 10 | description: foo 11 | content: 12 | 'application/json': 13 | schema: 14 | type: object 15 | properties: 16 | integer: 17 | type: integer 18 | format: int32 19 | minimum: 0 20 | maximum: 1 21 | real: 22 | type: number 23 | format: double 24 | minimum: 0.0 25 | maximum: 1.0 26 | -------------------------------------------------------------------------------- /tests/fixtures/schema-oneOf-nullable-v30.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: {} 12 | 13 | components: 14 | schemas: 15 | object: 16 | type: object 17 | additionalProperties: false 18 | properties: 19 | typed: 20 | oneOf: 21 | - type: string 22 | enum: 23 | - "5" 24 | - type: string 25 | enum: 26 | - "4" 27 | - type: object 28 | additionalProperties: false 29 | nullable: true 30 | 31 | enumed: 32 | oneOf: 33 | - type: string 34 | enum: ["5"] 35 | - enum: [null] 36 | -------------------------------------------------------------------------------- /tests/fixtures/paths-parameter-name-invalid.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: 12 | /test/{Path:}/{}: 13 | parameters: 14 | - name: "Path:" 15 | in: path 16 | description: "" 17 | required: true 18 | schema: 19 | type: string 20 | - name: "" 21 | in: path 22 | description: "" 23 | required: true 24 | schema: 25 | type: string 26 | get: 27 | operationId: getTest 28 | responses: 29 | '200': 30 | description: "" 31 | content: 32 | application/json: 33 | schema: 34 | type: string 35 | -------------------------------------------------------------------------------- /tests/fixtures/README.md: -------------------------------------------------------------------------------- 1 | # Test Specifications 2 | 3 | These OpenAPI specs are included for test purposes only. 4 | 5 | ## Examples from OpenAPI repository 6 | 7 | The following specifications in this directory are taken from OpenAPI example specifications 8 | hosted here: https://github.com/OAI/OpenAPI-Specification/tree/master/examples 9 | 10 | - petstore-expanded.yaml 11 | 12 | These specifications are included under OpenAPI's Apache License 2.0, which can 13 | be found here: https://github.com/OAI/OpenAPI-Specification/blob/master/LICENSE 14 | 15 | These specifications are reproduced here to aid testing, but are not authoritative 16 | copies - those in the repo linked above should be considered authoritative, and 17 | these should be able to be updated seamlessly should they change. 18 | -------------------------------------------------------------------------------- /tests/fixtures/paths-response-header-v20.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: with response headers 4 | description: with response headers 5 | version: 1.0.0 6 | host: api.example.com 7 | basePath: /v1 8 | schemes: 9 | - https 10 | 11 | consumes: 12 | - application/json 13 | produces: 14 | - application/json 15 | 16 | definitions: {} 17 | 18 | paths: 19 | /: 20 | get: 21 | operationId: get 22 | responses: 23 | "200": 24 | description: OK 25 | schema: 26 | type: string 27 | enum: 28 | - get 29 | headers: 30 | X-required: 31 | type: string 32 | X-optional: 33 | type: array 34 | items: 35 | type: string 36 | -------------------------------------------------------------------------------- /tests/fixtures/paths-tags.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: 12 | /user/: 13 | get: 14 | operationId: list 15 | tags: ["users"] 16 | responses: 17 | "200": 18 | description: "ok" 19 | content: 20 | application/json: 21 | schema: 22 | type: string 23 | enum: 24 | - list 25 | /item/: 26 | get: 27 | operationId: list 28 | tags: ["items", "objects"] 29 | responses: 30 | "200": 31 | description: "ok" 32 | content: 33 | application/json: 34 | schema: 35 | type: string 36 | -------------------------------------------------------------------------------- /src/aiopenapi3/json.py: -------------------------------------------------------------------------------- 1 | import urllib.parse 2 | 3 | from yarl import URL 4 | 5 | 6 | class JSONPointer: 7 | """ 8 | JavaScript Object Notation (JSON) Pointer 9 | 10 | https://datatracker.ietf.org/doc/html/rfc6901 11 | """ 12 | 13 | @staticmethod 14 | def decode(part: str) -> str: 15 | """ 16 | 17 | https://swagger.io/docs/specification/using-ref/ 18 | :param part: 19 | """ 20 | part = urllib.parse.unquote(part) 21 | part = part.replace("~1", "/") 22 | return part.replace("~0", "~") 23 | 24 | 25 | class JSONReference: 26 | @staticmethod 27 | def split(url: str | URL) -> tuple[str, str]: 28 | """ 29 | split the url into path and fragment 30 | """ 31 | u = URL(url) 32 | return str(u.with_fragment("")), u.raw_fragment 33 | -------------------------------------------------------------------------------- /tests/fixtures/parsing-paths-content-schema-object.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Object example validations 5 | paths: 6 | /check-dict: 7 | get: 8 | responses: 9 | '200': 10 | description: Checking example dict 11 | content: 12 | 'application/json': 13 | schema: 14 | type: object 15 | properties: 16 | integer: 17 | type: integer 18 | real: 19 | type: number 20 | example: 21 | integer: 42 22 | real: 0.5 23 | /check-str: 24 | get: 25 | responses: 26 | '200': 27 | description: Checking example string 28 | content: 29 | 'text/plain': 30 | example: 'Hello' 31 | -------------------------------------------------------------------------------- /tests/fixtures/parsing-schema-names.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: API 4 | version: 1.0.0 5 | 6 | paths: {} 7 | 8 | components: 9 | schemas: 10 | ? "Rechnungsdruck.WebApp.Controllers.Api.ApiPagedResult.PagingInformation[System.Collections.Generic.List[Billbee.Interfaces.BillbeeAPI.Model.CustomerAddressApiModel]]" 11 | : type: object 12 | properties: 13 | Page: 14 | format: int32 15 | type: integer 16 | 17 | ? "Rechnungsdruck.WebApp.Controllers.Api.ApiPagedResult[System.Collections.Generic.List[Billbee.Interfaces.BillbeeAPI.Model.CustomerAddressApiModel]]" 18 | : type: object 19 | properties: 20 | Paging: 21 | $ref: "#/components/schemas/Rechnungsdruck.WebApp.Controllers.Api.ApiPagedResult.PagingInformation[System.Collections.Generic.List[Billbee.Interfaces.BillbeeAPI.Model.CustomerAddressApiModel]]" 22 | -------------------------------------------------------------------------------- /tests/fixtures/paths-parameter-missing.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | components: {} 12 | 13 | paths: 14 | /{path}/{op}/{missing}: 15 | parameters: 16 | - in: path 17 | name: path 18 | style: simple 19 | explode: false 20 | required: true 21 | schema: 22 | type: string 23 | 24 | get: 25 | parameters: 26 | - in: path 27 | name: op 28 | style: label 29 | explode: false 30 | required: true 31 | schema: 32 | type: string 33 | operationId: missing 34 | responses: 35 | '200': 36 | content: 37 | application/json: 38 | schema: 39 | type: string 40 | description: '' 41 | -------------------------------------------------------------------------------- /tests/fixtures/extra-cookie.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - cookieAuth: [] 10 | 11 | paths: 12 | /set-cookie: 13 | get: 14 | operationId: set_cookie 15 | responses: 16 | '200': 17 | content: 18 | application/json: 19 | schema: 20 | type: string 21 | description: '' 22 | security: [] 23 | 24 | /require-cookie: 25 | get: 26 | operationId: require_cookie 27 | description: '' 28 | responses: 29 | '200': 30 | content: 31 | application/json: 32 | schema: 33 | type: string 34 | description: '' 35 | 36 | components: 37 | securitySchemes: 38 | cookieAuth: 39 | type: apiKey 40 | in: cookie 41 | name: Session 42 | -------------------------------------------------------------------------------- /tests/fixtures/parsing-paths-links-invalid.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: Example spec with valid links 5 | paths: 6 | /with-links: 7 | get: 8 | operationId: withLinks 9 | responses: 10 | '200': 11 | description: This has links 12 | content: 13 | applicaton/json: 14 | schema: 15 | type: object 16 | properties: 17 | test: 18 | type: string 19 | description: A test response fields 20 | example: foobar 21 | links: 22 | exampleWithBoth: 23 | operationId: withLinksTwo 24 | operationRef: "/with-links" 25 | parameters: 26 | param: baz 27 | exampleWithNeither: 28 | parameters: 29 | param: baz 30 | -------------------------------------------------------------------------------- /tests/fixtures/paths-servers.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: operation url 4 | version: 1.0.0 5 | servers: 6 | - url: "https://servers/" 7 | 8 | paths: 9 | /servers: 10 | get: 11 | operationId: servers 12 | responses: 13 | '200': 14 | description: . 15 | content: 16 | application/json: 17 | schema: 18 | type: string 19 | 20 | /defined: 21 | servers: 22 | - url: https://path/ 23 | head: 24 | operationId: path 25 | responses: 26 | '204': 27 | description: . 28 | content: {} 29 | get: 30 | servers: 31 | - url: https://operation/ 32 | operationId: operation 33 | responses: 34 | '200': 35 | description: . 36 | content: 37 | application/json: 38 | schema: 39 | type: string 40 | -------------------------------------------------------------------------------- /tests/fixtures/schema-empty.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.2" 2 | info: 3 | version: 1.0.0 4 | title: with emptieness 5 | 6 | components: 7 | schemas: 8 | empty-schema: {} 9 | 10 | paths: 11 | /empty-request: 12 | post: 13 | operationId: emptyRequest 14 | requestBody: 15 | content: 16 | "*/*": 17 | examples: 18 | OpenAPI Example: 19 | value: 20 | a: b 21 | description: The content of the artifact being created. This is often, but not always, JSON data 22 | responses: {} 23 | 24 | /empty-response: 25 | get: 26 | operationId: emptyResponse 27 | responses: 28 | "200": 29 | content: 30 | application/json: 31 | examples: 32 | OpenAPI Example: 33 | value: 34 | a: b 35 | description: On a successful response 36 | -------------------------------------------------------------------------------- /tests/fixtures/paths-response-content-type-octet.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: 12 | /octet/headers: 13 | get: 14 | operationId: header 15 | responses: 16 | "200": 17 | description: "ok" 18 | content: 19 | application/octet-stream: 20 | schema: 21 | type: string 22 | format: byte 23 | headers: 24 | X-required: 25 | required: true 26 | schema: 27 | type: string 28 | /octet: 29 | get: 30 | operationId: octet 31 | responses: 32 | "200": 33 | description: "ok" 34 | content: 35 | application/octet-stream: 36 | schema: 37 | type: string 38 | format: byte 39 | -------------------------------------------------------------------------------- /tests/fixtures/schema-oneOf.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | version: 1.0.0 4 | title: oneOf with possibilities 5 | 6 | components: 7 | schemas: 8 | A: 9 | title: A 10 | type: object 11 | additionalProperties: false 12 | properties: 13 | type: 14 | const: "a" 15 | value: 16 | type: integer 17 | 18 | B: 19 | title: B 20 | type: string 21 | 22 | AL: 23 | type: array 24 | items: 25 | $ref: "#/components/schemas/A" 26 | 27 | AB: 28 | title: AB 29 | oneOf: 30 | - type: object 31 | additionalProperties: false 32 | oneOf: 33 | - $ref: '#/components/schemas/A' 34 | discriminator: 35 | propertyName: type 36 | mapping: 37 | a: '#/components/schemas/A' 38 | - $ref: "#/components/schemas/AL" 39 | - $ref: "#/components/schemas/B" 40 | -------------------------------------------------------------------------------- /tests/fixtures/schema-discriminated-union-extends.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | 3 | info: 4 | title: title collision 5 | version: 2.2.7 6 | 7 | servers: 8 | - url: / 9 | 10 | paths: {} 11 | 12 | 13 | components: 14 | schemas: 15 | A: 16 | type: object 17 | additionalProperties: false 18 | properties: 19 | a: 20 | type: integer 21 | B: 22 | type: object 23 | additionalProperties: false 24 | properties: 25 | b: 26 | type: integer 27 | 28 | AB: 29 | type: object 30 | additionalProperties: false 31 | properties: 32 | type: 33 | type: string 34 | const: "" 35 | oneOf: 36 | - $ref: "#/components/schemas/A" 37 | - $ref: "#/components/schemas/B" 38 | discriminator: 39 | propertyName: type 40 | mapping: 41 | A: "#/components/schemas/A" 42 | B: "#/components/schemas/B" 43 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /tests/fixtures/paths-parameter-format-complex.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: 12 | /test: 13 | get: 14 | operationId: get 15 | parameters: 16 | - $ref: "#/components/parameters/ContentDefinedParameter" 17 | 18 | responses: 19 | '200': 20 | content: 21 | application/json: 22 | schema: 23 | $ref: '#/components/schemas/Test' 24 | description: '' 25 | 26 | components: 27 | schemas: 28 | Test: 29 | type: string 30 | enum: 31 | - test 32 | 33 | parameters: 34 | ContentDefinedParameter: 35 | in: query 36 | name: value 37 | required: true 38 | content: 39 | "application/json": 40 | schema: 41 | type: integer 42 | default: 5 43 | example: 5 44 | -------------------------------------------------------------------------------- /tests/fixtures/schema-allof-string.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: from authentik 5 | 6 | components: 7 | schemas: 8 | Enum: 9 | enum: 10 | - valid 11 | type: string 12 | minLength: 13 | type: string 14 | minLength: 5 15 | maxLength: 16 | type: string 17 | maxLength: 5 18 | mixLength: 19 | type: string 20 | maxLength: 5 21 | minLength: 5 22 | allOfEnum: 23 | allOf: 24 | - $ref: '#/components/schemas/Enum' 25 | description: single allOf 26 | allOfmixLength: 27 | allOf: 28 | - $ref: '#/components/schemas/mixLength' 29 | description: single allOf 30 | allOfCombined: 31 | type: string 32 | maxLength: 5 33 | allOf: 34 | - $ref: '#/components/schemas/minLength' 35 | allOfMinMaxLength: 36 | allOf: 37 | - $ref: '#/components/schemas/minLength' 38 | - $ref: '#/components/schemas/maxLength' 39 | -------------------------------------------------------------------------------- /tests/fixtures/schema-pathitems.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: "0.1" 4 | title: paths refs 5 | servers: 6 | - url: http://127.0.0.1/ 7 | paths: 8 | /a: 9 | $ref: "#/components/pathItems/A" 10 | /b: 11 | $ref: "#/components/pathItems/B" 12 | components: 13 | schemas: 14 | D: 15 | type: object 16 | properties: 17 | foo: 18 | type: string 19 | pathItems: 20 | B: 21 | get: 22 | operationId: b 23 | description: "" 24 | responses: 25 | "200": 26 | description: "" 27 | content: 28 | 'application/json': 29 | schema: 30 | $ref: '#/components/schemas/D' 31 | A: 32 | get: 33 | description: "" 34 | responses: 35 | "200": 36 | description: "" 37 | content: 38 | 'application/json': 39 | schema: 40 | $ref: '#/components/schemas/D' 41 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.10" 13 | apt_packages: 14 | - graphviz 15 | jobs: 16 | post_create_environment: 17 | - pip install uv 18 | - uv export --no-hashes --all-packages --output-file requirements-tests.txt 19 | 20 | # Build documentation in the docs/ directory with Sphinx 21 | sphinx: 22 | configuration: docs/source/conf.py 23 | 24 | # If using Sphinx, optionally build your docs in additional formats such as PDF 25 | # formats: 26 | # - pdf 27 | 28 | # Optionally declare the Python requirements required to build your docs 29 | python: 30 | install: 31 | - method: pip 32 | path: . 33 | - requirements: docs/requirements.txt 34 | - requirements: requirements-tests.txt 35 | -------------------------------------------------------------------------------- /tests/fixtures/paths-response-error-v20.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: with response headers 4 | description: with response headers 5 | version: 1.0.0 6 | host: api.example.com 7 | basePath: /v1 8 | schemes: 9 | - https 10 | 11 | consumes: 12 | - application/json 13 | produces: 14 | - application/json 15 | 16 | definitions: {} 17 | 18 | paths: 19 | /test: 20 | get: 21 | operationId: test 22 | responses: 23 | "200": 24 | description: "ok" 25 | schema: 26 | type: string 27 | enum: ["ok"] 28 | 29 | "437": 30 | description: "client error" 31 | schema: 32 | type: string 33 | enum: ["ok"] 34 | 35 | headers: 36 | X-required: 37 | type: string 38 | 39 | "537": 40 | description: "server error" 41 | schema: 42 | type: string 43 | enum: ["ok"] 44 | headers: 45 | X-required: 46 | type: string 47 | -------------------------------------------------------------------------------- /tests/fixtures/schema-nullable-v31.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: {} 12 | 13 | components: 14 | schemas: 15 | object: 16 | type: [object, "null"] 17 | additionalProperties: false 18 | properties: 19 | attr: 20 | $ref: '#/components/schemas/nullable' 21 | required: 22 | - attr 23 | 24 | array: 25 | type: [array, "null"] 26 | items: 27 | $ref: '#/components/schemas/nullable' 28 | 29 | union: 30 | oneOf: 31 | - $ref: '#/components/schemas/string' 32 | - $ref: '#/components/schemas/integer' 33 | 34 | string: 35 | type: [string, "null"] 36 | 37 | integer: 38 | type: [integer, "null"] 39 | 40 | boolean: 41 | type: [boolean, "null"] 42 | 43 | nullable: 44 | type: [string, "null"] 45 | 46 | multi: 47 | type: [integer, string, "null"] 48 | 49 | "null": 50 | type: "null" 51 | -------------------------------------------------------------------------------- /tests/fixtures/schema-nullable-v30.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: {} 12 | 13 | components: 14 | schemas: 15 | object: 16 | type: object 17 | additionalProperties: false 18 | properties: 19 | attr: 20 | $ref: '#/components/schemas/nullable' 21 | nullable: true 22 | required: 23 | - attr 24 | 25 | array: 26 | type: array 27 | items: 28 | $ref: '#/components/schemas/nullable' 29 | nullable: true 30 | 31 | union: 32 | oneOf: 33 | - $ref: '#/components/schemas/string' 34 | - $ref: '#/components/schemas/integer' 35 | nullable: true 36 | 37 | string: 38 | type: string 39 | nullable: true 40 | 41 | integer: 42 | type: integer 43 | nullable: true 44 | 45 | boolean: 46 | type: boolean 47 | nullable: true 48 | 49 | nullable: 50 | nullable: true 51 | type: string 52 | -------------------------------------------------------------------------------- /src/aiopenapi3/v20/__init__.py: -------------------------------------------------------------------------------- 1 | import pydantic_core 2 | 3 | from .glue import Request, AsyncRequest 4 | 5 | from .general import ExternalDocumentation, Reference 6 | from .info import Contact, License, Info 7 | from .parameter import Parameter, Header 8 | from .paths import Response, Operation, PathItem, Paths 9 | from .root import Root 10 | from .schemas import Schema 11 | from .security import SecurityScheme, SecurityRequirement 12 | from .tag import Tag 13 | from .xml import XML 14 | 15 | 16 | def __init(): 17 | r = dict() 18 | CLASSES = [ 19 | ExternalDocumentation, 20 | Reference, 21 | Contact, 22 | License, 23 | Info, 24 | Parameter, 25 | Header, 26 | Response, 27 | Operation, 28 | PathItem, 29 | Paths, 30 | Schema, 31 | SecurityScheme, 32 | SecurityRequirement, 33 | Tag, 34 | XML, 35 | Root, 36 | ] 37 | for i in CLASSES: 38 | r[i.__name__] = i 39 | for i in CLASSES: 40 | i.model_rebuild(_types_namespace=r) 41 | 42 | 43 | __init() 44 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # https://pdm.fming.dev/latest/usage/publish/#publish-with-trusted-publishers 2 | 3 | name: Publish 4 | 5 | on: 6 | workflow_dispatch: {} 7 | 8 | jobs: 9 | pypi-publish-test: 10 | name: upload release to test.PyPI 11 | runs-on: ubuntu-latest 12 | permissions: 13 | # IMPORTANT: this permission is mandatory for trusted publishing 14 | id-token: write 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: astral-sh/setup-uv@v5 18 | - name: Build package 19 | run: uv build 20 | - name: Publish package distributions to test.PyPI 21 | run: uv publish --index testpypi 22 | 23 | pypi-publish: 24 | name: upload release to PyPI 25 | runs-on: ubuntu-latest 26 | permissions: 27 | # IMPORTANT: this permission is mandatory for trusted publishing 28 | id-token: write 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: astral-sh/setup-uv@v5 32 | - name: Build package 33 | run: uv build 34 | - name: Publish package distributions to PyPI 35 | run: uv publish 36 | -------------------------------------------------------------------------------- /tests/fixtures/schema-discriminated-union.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: enum test 5 | 6 | components: 7 | schemas: 8 | A: 9 | title: A 10 | type: object 11 | additionalProperties: false 12 | properties: 13 | object_type: 14 | type: string 15 | enum: ["a"] 16 | a: 17 | type: string 18 | 19 | B: 20 | title: B 21 | type: object 22 | additionalProperties: false 23 | properties: 24 | object_type: 25 | type: string 26 | enum: ["b"] 27 | b: 28 | type: string 29 | 30 | 31 | 32 | AB: 33 | title: AB 34 | type: object 35 | additionalProperties: false 36 | oneOf: 37 | - $ref: '#/components/schemas/A' 38 | - $ref: '#/components/schemas/B' 39 | discriminator: 40 | propertyName: object_type 41 | mapping: 42 | a: '#/components/schemas/A' 43 | b: '#/components/schemas/B' 44 | 45 | L: 46 | type: array 47 | items: 48 | $ref: '#/components/schemas/AB' 49 | -------------------------------------------------------------------------------- /tests/fixtures/paths-response-status-pattern-default.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: response status tests 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: 12 | /test: 13 | get: 14 | operationId: test 15 | responses: 16 | default: 17 | description: unknown 18 | content: 19 | application/json: 20 | schema: 21 | type: string 22 | const: unknown 23 | 24 | "2XX": 25 | description: good 26 | content: 27 | application/json: 28 | schema: 29 | type: string 30 | const: good 31 | 32 | "5XX": 33 | description: bad 34 | content: 35 | application/json: 36 | schema: 37 | type: string 38 | const: bad 39 | 40 | 41 | "201": 42 | description: "created" 43 | content: 44 | application/json: 45 | schema: 46 | type: string 47 | const: created 48 | -------------------------------------------------------------------------------- /tests/fixtures/schema-discriminated-union-merge.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: enum test 5 | 6 | components: 7 | schemas: 8 | CustomContextVariable: 9 | additionalProperties: false 10 | discriminator: 11 | mapping: 12 | user: '#/components/schemas/UserContextVariable' 13 | propertyName: type 14 | oneOf: 15 | - $ref: '#/components/schemas/UserContextVariable' 16 | properties: 17 | type: 18 | description: Type of custom context variable. 19 | type: string 20 | required: 21 | - type 22 | type: object 23 | UserContextVariable: 24 | description: A [user](https://developer.atlassian.com/cloud/jira/platform/jira-expressions-type-reference#user) specified as an Atlassian account ID. 25 | properties: 26 | accountId: 27 | description: The account ID of the user. 28 | type: string 29 | type: 30 | description: Type of custom context variable. 31 | type: string 32 | required: 33 | - accountId 34 | - type 35 | type: object 36 | -------------------------------------------------------------------------------- /tests/fixtures/paths-parameter-default.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | components: {} 12 | 13 | paths: 14 | /{path}/{op}: 15 | parameters: 16 | - in: cookie 17 | name: debug 18 | schema: 19 | type: string 20 | enum: ["0", "1"] 21 | default: "0" 22 | - in: path 23 | name: path 24 | style: simple 25 | explode: false 26 | required: true 27 | schema: 28 | type: string 29 | default: "op" 30 | 31 | get: 32 | parameters: 33 | - in: path 34 | name: op 35 | style: simple 36 | explode: false 37 | required: true 38 | schema: 39 | type: string 40 | default: "path" 41 | operationId: default 42 | responses: 43 | '200': 44 | content: 45 | application/json: 46 | schema: 47 | type: string 48 | enum: 49 | - default 50 | description: '' 51 | -------------------------------------------------------------------------------- /tests/fixtures/paths-response-error.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: response error 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: 12 | /test: 13 | get: 14 | operationId: test 15 | responses: 16 | "200": 17 | description: "ok" 18 | content: 19 | application/json: 20 | schema: 21 | type: string 22 | const: ok 23 | "437": 24 | description: "client error" 25 | content: 26 | application/json: 27 | schema: 28 | type: string 29 | const: ok 30 | headers: 31 | X-required: 32 | schema: 33 | type: string 34 | required: true 35 | "537": 36 | description: "server error" 37 | content: 38 | application/json: 39 | schema: 40 | type: string 41 | const: ok 42 | headers: 43 | X-required: 44 | schema: 45 | type: string 46 | required: true 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest a new feature for aiopenapi3 3 | labels: [feature] 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thank you for proposing an improvement ✊ 9 | 10 | - type: checkboxes 11 | id: searched 12 | attributes: 13 | label: Initial Checks 14 | description: | 15 | It does not exist or is related to OpenAPI or other libraries can do it already 16 | options: 17 | - label: I have searched Google & GitHub for similar requests and couldn't find anything 18 | required: true 19 | - label: I have read [the docs](https://aiopenapi3.readthedocs.org) and still think this feature is missing 20 | required: true 21 | 22 | - type: textarea 23 | id: description 24 | attributes: 25 | label: Description 26 | description: | 27 | Please give as much detail as possible about the feature you would like to suggest. 🙏 28 | 29 | You might like to add: 30 | * A demo of how code might look when using the feature 31 | * Your use case(s) for the feature 32 | validations: 33 | required: true 34 | -------------------------------------------------------------------------------- /docs/source/extra.rst: -------------------------------------------------------------------------------- 1 | .. include:: links.rst 2 | 3 | ***** 4 | Extra 5 | ***** 6 | 7 | 8 | Large Description Documents 9 | =========================== 10 | 11 | To assist working with large description documents it is possible to limit the models build to the minimum required. 12 | This "minimum required" by the requirements of the Operations. 13 | 14 | Currently there are two Plugins to assist such reduction - :class:`aiopenapi3.extra.Reduce` and :class:`aiopenapi3.extra.Cull`. 15 | Cull is faster but limited to single file description documents, Reduce assists in debugging and can do multi file description documents. 16 | 17 | Large description documents which are autogenerated by converting other service description document formats -such as odata- 18 | may benefit from additional changes to the description document to eliminate conversion artifacts depending on the converter used. 19 | 20 | As an example for additional steps based on the `Microsoft Graph API `_ refer to :aioai3:ref:`tests.extra_test.MSGraph`. 21 | 22 | Cookies 23 | ======= 24 | To assist in dealing with cookie requirements of some APIs, :class:`aiopenapi3.extra.Cookies` can be used. 25 | -------------------------------------------------------------------------------- /tests/fixtures/schema-anyOf.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: 1.0.0 4 | title: oneOf with possibilities 5 | 6 | components: 7 | schemas: 8 | A: 9 | title: A 10 | type: object 11 | additionalProperties: false 12 | properties: 13 | type: 14 | enum: ["a"] 15 | value: 16 | type: integer 17 | exclusiveMaximum: true 18 | maximum: 5 19 | 20 | B: 21 | title: B 22 | type: string 23 | enum: ["b"] 24 | 25 | L: 26 | type: array 27 | items: 28 | $ref: "#/components/schemas/A" 29 | 30 | OA: 31 | title: OA 32 | anyOf: 33 | - $ref: "#/components/schemas/A" 34 | - type: object 35 | additionalProperties: false 36 | nullable: true 37 | 38 | 39 | OB: 40 | title: OB 41 | anyOf: 42 | - $ref: "#/components/schemas/B" 43 | - type: object 44 | additionalProperties: false 45 | nullable: true 46 | 47 | OL: 48 | title: OL 49 | anyOf: 50 | - $ref: "#/components/schemas/L" 51 | - type: object 52 | additionalProperties: false 53 | nullable: true 54 | -------------------------------------------------------------------------------- /tests/fixtures/paths-parameters-oneOf.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: 12 | /test/: 13 | parameters: 14 | - name: nullable_array 15 | in: query 16 | description: "" 17 | required: false 18 | schema: 19 | oneOf: 20 | - type: array 21 | items: 22 | type: string 23 | - enum: [null] 24 | - name: nullable_object 25 | in: query 26 | description: "" 27 | required: false 28 | schema: 29 | oneOf: 30 | - type: object 31 | additionalProperties: false 32 | properties: 33 | a: 34 | type: string 35 | - enum: [null] 36 | 37 | 38 | get: 39 | operationId: getTest 40 | responses: 41 | '200': 42 | content: 43 | application/json: 44 | schema: 45 | $ref: '#/components/schemas/Test' 46 | description: '' 47 | 48 | 49 | 50 | components: 51 | schemas: 52 | Test: 53 | type: string 54 | enum: 55 | - test 56 | -------------------------------------------------------------------------------- /src/aiopenapi3/v30/root.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | from pydantic import Field, validator 5 | 6 | 7 | from ..base import ObjectExtended, RootBase 8 | 9 | from .components import Components 10 | from .general import Reference 11 | from .info import Info 12 | from .paths import PathItem, Paths 13 | from .security import SecurityRequirement 14 | from .servers import Server 15 | from .tag import Tag 16 | 17 | 18 | class Root(ObjectExtended, RootBase): 19 | """ 20 | This class represents the root of the OpenAPI schema document, as defined 21 | in `the spec`_ 22 | 23 | .. _the spec: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#openapi-object 24 | """ 25 | 26 | openapi: str = Field(...) 27 | info: Info = Field(...) 28 | servers: list[Server] = Field(default_factory=list) 29 | paths: Paths = Field(default_factory=dict) 30 | components: Components = Field(default_factory=Components) 31 | security: list[SecurityRequirement] = Field(default_factory=list) 32 | tags: list[Tag] = Field(default_factory=list) 33 | externalDocs: dict[Any, Any] = Field(default_factory=dict) 34 | 35 | def _resolve_references(self, api): 36 | RootBase.resolve(api, self, self, PathItem, Reference) 37 | -------------------------------------------------------------------------------- /tests/fixtures/paths-server-variables.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: operation url 4 | version: 1.0.0 5 | servers: 6 | - url: "https://{host}/" 7 | variables: 8 | host: 9 | enum: 10 | - default 11 | - defined 12 | default: default 13 | 14 | paths: 15 | /servers: 16 | get: 17 | operationId: servers 18 | responses: 19 | '200': 20 | description: . 21 | content: 22 | application/json: 23 | schema: 24 | type: string 25 | 26 | /defined: 27 | servers: 28 | - url: /{version}/ 29 | variables: 30 | version: 31 | enum: 32 | - v1 33 | - v2 34 | default: v1 35 | head: 36 | operationId: path 37 | responses: 38 | '204': 39 | description: . 40 | content: {} 41 | get: 42 | servers: 43 | - url: https://operation/{path} 44 | variables: 45 | path: 46 | default: v3 47 | operationId: operation 48 | responses: 49 | '200': 50 | description: . 51 | content: 52 | application/json: 53 | schema: 54 | type: string 55 | -------------------------------------------------------------------------------- /tests/fixtures/parsing-paths-content-nested-array-ref.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: API 4 | version: 1.0.0 5 | paths: 6 | /pets: 7 | get: 8 | description: "yes" 9 | operationId: findPets 10 | responses: 11 | '200': 12 | description: pet response 13 | content: 14 | application/json: 15 | schema: 16 | type: array 17 | items: 18 | $ref: '#/components/schemas/LKENodePoolRequestBody' 19 | components: 20 | schemas: 21 | LKENodePoolRequestBody: 22 | type: object 23 | description: > 24 | Specifies a collection of Linodes which will be members of a Kubernetes 25 | cluster. 26 | properties: 27 | disks: 28 | type: array 29 | items: 30 | $ref: '#/components/schemas/LKENodePool/properties/disks/items' 31 | LKENodePool: 32 | type: object 33 | properties: 34 | disks: 35 | type: array 36 | items: 37 | type: object 38 | properties: 39 | size: 40 | type: integer 41 | type: 42 | type: string 43 | enum: 44 | - raw 45 | - ext4 46 | -------------------------------------------------------------------------------- /src/aiopenapi3/v20/info.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | 3 | from ..base import ObjectExtended 4 | 5 | 6 | class Contact(ObjectExtended): 7 | """ 8 | Contact object belonging to an Info object, as described `here`_ 9 | 10 | .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#contact-object 11 | """ 12 | 13 | email: str = Field(default=None) 14 | name: str = Field(default=None) 15 | url: str = Field(default=None) 16 | 17 | 18 | class License(ObjectExtended): 19 | """ 20 | License object belonging to an Info object, as described `here`_ 21 | 22 | .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#license-object 23 | """ 24 | 25 | name: str = Field(...) 26 | url: str | None = Field(default=None) 27 | 28 | 29 | class Info(ObjectExtended): 30 | """ 31 | An OpenAPI Info object, as defined in `the spec`_. 32 | 33 | .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#info-object 34 | """ 35 | 36 | title: str = Field(...) 37 | description: str | None = Field(default=None) 38 | termsOfService: str | None = Field(default=None) 39 | license: License | None = Field(default=None) 40 | contact: Contact | None = Field(default=None) 41 | version: str = Field(...) 42 | -------------------------------------------------------------------------------- /src/aiopenapi3/v30/info.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | 3 | from ..base import ObjectExtended 4 | 5 | 6 | class Contact(ObjectExtended): 7 | """ 8 | Contact object belonging to an Info object, as described `here`_ 9 | 10 | .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contact-object 11 | """ 12 | 13 | email: str = Field(default=None) 14 | name: str = Field(default=None) 15 | url: str = Field(default=None) 16 | 17 | 18 | class License(ObjectExtended): 19 | """ 20 | License object belonging to an Info object, as described `here`_ 21 | 22 | .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#license-object 23 | """ 24 | 25 | name: str = Field(...) 26 | url: str | None = Field(default=None) 27 | 28 | 29 | class Info(ObjectExtended): 30 | """ 31 | An OpenAPI Info object, as defined in `the spec`_. 32 | 33 | .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#info-object 34 | """ 35 | 36 | title: str = Field(...) 37 | description: str | None = Field(default=None) 38 | termsOfService: str | None = Field(default=None) 39 | license: License | None = Field(default=None) 40 | contact: Contact | None = Field(default=None) 41 | version: str = Field(...) 42 | -------------------------------------------------------------------------------- /docs/source/_ext/aioai3.py: -------------------------------------------------------------------------------- 1 | from docutils.parsers.rst import directives 2 | from sphinx import addnodes 3 | from sphinx.directives import ObjectDescription 4 | from sphinx.domains import Domain, Index 5 | from sphinx.roles import XRefRole 6 | from sphinx.util.nodes import make_refnode 7 | 8 | from docutils import nodes, utils 9 | 10 | 11 | def resolve_url(env, name): 12 | resolve_target = getattr(env.config, "linkcode_resolve", None) 13 | module, _, name = name.rpartition(".") 14 | uri = resolve_target("py", {"module": module, "fullname": name}) 15 | return uri 16 | 17 | 18 | class aiopenapi3Domain(Domain): 19 | name = "aioai3" 20 | label = "aiopenapi3 code linker" 21 | 22 | roles = { 23 | "ref": XRefRole(), 24 | } 25 | 26 | def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): 27 | # print(f"{env=} {fromdocname=}, {builder=} {typ=} {target=} {node=} {contnode=}") 28 | title = contnode.astext() 29 | full_url = resolve_url(env, target) 30 | pnode = nodes.reference(title, title, internal=False, refuri=full_url) 31 | return pnode 32 | 33 | 34 | def setup(app): 35 | app.add_domain(aiopenapi3Domain) 36 | 37 | return { 38 | "version": "0.1", 39 | "parallel_read_safe": True, 40 | "parallel_write_safe": True, 41 | } 42 | -------------------------------------------------------------------------------- /tests/fixtures/paths-parameters.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: 12 | /test/{Path}: 13 | parameters: 14 | - $ref: "#/components/parameters/Path" 15 | - name: Header 16 | in: header 17 | description: "" 18 | required: true 19 | schema: 20 | type: array 21 | items: 22 | type: integer 23 | get: 24 | operationId: getTest 25 | parameters: 26 | - $ref: "#/components/parameters/Cookie" 27 | - name: Query 28 | in: query 29 | description: "" 30 | required: true 31 | schema: 32 | type: string 33 | responses: 34 | '200': 35 | content: 36 | application/json: 37 | schema: 38 | $ref: '#/components/schemas/Test' 39 | description: '' 40 | 41 | 42 | 43 | components: 44 | schemas: 45 | Test: 46 | type: string 47 | enum: 48 | - test 49 | parameters: 50 | Path: 51 | name: Path 52 | in: path 53 | required: true 54 | schema: 55 | type: string 56 | Cookie: 57 | name: Cookie 58 | in: cookie 59 | required: true 60 | schema: 61 | type: string 62 | -------------------------------------------------------------------------------- /src/aiopenapi3/v30/components.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | 3 | from ..base import ObjectExtended 4 | 5 | from .example import Example 6 | from .paths import RequestBody, Link, Response, Callback 7 | from .general import Reference 8 | from .parameter import Header, Parameter 9 | from .schemas import Schema 10 | from .security import SecurityScheme 11 | 12 | 13 | class Components(ObjectExtended): 14 | """ 15 | A `Components Object`_ holds a reusable set of different aspects of the OAS 16 | spec. 17 | 18 | .. _Components Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#components-object 19 | """ 20 | 21 | schemas: dict[str, Schema | Reference] = Field(default_factory=dict) 22 | responses: dict[str, Response | Reference] = Field(default_factory=dict) 23 | parameters: dict[str, Parameter | Reference] = Field(default_factory=dict) 24 | examples: dict[str, Example | Reference] = Field(default_factory=dict) 25 | requestBodies: dict[str, RequestBody | Reference] = Field(default_factory=dict) 26 | headers: dict[str, Header | Reference] = Field(default_factory=dict) 27 | securitySchemes: dict[str, SecurityScheme | Reference] = Field(default_factory=dict) 28 | links: dict[str, Link | Reference] = Field(default_factory=dict) 29 | callbacks: dict[str, Callback | Reference] = Field(default_factory=dict) 30 | -------------------------------------------------------------------------------- /tests/pickle_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests parsing specs 3 | """ 4 | 5 | from pathlib import Path 6 | import pickle 7 | import copy 8 | 9 | 10 | import pytest 11 | 12 | 13 | from aiopenapi3 import OpenAPI 14 | 15 | URLBASE = "/" 16 | 17 | 18 | def test_pickle(with_paths_security_v20, with_schema_oneOf_properties, with_parsing_paths_links): 19 | """ 20 | Test pickle for 21 | * Swagger 22 | * OpenAPI 3 23 | * OpenAPI 3.1 24 | """ 25 | for dd in [with_paths_security_v20, with_schema_oneOf_properties, with_parsing_paths_links]: 26 | api = OpenAPI(URLBASE, dd) 27 | name = "test" 28 | p = Path(f"{name}.pickle") 29 | 30 | if dd == with_schema_oneOf_properties: 31 | A = api.components.schemas["A"].model_construct() 32 | with p.open("wb") as f: 33 | pickle.dump(A, f) 34 | with p.open("rb") as f: 35 | A_ = pickle.load(f) 36 | 37 | api.cache_store(p) 38 | OpenAPI.cache_load(p) 39 | 40 | 41 | def test_unpickle(): 42 | name = "test" 43 | p = Path(f"{name}.pickle") 44 | with p.open("rb") as f: 45 | A_ = pickle.load(f) 46 | 47 | 48 | def test_copy(petstore_expanded): 49 | api_ = OpenAPI(URLBASE, petstore_expanded) 50 | api = copy.copy(api_) 51 | assert api != api_ 52 | assert id(api_._security) != id(api._security) 53 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv export --no-dev --no-hashes --no-editable -o requirements.txt 3 | . 4 | annotated-types==0.7.0 5 | # via pydantic 6 | anyio==4.11.0 7 | # via httpx 8 | certifi==2025.11.12 9 | # via 10 | # httpcore 11 | # httpx 12 | dnspython==2.8.0 13 | # via email-validator 14 | email-validator==2.3.0 15 | # via aiopenapi3 16 | exceptiongroup==1.3.1 ; python_full_version < '3.11' 17 | # via anyio 18 | h11==0.16.0 19 | # via httpcore 20 | httpcore==1.0.9 21 | # via httpx 22 | httpx==0.28.1 23 | # via aiopenapi3 24 | idna==3.11 25 | # via 26 | # anyio 27 | # email-validator 28 | # httpx 29 | # yarl 30 | jmespath==1.0.1 31 | # via aiopenapi3 32 | more-itertools==10.8.0 33 | # via aiopenapi3 34 | multidict==6.7.0 35 | # via yarl 36 | propcache==0.4.1 37 | # via yarl 38 | pydantic==2.11.9 39 | # via aiopenapi3 40 | pydantic-core==2.33.2 41 | # via pydantic 42 | pyyaml==6.0.3 43 | # via aiopenapi3 44 | sniffio==1.3.1 45 | # via anyio 46 | typing-extensions==4.15.0 47 | # via 48 | # anyio 49 | # exceptiongroup 50 | # multidict 51 | # pydantic 52 | # pydantic-core 53 | # typing-inspection 54 | typing-inspection==0.4.2 55 | # via pydantic 56 | yarl==1.22.0 57 | # via aiopenapi3 58 | -------------------------------------------------------------------------------- /tests/fixtures/paths-response-header.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - {} 10 | 11 | paths: 12 | /: 13 | get: 14 | operationId: get 15 | responses: 16 | "200": 17 | description: "ok" 18 | content: 19 | application/json: 20 | schema: 21 | type: string 22 | enum: 23 | - get 24 | headers: 25 | X-required: 26 | required: true 27 | schema: 28 | type: string 29 | X-optional: 30 | schema: 31 | type: array 32 | items: 33 | type: string 34 | /types: 35 | get: 36 | operationId: types 37 | responses: 38 | "200": 39 | description: "ok" 40 | content: 41 | application/json: 42 | schema: 43 | type: string 44 | enum: 45 | - types 46 | headers: 47 | X-object: 48 | required: true 49 | schema: 50 | type: object 51 | properties: 52 | A: 53 | type: integer 54 | B: 55 | type: string 56 | -------------------------------------------------------------------------------- /src/aiopenapi3/v31/media.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from pydantic import Field 4 | 5 | from ..base import ObjectExtended 6 | 7 | from .example import Example 8 | from .general import Reference 9 | from .schemas import Schema 10 | from .parameter import Header 11 | 12 | 13 | class Encoding(ObjectExtended): 14 | """ 15 | A single encoding definition applied to a single schema property. 16 | 17 | .. _Encoding: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#encodingObject 18 | """ 19 | 20 | contentType: str | None = Field(default=None) 21 | headers: dict[str, Header | Reference] = Field(default_factory=dict) 22 | style: str | None = Field(default=None) 23 | explode: bool | None = Field(default=None) 24 | allowReserved: bool | None = Field(default=None) 25 | 26 | 27 | class MediaType(ObjectExtended): 28 | """ 29 | A `MediaType`_ object provides schema and examples for the media type identified 30 | by its key. These are used in a RequestBody object. 31 | 32 | .. _MediaType: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#media-type-object 33 | """ 34 | 35 | schema_: Schema | None = Field(default=None, alias="schema") 36 | example: Any | None = Field(default=None) # 'any' type 37 | examples: dict[str, Example | Reference] = Field(default_factory=dict) 38 | encoding: dict[str, Encoding] = Field(default_factory=dict) 39 | -------------------------------------------------------------------------------- /tests/fixtures/schema-discriminated-union-warning.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: enum test 5 | 6 | components: 7 | schemas: 8 | A: 9 | title: A 10 | type: object 11 | additionalProperties: false 12 | properties: 13 | object_type: 14 | type: string 15 | enum: ["a"] 16 | a: 17 | type: string 18 | 19 | B: 20 | title: B 21 | type: object 22 | additionalProperties: false 23 | properties: 24 | object_type: 25 | type: string 26 | enum: ["b"] 27 | b: 28 | type: string 29 | 30 | C: 31 | title: B 32 | type: object 33 | additionalProperties: false 34 | properties: 35 | object_type: 36 | type: string 37 | enum: ["c"] 38 | c: 39 | type: string 40 | 41 | ABC: 42 | title: AB 43 | type: object 44 | additionalProperties: false 45 | oneOf: 46 | - $ref: '#/components/schemas/A' 47 | - $ref: '#/components/schemas/B' 48 | - $ref: '#/components/schemas/C' 49 | discriminator: 50 | propertyName: object_type 51 | mapping: 52 | a: '#/components/schemas/A' 53 | b: '#/components/schemas/B' 54 | c: '#/components/schemas/C' 55 | 56 | L: 57 | type: array 58 | items: 59 | $ref: '#/components/schemas/ABC' 60 | -------------------------------------------------------------------------------- /src/aiopenapi3/v31/components.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | 3 | from ..base import ObjectExtended 4 | 5 | from .example import Example 6 | from .paths import RequestBody, Link, Response, Callback, PathItem 7 | from .general import Reference 8 | from .parameter import Header, Parameter 9 | from .schemas import Schema 10 | from .security import SecurityScheme 11 | 12 | 13 | class Components(ObjectExtended): 14 | """ 15 | A `Components Object`_ holds a reusable set of different aspects of the OAS 16 | spec. 17 | 18 | .. _Components Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#components-object 19 | """ 20 | 21 | schemas: dict[str, Schema] = Field(default_factory=dict) 22 | responses: dict[str, Response | Reference] = Field(default_factory=dict) 23 | parameters: dict[str, Parameter | Reference] = Field(default_factory=dict) 24 | examples: dict[str, Example | Reference] = Field(default_factory=dict) 25 | requestBodies: dict[str, RequestBody | Reference] = Field(default_factory=dict) 26 | headers: dict[str, Header | Reference] = Field(default_factory=dict) 27 | securitySchemes: dict[str, SecurityScheme | Reference] = Field(default_factory=dict) 28 | links: dict[str, Link | Reference] = Field(default_factory=dict) 29 | callbacks: dict[str, Callback | Reference] = Field(default_factory=dict) 30 | pathItems: dict[str, PathItem | Reference] = Field(default_factory=dict) # v3.1 31 | -------------------------------------------------------------------------------- /tests/parse_data_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import pathlib 4 | 5 | import yarl 6 | 7 | import aiopenapi3.loader 8 | from aiopenapi3 import FileSystemLoader, OpenAPI 9 | 10 | URLBASE = yarl.URL("http://127.1.1.1/open5gs/") 11 | 12 | 13 | def pytest_generate_tests(metafunc): 14 | argnames, dir, filterfn = metafunc.cls.params[metafunc.function.__name__] 15 | dir = pathlib.Path(dir).expanduser() 16 | metafunc.parametrize( 17 | argnames, 18 | [[dir, i.name] for i in sorted(filter(filterfn, dir.iterdir() if dir.exists() else []), key=lambda x: x.name)], 19 | ) 20 | 21 | 22 | @pytest.mark.skip 23 | class TestParseData: 24 | # a map specifying multiple argument sets for a test method 25 | params = { 26 | "test_data": [("dir", "file"), "tests/data", lambda x: x.is_file() and x.suffix in (".json", ".yaml")], 27 | "test_data_open5gs": [ 28 | ("dir", "file"), 29 | "tests/data/open5gs/", 30 | lambda x: x.is_file() 31 | and x.suffix in (".json", ".yaml") 32 | and x.name.split("_")[0] not in ("TS29520", "TS29509", "TS29544", "TS29517"), 33 | ], 34 | } 35 | 36 | def test_data(self, dir, file): 37 | OpenAPI.load_file(str(URLBASE / file), pathlib.Path(file), loader=FileSystemLoader(pathlib.Path(dir))) 38 | 39 | def test_data_open5gs(self, dir, file): 40 | OpenAPI.load_file(str(URLBASE / file), pathlib.Path(file), loader=FileSystemLoader(pathlib.Path(dir))) 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: aiopenapi3 Bug report 2 | description: Report a bug or unexpected behavior in aiopenapi3 3 | labels: [bug, unconfirmed] 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thank you for taking the time to report a problem. 9 | 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Description of the problem 14 | description: | 15 | Provide the detail required to understand the problem. 16 | Intention, Environment, Expectations, Failure 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: example 22 | attributes: 23 | label: Example - Description Document/Code 24 | description: > 25 | [MRE](https://stackoverflow.com/help/minimal-reproducible-example) demonstrating the bug. 26 | Description Document (and Code). 27 | placeholder: | 28 | ... 29 | render: yaml 30 | 31 | - type: textarea 32 | id: version 33 | attributes: 34 | label: Python, Pydantic & OS Version 35 | description: | 36 | Which version of Python & Pydantic are you using, and which Operating System? 37 | 38 | Please run the following command and copy the output below: 39 | 40 | ```bash 41 | python3 -c "import pydantic.version; print(pydantic.version.version_info()); import aiopenapi3; print(aiopenapi3.__version__)" 42 | ``` 43 | render: Text 44 | validations: 45 | required: true 46 | -------------------------------------------------------------------------------- /src/aiopenapi3/v30/media.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from typing import Union, Any 3 | 4 | from pydantic import Field 5 | 6 | from ..base import ObjectExtended 7 | 8 | from .example import Example 9 | from .general import Reference 10 | from .schemas import Schema 11 | 12 | if typing.TYPE_CHECKING: 13 | from .paths import Header 14 | 15 | 16 | class Encoding(ObjectExtended): 17 | """ 18 | A single encoding definition applied to a single schema property. 19 | 20 | .. _Encoding: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encoding-object 21 | """ 22 | 23 | contentType: str | None = Field(default=None) 24 | headers: dict[str, Union["Header", Reference]] = Field(default_factory=dict) 25 | style: str | None = Field(default=None) 26 | explode: bool | None = Field(default=None) 27 | allowReserved: bool | None = Field(default=None) 28 | 29 | 30 | class MediaType(ObjectExtended): 31 | """ 32 | A `MediaType`_ object provides schema and examples for the media type identified 33 | by its key. These are used in a RequestBody object. 34 | 35 | .. _MediaType: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#media-type-object 36 | """ 37 | 38 | schema_: Schema | Reference | None = Field(default=None, alias="schema") 39 | example: Any | None = Field(default=None) # 'any' type 40 | examples: dict[str, Example | Reference] = Field(default_factory=dict) 41 | encoding: dict[str, Encoding] = Field(default_factory=dict) 42 | -------------------------------------------------------------------------------- /tests/fixtures/schema-Of-parent-properties.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | 3 | info: 4 | version: '1.0' 5 | title: 'having any/oneOf with parent properties' 6 | description: "yes" 7 | 8 | servers: [] 9 | paths: {} 10 | 11 | components: 12 | schemas: 13 | A: 14 | type: object 15 | anyOf: 16 | - required: [a] 17 | - required: [b] 18 | - required: [c] 19 | properties: 20 | a: 21 | type: boolean 22 | b: 23 | type: boolean 24 | c: 25 | type: boolean 26 | 27 | B: 28 | type: object 29 | allOf: 30 | - anyOf: 31 | - required: [a] 32 | - required: [b] 33 | - required: [c] 34 | properties: 35 | a: 36 | type: boolean 37 | b: 38 | type: boolean 39 | c: 40 | type: boolean 41 | 42 | C: 43 | type: object 44 | allOf: 45 | - oneOf: 46 | - required: [a] 47 | - required: [b] 48 | - required: [c] 49 | properties: 50 | a: 51 | type: boolean 52 | b: 53 | type: boolean 54 | c: 55 | type: boolean 56 | 57 | D: 58 | type: object 59 | anyOf: 60 | - oneOf: 61 | - required: [a] 62 | - required: [b] 63 | - required: [c] 64 | properties: 65 | a: 66 | type: boolean 67 | b: 68 | type: boolean 69 | c: 70 | type: boolean 71 | -------------------------------------------------------------------------------- /tests/debug_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import httpx 3 | 4 | import aiopenapi3.debug 5 | from aiopenapi3 import OpenAPI, ResponseSchemaError 6 | 7 | 8 | def test_debug_log(httpx_mock, petstore_expanded): 9 | httpx_mock.add_response(headers={"Content-Type": "application/json"}, json={"foo": 1}) 10 | 11 | def debug_session_factory(*args, **kwargs) -> httpx.Client: 12 | s = httpx.Client(*args, event_hooks=aiopenapi3.debug.httpx_debug_event_hooks(), **kwargs) 13 | return s 14 | 15 | api = OpenAPI("test.yaml", petstore_expanded, session_factory=debug_session_factory) 16 | 17 | with pytest.raises(ResponseSchemaError) as r: 18 | p = api._.find_pet_by_id(data={}, parameters={"id": 1}) 19 | 20 | 21 | @pytest.mark.asyncio(loop_scope="session") 22 | async def test_debug_log_async(httpx_mock, petstore_expanded): 23 | httpx_mock.add_response(headers={"Content-Type": "application/json"}, json={"foo": 1}) 24 | 25 | def debug_session_factory(*args, **kwargs) -> httpx.AsyncClient: 26 | s = httpx.AsyncClient(*args, event_hooks=aiopenapi3.debug.httpx_debug_event_hooks_async(), **kwargs) 27 | return s 28 | 29 | api = OpenAPI("test.yaml", petstore_expanded, session_factory=debug_session_factory) 30 | 31 | with pytest.raises(ResponseSchemaError) as r: 32 | p = await api._.find_pet_by_id(data={}, parameters={"id": 1}) 33 | 34 | 35 | def test_debug_dump(petstore_expanded): 36 | api = OpenAPI( 37 | "test.yaml", petstore_expanded, plugins=[aiopenapi3.debug.DescriptionDocumentDumper("debug-test.yaml")] 38 | ) 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, William Smith 2 | Copyright (c) 2022, Markus Kötter 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its contributors 16 | may be used to endorse or promote products derived from this software without 17 | specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /src/aiopenapi3/v31/__init__.py: -------------------------------------------------------------------------------- 1 | import pydantic_core 2 | 3 | from .components import Components 4 | from .example import Example 5 | from .general import ExternalDocumentation, Reference 6 | from .info import Contact, License, Info 7 | from .media import Encoding, MediaType 8 | from .parameter import Parameter, Header 9 | from .paths import RequestBody, Link, Response, Operation, PathItem, Paths, Callback, RuntimeExpression 10 | from .root import Root 11 | from .schemas import Discriminator, Schema 12 | from .security import OAuthFlow, OAuthFlows, SecurityScheme, SecurityRequirement 13 | from .servers import ServerVariable, Server 14 | from .tag import Tag 15 | from .xml import XML 16 | 17 | 18 | def __init(): 19 | r = dict() 20 | CLASSES = [ 21 | Components, 22 | Example, 23 | ExternalDocumentation, 24 | Reference, 25 | Contact, 26 | License, 27 | Info, 28 | Encoding, 29 | MediaType, 30 | Parameter, 31 | Header, 32 | RequestBody, 33 | Link, 34 | Response, 35 | Operation, 36 | PathItem, 37 | Paths, 38 | Callback, 39 | RuntimeExpression, 40 | Discriminator, 41 | Schema, 42 | OAuthFlow, 43 | OAuthFlows, 44 | SecurityScheme, 45 | SecurityRequirement, 46 | ServerVariable, 47 | Server, 48 | Tag, 49 | XML, 50 | Root, 51 | ] 52 | for i in CLASSES: 53 | r[i.__name__] = i 54 | for i in CLASSES: 55 | i.model_rebuild(_types_namespace=r) 56 | 57 | 58 | __init() 59 | -------------------------------------------------------------------------------- /tests/fixtures/parsing-paths-links.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: Example spec with valid links 5 | paths: 6 | /with-links: 7 | get: 8 | operationId: withLinks 9 | responses: 10 | '200': 11 | description: This has links 12 | content: 13 | applicaton/json: 14 | schema: 15 | type: object 16 | properties: 17 | test: 18 | type: string 19 | description: A test response fields 20 | example: foobar 21 | links: 22 | exampleWithOperationId: 23 | operationId: withLinksTwo 24 | parameters: 25 | param: baz 26 | /with-links-two/{param}: 27 | parameters: 28 | - name: param 29 | in: path 30 | required: true 31 | schema: 32 | type: string 33 | get: 34 | operationId: withLinksTwo 35 | responses: 36 | '200': 37 | description: This has links too 38 | content: 39 | application/json: 40 | schema: 41 | type: object 42 | properties: 43 | test2: 44 | type: string 45 | description: Another test response 46 | example: foobaz 47 | links: 48 | exampleWithRef: 49 | $ref: '#/components/links/exampleWithOperationRef' 50 | components: 51 | links: 52 | exampleWithOperationRef: 53 | operationRef: '/with-links' 54 | -------------------------------------------------------------------------------- /src/aiopenapi3/v20/root.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field, validator 2 | 3 | from .general import Reference, ExternalDocumentation 4 | from .info import Info 5 | from .parameter import Parameter 6 | from .paths import Response, Paths, PathItem 7 | from .schemas import Schema 8 | from .security import SecurityScheme, SecurityRequirement 9 | from .tag import Tag 10 | from ..base import ObjectExtended, RootBase 11 | 12 | 13 | class Root(ObjectExtended, RootBase): 14 | """ 15 | This is the root document object for the API specification. 16 | 17 | https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#swagger-object 18 | """ 19 | 20 | swagger: str = Field(...) 21 | info: Info = Field(...) 22 | host: str | None = Field(default=None) 23 | basePath: str | None = Field(default=None) 24 | schemes: list[str] = Field(default_factory=list) 25 | consumes: list[str] = Field(default_factory=list) 26 | produces: list[str] = Field(default_factory=list) 27 | paths: Paths = Field(default_factory=dict) 28 | definitions: dict[str, Schema] = Field(default_factory=dict) 29 | parameters: dict[str, Parameter] = Field(default_factory=dict) 30 | responses: dict[str, Response] = Field(default_factory=dict) 31 | securityDefinitions: dict[str, SecurityScheme] = Field(default_factory=dict) 32 | security: list[SecurityRequirement] | None = Field(default=None) 33 | tags: list[Tag] = Field(default_factory=list) 34 | externalDocs: ExternalDocumentation | None = Field(default=None) 35 | 36 | def _resolve_references(self, api): 37 | RootBase.resolve(api, self, self, PathItem, Reference) 38 | -------------------------------------------------------------------------------- /src/aiopenapi3/v30/__init__.py: -------------------------------------------------------------------------------- 1 | from .glue import Request, AsyncRequest 2 | 3 | import pydantic_core 4 | 5 | from .components import Components 6 | from .example import Example 7 | from .general import ExternalDocumentation, Reference 8 | from .info import Contact, License, Info 9 | from .media import Encoding, MediaType 10 | from .parameter import Parameter, Header 11 | from .paths import RequestBody, Link, Response, Operation, PathItem, Paths, Callback, RuntimeExpression 12 | from .root import Root 13 | from .schemas import Discriminator, Schema 14 | from .security import OAuthFlow, OAuthFlows, SecurityScheme, SecurityRequirement 15 | from .servers import ServerVariable, Server 16 | from .tag import Tag 17 | from .xml import XML 18 | 19 | 20 | def __init(): 21 | r = dict() 22 | CLASSES = [ 23 | Components, 24 | Example, 25 | ExternalDocumentation, 26 | Reference, 27 | Contact, 28 | License, 29 | Info, 30 | Encoding, 31 | MediaType, 32 | Parameter, 33 | Header, 34 | RequestBody, 35 | Link, 36 | Response, 37 | Operation, 38 | PathItem, 39 | Paths, 40 | Callback, 41 | RuntimeExpression, 42 | Discriminator, 43 | Schema, 44 | OAuthFlow, 45 | OAuthFlows, 46 | SecurityScheme, 47 | SecurityRequirement, 48 | ServerVariable, 49 | Server, 50 | Tag, 51 | XML, 52 | Root, 53 | ] 54 | for i in CLASSES: 55 | r[i.__name__] = i 56 | for i in CLASSES: 57 | i.model_rebuild(_types_namespace=r) 58 | 59 | 60 | __init() 61 | -------------------------------------------------------------------------------- /tests/fixtures/schema-ref-nesting.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: OpenStreetMap circular refs 4 | description: This is the editing API for OpenStreetMap. 5 | version: '0.6' 6 | servers: 7 | - url: '/' 8 | paths: 9 | /api/0.6/map: 10 | get: 11 | summary: Retrieves map data by the given bounding box. 12 | description: | 13 | The operation returns: 14 | operationId: getMapDataByBoundingBox 15 | responses: 16 | '200': 17 | description: okay 18 | content: 19 | application/json: 20 | schema: 21 | type: object 22 | properties: 23 | elements: 24 | type: array 25 | items: 26 | oneOf: 27 | - $ref: '#/components/schemas/Node' 28 | components: 29 | schemas: 30 | Node: 31 | allOf: 32 | - $ref: '#/components/schemas/Way/allOf/0' 33 | Way: 34 | allOf: 35 | - type: object 36 | properties: 37 | type: 38 | type: string 39 | Relation: 40 | allOf: 41 | - type: object 42 | properties: 43 | members: 44 | type: array 45 | items: 46 | type: object 47 | properties: 48 | type: 49 | $ref: '#/paths/~1api~10.6~1map/get/responses/200/content/application~1json/schema/properties/elements/items/oneOf/0/allOf/0/properties/type' 50 | required: 51 | - members 52 | externalDocs: 53 | description: Find more information on the OSM wiki 54 | url: 'https://wiki.openstreetmap.org/' 55 | -------------------------------------------------------------------------------- /src/aiopenapi3/v31/root.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from pydantic import Field, model_validator 4 | 5 | from ..base import ObjectExtended, RootBase 6 | 7 | from .info import Info 8 | from .paths import Paths, PathItem 9 | from .security import SecurityRequirement 10 | from .servers import Server 11 | 12 | from .components import Components 13 | from .general import Reference 14 | from .tag import Tag 15 | 16 | 17 | class Root(ObjectExtended, RootBase): 18 | """ 19 | This class represents the root of the OpenAPI schema document, as defined 20 | in `the spec`_ 21 | 22 | .. _the spec: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#openapi-object 23 | """ 24 | 25 | openapi: str = Field(...) 26 | info: Info = Field(...) 27 | jsonSchemaDialect: str | None = Field(default=None) # FIXME should be URI 28 | servers: list[Server] | None = Field(default_factory=list) 29 | # paths: Dict[str, PathItem] = Field(default_factory=dict) 30 | paths: Paths = Field(default_factory=dict) 31 | webhooks: dict[str, PathItem | Reference] = Field(default_factory=dict) 32 | components: Components | None = Field(default_factory=Components) 33 | security: list[SecurityRequirement] | None = Field(default_factory=list) 34 | tags: list[Tag] = Field(default_factory=list) 35 | externalDocs: dict[Any, Any] = Field(default_factory=dict) 36 | 37 | @model_validator(mode="after") 38 | @classmethod 39 | def validate_Root(cls, r: "Root") -> "Self": 40 | assert r.paths or r.components or r.webhooks 41 | return r 42 | 43 | def _resolve_references(self, api): 44 | RootBase.resolve(api, self, self, PathItem, Reference) 45 | -------------------------------------------------------------------------------- /docs/source/_static/my_custom.js: -------------------------------------------------------------------------------- 1 | // rewrite the url of the home-icon so we can have the toc on a page which is not index 2 | // and have the home icon link to the index instead of the toc 3 | 4 | // https://github.com/readthedocs/sphinx_rtd_theme/issues/777#issuecomment-731571737 5 | // this function changes a relative URL into an absolute url 6 | // from https://stackoverflow.com/questions/14780350/convert-relative-path-to-absolute-using-javascript 7 | function absolute_url(base, relative) { 8 | var stack = base.split("/"), 9 | parts = relative.split("/"); 10 | stack.pop(); // remove current file name (or empty string) 11 | // (omit if "base" is the current folder without trailing slash) 12 | for (var i=0; i "Document.Context": 13 | if self.path.suffix in [".yaml", ".yml"]: 14 | with self.path.open("wt") as f: 15 | yaml.safe_dump(ctx.document, f) 16 | elif self.path.suffix == ".json": 17 | self.path.write_text(json.dumps(ctx.document)) 18 | return ctx 19 | 20 | 21 | def log_request(request): 22 | print(f"Request event hook: {request.method} {request.url} - Waiting for response") 23 | for k, v in request.headers.items(): 24 | print(f"{k}:{v}") 25 | 26 | 27 | async def log_request_async(request): 28 | log_request(request) 29 | 30 | 31 | def log_response(response): 32 | request = response.request 33 | print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}") 34 | data = request.read() 35 | if request.headers.get("content-type", "") == "application/json": 36 | try: 37 | if data := request.read(): 38 | print(json.dumps(json.loads(data.decode()), indent=4)) 39 | except Exception as e: 40 | print(e) 41 | 42 | 43 | async def log_response_async(response): 44 | log_response(response) 45 | 46 | 47 | def httpx_debug_event_hooks(): 48 | return {"request": [log_request], "response": [log_response]} 49 | 50 | 51 | def httpx_debug_event_hooks_async(): 52 | return {"request": [log_request_async], "response": [log_response_async]} 53 | -------------------------------------------------------------------------------- /src/aiopenapi3/v20/general.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from typing import Any, Union 3 | 4 | from pydantic import Field, ConfigDict, PrivateAttr 5 | 6 | 7 | from ..base import ObjectExtended, ObjectBase, ReferenceBase 8 | 9 | if typing.TYPE_CHECKING: 10 | from .schemas import Schema 11 | from .parameter import Parameter 12 | 13 | 14 | class ExternalDocumentation(ObjectExtended): 15 | """ 16 | An `External Documentation Object`_ references external resources for extended 17 | documentation. 18 | 19 | .. _External Documentation Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#external-documentation-object 20 | """ 21 | 22 | description: str | None = Field(default=None) 23 | url: str = Field(...) 24 | 25 | 26 | class Reference(ObjectBase, ReferenceBase): 27 | """ 28 | A `Reference Object`_ designates a reference to another node in the specification. 29 | 30 | .. _Reference Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#reference-object 31 | """ 32 | 33 | ref: str = Field(alias="$ref") 34 | 35 | _target: Union["Schema", "Parameter", "Reference"] = PrivateAttr(default=None) 36 | 37 | model_config = ConfigDict(extra="ignore") 38 | 39 | def __getattr__(self, item: str) -> Any: 40 | if item != "_target" and not item.startswith("__pydantic_private__"): 41 | return getattr(self._target, item) 42 | else: 43 | return super().__getattr__(item) 44 | 45 | def __setattr__(self, item, value): 46 | if item != "_target" and not item.startswith("__pydantic_private__"): 47 | setattr(self._target, item, value) 48 | else: 49 | super().__setattr__(item, value) 50 | -------------------------------------------------------------------------------- /tests/fixtures/schema-recursion.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Any OpenAPI schema with recursion 5 | 6 | components: 7 | schemas: 8 | B: 9 | type: object 10 | properties: 11 | ofB: 12 | type: string 13 | a: 14 | $ref: '#/components/schemas/A' 15 | 16 | A: 17 | type: object 18 | properties: 19 | ofA: 20 | type: integer 21 | b: 22 | $ref: '#/components/schemas/B' 23 | 24 | C: 25 | type: object 26 | properties: 27 | a: 28 | $ref: '#/components/schemas/A' 29 | 30 | Expressions: 31 | type: array 32 | items: 33 | $ref: '#/components/schemas/Expression' 34 | 35 | Expression: 36 | type: object 37 | properties: 38 | Or: 39 | allOf: 40 | - $ref: '#/components/schemas/Expressions' 41 | - description: Return results that match either Dimension object. 42 | And: 43 | allOf: 44 | - $ref: '#/components/schemas/Expressions' 45 | - description: Return results that match both Dimension objects. 46 | Not: 47 | allOf: 48 | - $ref: '#/components/schemas/Expression' 49 | - description: Return results that don't match a Dimension object. 50 | 51 | 52 | D: 53 | description: galaxyproject openapi recursive element 54 | type: object 55 | properties: 56 | E: 57 | type: string 58 | F: 59 | items: 60 | # anyOf: 61 | type: object 62 | $ref: '#/components/schemas/D' 63 | title: Elements 64 | type: array 65 | -------------------------------------------------------------------------------- /tests/fixtures/schema-additionalProperties.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: with additionalProperties 5 | 6 | components: 7 | schemas: 8 | A: 9 | type: object 10 | additionalProperties: 11 | type: integer 12 | format: int32 13 | 14 | B: 15 | type: object 16 | additionalProperties: 17 | $ref: "#/components/schemas/A" 18 | 19 | C: 20 | type: object 21 | additionalProperties: true 22 | properties: 23 | i: 24 | type: integer 25 | 26 | D: 27 | type: object 28 | additionalProperties: false 29 | 30 | Translation: 31 | type: object 32 | additionalProperties: 33 | type: string 34 | 35 | Errors: 36 | type: object 37 | additionalProperties: 38 | type: object 39 | properties: 40 | code: 41 | type: integer 42 | text: 43 | type: string 44 | 45 | Errnos: # <---- dictionary 46 | type: object 47 | additionalProperties: 48 | $ref: '#/components/schemas/Errno' 49 | Errno: 50 | type: object 51 | additionalProperties: false 52 | required: [code, text] 53 | properties: 54 | code: 55 | type: integer 56 | text: 57 | type: string 58 | 59 | Annotations: 60 | type: object 61 | additionalProperties: 62 | type: object 63 | description: Additional information provided through arbitrary metadata. 64 | properties: 65 | org.opencontainers.image.authors: 66 | description: Contact details of the people or organization responsible for the image. 67 | type: string 68 | x-ms-client-name: Authors 69 | -------------------------------------------------------------------------------- /tests/api/v2/schema.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | import uuid 3 | 4 | 5 | from typing import Literal, Union, Annotated 6 | 7 | import pydantic 8 | from pydantic import BaseModel, RootModel, Field, ConfigDict 9 | 10 | # from pydantic.fields import Undefined 11 | from pydantic_core import PydanticUndefined as Undefined 12 | 13 | 14 | class PetBase(BaseModel): 15 | model_config = ConfigDict(extra="forbid") 16 | identifier: str | None = Field(default_factory=lambda: str(uuid.uuid4())) 17 | name: str 18 | tags: list[str] = Field(default_factory=list) 19 | 20 | 21 | class BlackCat(PetBase): 22 | pet_type: Literal["cat"] = "cat" 23 | color: Literal["black"] = "black" 24 | black_name: str 25 | 26 | 27 | class WhiteCat(PetBase): 28 | pet_type: Literal["cat"] = "cat" 29 | color: Literal["white"] = "white" 30 | white_name: str 31 | 32 | 33 | class Cat(RootModel[Annotated[Union[BlackCat, WhiteCat], Field(discriminator="color")]]): 34 | def __getattr__(self, item): 35 | return getattr(self.root, item) 36 | 37 | def __setattr__(self, item, value): 38 | return setattr(self.root, item, value) 39 | 40 | 41 | class Dog(PetBase): 42 | pet_type: Literal["dog"] = "dog" 43 | name: str 44 | age: timedelta 45 | 46 | 47 | class Pet(RootModel[Annotated[Union[Cat, Dog], Field(discriminator="pet_type")]]): 48 | def __getattr__(self, item): 49 | return getattr(self.root, item) 50 | 51 | def __setattr__(self, item, value): 52 | return setattr(self.root, item, value) 53 | 54 | 55 | # class Pet(RootModel): 56 | # root: Annotated[Union[Cat, Dog], Field(discriminator="pet_type")] 57 | 58 | 59 | Pets = list[Pet] 60 | 61 | 62 | class Error(BaseModel): 63 | code: int 64 | message: str 65 | -------------------------------------------------------------------------------- /src/aiopenapi3/v20/security.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Annotated, Literal 2 | 3 | from pydantic import Field, RootModel 4 | 5 | from ..base import ObjectExtended 6 | 7 | 8 | class _SecuritySchemes: 9 | class _SecurityScheme(ObjectExtended): 10 | type: Literal["basic", "apiKey", "oauth2"] 11 | description: str | None = Field(default=None) 12 | 13 | def validate_authentication_value(self, value): 14 | pass 15 | 16 | class basic(_SecurityScheme): 17 | type: Literal["basic"] 18 | 19 | class apiKey(_SecurityScheme): 20 | type: Literal["apiKey"] 21 | in_: str = Field(alias="in") 22 | name: str 23 | 24 | class oauth2(_SecurityScheme): 25 | type: Literal["oauth2"] 26 | flow: Literal["implicit", "password", "application", "accessCode"] 27 | authorizationUrl: str 28 | tokenUrl: str 29 | scopes: dict[str, str] 30 | 31 | 32 | class SecurityScheme( 33 | RootModel[ 34 | Annotated[ 35 | Union[ 36 | _SecuritySchemes.basic, 37 | _SecuritySchemes.apiKey, 38 | _SecuritySchemes.oauth2, 39 | ], 40 | Field(discriminator="type"), 41 | ] 42 | ] 43 | ): 44 | """ 45 | Allows the definition of a security scheme that can be used by the operations. 46 | 47 | https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#security-scheme-object 48 | """ 49 | 50 | pass 51 | 52 | 53 | class SecurityRequirement(RootModel): 54 | """ 55 | Lists the required security schemes to execute this operation. 56 | 57 | https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#security-requirement-object 58 | """ 59 | 60 | root: dict[str, list[str]] 61 | -------------------------------------------------------------------------------- /tests/fixtures/schema-boolean.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: 1.0.0 4 | title: Example 5 | license: 6 | name: MIT 7 | description: | 8 | https://github.com/swagger-api/swagger-parser/issues/1770 9 | servers: 10 | - url: http://api.example.xyz/v1 11 | paths: 12 | /person/display/{personId}: 13 | get: 14 | parameters: 15 | - name: personId 16 | in: path 17 | required: true 18 | description: The id of the person to retrieve 19 | schema: 20 | type: string 21 | operationId: list 22 | responses: 23 | '200': 24 | description: OK 25 | content: 26 | application/json: 27 | schema: 28 | $ref: "#/components/schemas/BooleanTrue" 29 | components: 30 | schemas: 31 | BooleanTrue: true 32 | ArrayWithTrueItems: 33 | type: array 34 | items: true 35 | ObjectWithTrueProperty: 36 | properties: 37 | someProp: true 38 | ObjectWithTrueAdditionalProperties: 39 | additionalProperties: true 40 | AllOfWithTrue: 41 | allOf: 42 | - true 43 | AnyOfWithTrue: 44 | anyOf: 45 | - true 46 | OneOfWithTrue: 47 | oneOf: 48 | - true 49 | NotWithTrue: 50 | not: true 51 | UnevaluatedItemsTrue: 52 | unevaluatedItems: true 53 | UnevaluatedPropertiesTrue: 54 | unevaluatedProperties: true 55 | PrefixitemsWithNoAdditionalItemsAllowed: 56 | $schema: https://json-schema.org/draft/2020-12/schema 57 | prefixItems: 58 | - {} 59 | - {} 60 | - {} 61 | items: false 62 | PrefixitemsWithBooleanSchemas: 63 | $schema: https://json-schema.org/draft/2020-12/schema 64 | prefixItems: 65 | - true 66 | - false 67 | -------------------------------------------------------------------------------- /tests/fixtures/schema-additionalProperties-v20.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: with additionalProperties 4 | description: with additionalProperties 5 | version: 1.0.0 6 | host: api.example.com 7 | basePath: /v1 8 | schemes: 9 | - https 10 | 11 | consumes: 12 | - application/json 13 | produces: 14 | - application/json 15 | 16 | paths: {} 17 | 18 | definitions: 19 | 20 | A: 21 | type: object 22 | additionalProperties: 23 | type: integer 24 | format: int32 25 | 26 | B: 27 | type: object 28 | additionalProperties: 29 | $ref: "#/definitions/A" 30 | 31 | C: 32 | type: object 33 | additionalProperties: true 34 | properties: 35 | i: 36 | type: integer 37 | 38 | D: 39 | type: object 40 | additionalProperties: false 41 | 42 | Translation: 43 | type: object 44 | additionalProperties: 45 | type: string 46 | 47 | Errors: 48 | type: object 49 | additionalProperties: 50 | type: object 51 | properties: 52 | code: 53 | type: integer 54 | text: 55 | type: string 56 | 57 | Errnos: # <---- dictionary 58 | type: object 59 | additionalProperties: 60 | $ref: '#/definitions/Errno' 61 | Errno: 62 | type: object 63 | additionalProperties: false 64 | required: [code, text] 65 | properties: 66 | code: 67 | type: integer 68 | text: 69 | type: string 70 | 71 | Annotations: 72 | type: object 73 | additionalProperties: 74 | type: object 75 | description: Additional information provided through arbitrary metadata. 76 | properties: 77 | org.opencontainers.image.authors: 78 | description: Contact details of the people or organization responsible for the image. 79 | type: string 80 | x-ms-client-name: Authors 81 | -------------------------------------------------------------------------------- /src/aiopenapi3/v31/info.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field, EmailStr, model_validator 2 | 3 | from aiopenapi3.base import ObjectExtended 4 | 5 | 6 | class Contact(ObjectExtended): 7 | """ 8 | Contact object belonging to an Info object, as described `here`_ 9 | 10 | .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#contactObject 11 | """ 12 | 13 | email: EmailStr = Field(default=None) 14 | name: str = Field(default=None) 15 | url: str = Field(default=None) 16 | 17 | 18 | class License(ObjectExtended): 19 | """ 20 | License object belonging to an Info object, as described `here`_ 21 | 22 | .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#license-object 23 | """ 24 | 25 | name: str = Field(...) 26 | identifier: str | None = Field(default=None) 27 | url: str | None = Field(default=None) 28 | 29 | @model_validator(mode="after") 30 | @classmethod 31 | def validate_License(cls, l: "License"): 32 | """ 33 | 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. 34 | """ 35 | assert not all([getattr(l, i, None) is not None for i in ["identifier", "url"]]) 36 | return l 37 | 38 | 39 | class Info(ObjectExtended): 40 | """ 41 | An OpenAPI Info object, as defined in `the spec`_. 42 | 43 | .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#info-object 44 | """ 45 | 46 | title: str = Field(...) 47 | summary: str | None = Field(default=None) 48 | description: str | None = Field(default=None) 49 | termsOfService: str | None = Field(default=None) 50 | contact: Contact | None = Field(default=None) 51 | license: License | None = Field(default=None) 52 | version: str = Field(...) 53 | -------------------------------------------------------------------------------- /src/aiopenapi3/v30/general.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from typing import Union, Any 3 | 4 | from pydantic import Field, PrivateAttr, ConfigDict 5 | 6 | 7 | from ..base import ObjectExtended, ObjectBase, ReferenceBase 8 | 9 | 10 | if typing.TYPE_CHECKING: 11 | from .schemas import Schema 12 | from .parameter import Parameter 13 | 14 | 15 | class ExternalDocumentation(ObjectExtended): 16 | """ 17 | An `External Documentation Object`_ references external resources for extended 18 | documentation. 19 | 20 | .. _External Documentation Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#external-documentation-object 21 | """ 22 | 23 | url: str = Field(...) 24 | description: str | None = Field(default=None) 25 | 26 | 27 | class Reference(ObjectBase, ReferenceBase): 28 | """ 29 | A `Reference Object`_ designates a reference to another node in the specification. 30 | 31 | .. _Reference Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object 32 | """ 33 | 34 | ref: str = Field(alias="$ref") 35 | 36 | _target: Union["Schema", "Parameter", "Reference"] = PrivateAttr(default=None) 37 | 38 | model_config = ConfigDict( 39 | extra="ignore", # """This object cannot be extended with additional properties and any properties added SHALL be ignored.""" 40 | ) 41 | 42 | def __getattr__(self, item: str) -> Any: 43 | if item != "_target" and not item.startswith("__pydantic_private__"): 44 | return getattr(self._target, item) 45 | else: 46 | return super().__getattr__(item) 47 | 48 | def __setattr__(self, item, value): 49 | if item != "_target" and not item.startswith("__pydantic_private__"): 50 | setattr(self._target, item, value) 51 | else: 52 | super().__setattr__(item, value) 53 | -------------------------------------------------------------------------------- /tests/error_test.py: -------------------------------------------------------------------------------- 1 | from aiopenapi3 import OpenAPI 2 | from aiopenapi3 import ResponseSchemaError, ContentTypeError, HTTPStatusError, ResponseDecodingError, RequestError 3 | 4 | import httpx 5 | 6 | 7 | import pytest 8 | 9 | 10 | def test_response_error(httpx_mock, with_paths_response_error_vXX): 11 | api = OpenAPI("/", with_paths_response_error_vXX, session_factory=httpx.Client) 12 | 13 | httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="ok") 14 | r = api._.test() 15 | assert r == "ok" 16 | 17 | httpx_mock.add_response(headers={"Content-Type": "text/html"}, status_code=200, json="ok") 18 | with pytest.raises(ContentTypeError) as e: 19 | api._.test() 20 | str(e.value) 21 | 22 | httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=201, json="ok") 23 | with pytest.raises(HTTPStatusError) as e: 24 | api._.test() 25 | str(e.value) 26 | 27 | httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, content="'") 28 | with pytest.raises(ResponseDecodingError) as e: 29 | api._.test() 30 | str(e.value) 31 | 32 | httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="fail") 33 | with pytest.raises(ResponseSchemaError) as e: 34 | api._.test() 35 | str(e.value) 36 | 37 | 38 | def test_request_error(with_paths_response_error_vXX): 39 | class Client(httpx.Client): 40 | def __init__(self, *args, **kwargs): 41 | super().__init__(*args, transport=RaisingTransport(), **kwargs) 42 | 43 | class RaisingTransport(httpx.BaseTransport): 44 | def handle_request(self, request): 45 | raise httpx.TimeoutException(message="timeout") 46 | 47 | api = OpenAPI("/", with_paths_response_error_vXX, session_factory=Client) 48 | 49 | with pytest.raises(RequestError) as e: 50 | api._.test() 51 | str(e.value) 52 | -------------------------------------------------------------------------------- /src/aiopenapi3/v31/parameter.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import typing 3 | from typing import Union, Any 4 | 5 | from pydantic import Field 6 | 7 | from ..base import ObjectExtended, ParameterBase as _ParameterBase 8 | 9 | from .example import Example 10 | from .general import Reference 11 | from .schemas import Schema 12 | 13 | from ..v30.parameter import _ParameterCodec 14 | 15 | if typing.TYPE_CHECKING: 16 | from .paths import MediaType 17 | 18 | 19 | class ParameterBase(ObjectExtended, _ParameterBase): 20 | """ 21 | A `Parameter Object`_ defines a single operation parameter. 22 | 23 | .. _Parameter Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameterObject 24 | """ 25 | 26 | description: str | None = Field(default=None) 27 | required: bool | None = Field(default=None) 28 | deprecated: bool | None = Field(default=None) 29 | allowEmptyValue: bool | None = Field(default=None) 30 | 31 | style: str | None = Field(default=None) 32 | explode: bool | None = Field(default=None) 33 | allowReserved: bool | None = Field(default=None) 34 | schema_: Schema | None = Field(default=None, alias="schema") 35 | example: Any | None = Field(default=None) 36 | examples: dict[str, Union["Example", Reference]] = Field(default_factory=dict) 37 | 38 | content: dict[str, "MediaType"] | None = None 39 | 40 | 41 | class _In(str, enum.Enum): 42 | query = "query" 43 | header = "header" 44 | path = "path" 45 | cookie = "cookie" 46 | 47 | 48 | class Parameter(ParameterBase, _ParameterCodec): 49 | name: str = Field() 50 | in_: _In = Field(alias="in") 51 | 52 | 53 | class Header(ParameterBase, _ParameterCodec): 54 | """ 55 | 56 | .. _HeaderObject: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#headerObject 57 | """ 58 | 59 | def _codec(self): 60 | schema = self.schema_ or self.content.get("application/json").schema_ 61 | return schema, "simple", False 62 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | on: [push, pull_request] 3 | jobs: 4 | run: 5 | name: test ${{ matrix.os }} / ${{ matrix.python-version }} 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest] 10 | python-version: ["3.10", "3.11", "3.12", "3.13"] 11 | env: 12 | OS: ${{ matrix.os }} 13 | PYTHON: ${{ matrix.python-version }} 14 | steps: 15 | - uses: actions/checkout@master 16 | 17 | - uses: astral-sh/setup-uv@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | 21 | - name: Install core deps 22 | run: | 23 | uv sync 24 | - run: 'uv run python -c "import pydantic.version; print(pydantic.version.version_info())"' 25 | - name: Generate coverage report (core) 26 | run: | 27 | uv run pytest --cov=. --cov-report=xml:./coverage/coverage-core.xml tests/ 28 | 29 | - name: Install extra deps 30 | run: | 31 | uv sync --all-extras 32 | 33 | - name: Generate coverage report (extra) 34 | run: | 35 | uv run pytest --cov=. --cov-report=xml:./coverage/coverage-extra.xml tests/ 36 | 37 | - name: Upload coverage to Codecov (core) 38 | uses: codecov/codecov-action@v5 39 | with: 40 | token: ${{ secrets.CODECOV_TOKEN }} 41 | directory: coverage 42 | files: coverage-core.xml 43 | env_vars: OS,PYTHON 44 | fail_ci_if_error: false 45 | flags: core 46 | name: codecov-aiopenapi3 47 | verbose: true 48 | 49 | - name: Upload coverage to Codecov (extra) 50 | uses: codecov/codecov-action@v5 51 | with: 52 | token: ${{ secrets.CODECOV_TOKEN }} 53 | directory: coverage 54 | files: coverage-extra.xml 55 | env_vars: OS,PYTHON 56 | fail_ci_if_error: false 57 | flags: extras 58 | name: codecov-aiopenapi3-extras 59 | verbose: true 60 | -------------------------------------------------------------------------------- /src/aiopenapi3/v31/general.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from typing import Union, Any 3 | 4 | from pydantic import Field, AnyUrl, PrivateAttr, ConfigDict 5 | 6 | 7 | from ..base import ObjectExtended, ObjectBase, ReferenceBase 8 | 9 | if typing.TYPE_CHECKING: 10 | from .schemas import Schema 11 | from .paths import Parameter, PathItem 12 | 13 | 14 | class ExternalDocumentation(ObjectExtended): 15 | """ 16 | An `External Documentation Object`_ references external resources for extended 17 | documentation. 18 | 19 | .. _External Documentation Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#external-documentation-object 20 | """ 21 | 22 | url: AnyUrl = Field(...) 23 | description: str | None = Field(default=None) 24 | 25 | 26 | class Reference(ObjectBase, ReferenceBase): 27 | """ 28 | A `Reference Object`_ designates a reference to another node in the specification. 29 | 30 | .. _Reference Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#reference-object 31 | """ 32 | 33 | ref: str = Field(alias="$ref") 34 | summary: str | None = Field(default=None) 35 | description: str | None = Field(default=None) 36 | 37 | _target: Union["Schema", "Parameter", "Reference", "PathItem"] = PrivateAttr(default=None) 38 | 39 | model_config = ConfigDict( 40 | # """This object cannot be extended with additional properties and any properties added SHALL be ignored.""" 41 | extra="ignore" 42 | ) 43 | 44 | def __getattr__(self, item: str) -> Any: 45 | if item != "_target" and not item.startswith("__pydantic_private__"): 46 | return getattr(self._target, item) 47 | else: 48 | return super().__getattr__(item) 49 | 50 | def __setattr__(self, item, value): 51 | if item != "_target" and not item.startswith("__pydantic_private__"): 52 | setattr(self._target, item, value) 53 | else: 54 | super().__setattr__(item, value) 55 | -------------------------------------------------------------------------------- /tests/fixtures/schema-extensions.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.2" 2 | info: 3 | x-Info: Info 4 | version: 1.0.0 5 | title: with emptieness 6 | 7 | servers: 8 | - url: TBA 9 | x-Server: Server 10 | 11 | x-root: root 12 | 13 | components: 14 | schemas: 15 | X: 16 | type: string 17 | x-Schema: Schema 18 | Y: 19 | type: object 20 | additionalProperties: 21 | type: object 22 | description: Additional information provided through arbitrary metadata. 23 | properties: 24 | Z: 25 | description: Contact details of the people or organization responsible for the image. 26 | type: object 27 | x-Add: Add 28 | 29 | securitySchemes: 30 | user: 31 | x-SecurityScheme: SecurityScheme 32 | type: apiKey 33 | in: header 34 | name: x-user 35 | parameters: 36 | X: 37 | x-Parameter: Parameter 38 | name: Path 39 | in: path 40 | required: true 41 | schema: 42 | type: string 43 | 44 | 45 | 46 | security: 47 | - user: [] 48 | 49 | 50 | paths: 51 | x-Paths: Paths 52 | /x: 53 | x-PathItem: PathItem 54 | post: 55 | operationId: emptyRequest 56 | x-Operation: Operation 57 | requestBody: 58 | x-requestBody: requestBody 59 | content: 60 | multipart/form-data: 61 | x-MediaType: MediaType 62 | encoding: 63 | xml: 64 | x-Encoding: Encoding 65 | contentType: application/xml; charset=utf-8 66 | 67 | 68 | responses: 69 | "200": 70 | x-Response: Response 71 | description: "ok" 72 | content: 73 | application/json: 74 | schema: 75 | type: string 76 | headers: 77 | X-required: 78 | required: true 79 | schema: 80 | type: string 81 | X-optional: 82 | schema: 83 | type: array 84 | items: 85 | type: string 86 | -------------------------------------------------------------------------------- /tests/fixtures/extra-reduced.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.2" 2 | info: 3 | x-Info: Info 4 | version: 1.0.0 5 | title: reduction dependency tracking 6 | 7 | servers: 8 | - url: TBA 9 | 10 | components: 11 | schemas: 12 | A: 13 | type: object 14 | additionalProperties: false 15 | description: Additional information provided through arbitrary metadata. 16 | allOf: 17 | - $ref: "#/components/schemas/A0" 18 | - $ref: "#/components/schemas/A1" 19 | A0: 20 | type: object 21 | properties: 22 | a0: 23 | type: string 24 | A1: 25 | type: object 26 | properties: 27 | a1: 28 | type: string 29 | 30 | AA: 31 | type: object 32 | discriminator: 33 | propertyName: type 34 | mapping: 35 | "a": "#/components/schemas/A" 36 | 37 | parameters: 38 | A: 39 | name: Path 40 | in: path 41 | required: true 42 | schema: 43 | type: string 44 | 45 | requestBodies: 46 | A: 47 | content: 48 | application/json: 49 | schema: 50 | $ref: "#/components/schemas/AA" 51 | 52 | 53 | responses: 54 | A: 55 | description: "ok" 56 | content: 57 | application/json: 58 | schema: 59 | type: object 60 | properties: 61 | a: 62 | type: integer 63 | headers: 64 | X-A: 65 | required: true 66 | schema: 67 | type: string 68 | 69 | 70 | security: [] 71 | 72 | 73 | paths: 74 | /A/{Path}: 75 | parameters: 76 | - $ref: '#/components/parameters/A' 77 | post: 78 | operationId: A 79 | requestBody: 80 | $ref: "#/components/requestBodies/A" 81 | 82 | responses: 83 | "200": 84 | $ref: "#/components/responses/A" 85 | 86 | /B: 87 | get: 88 | operationId: B 89 | responses: 90 | "200": 91 | description: "ok" 92 | content: 93 | application/json: 94 | schema: 95 | type: string 96 | -------------------------------------------------------------------------------- /src/aiopenapi3/v30/servers.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from pydantic import Field, model_validator 4 | 5 | from ..base import ObjectExtended 6 | 7 | 8 | class ServerVariable(ObjectExtended): 9 | """ 10 | A ServerVariable object as defined `here`_. 11 | 12 | .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-variable-object 13 | """ 14 | 15 | enum: list[str] | None = Field(default=None) 16 | default: str | None = Field(...) 17 | description: str | None = Field(default=None) 18 | 19 | @model_validator(mode="after") 20 | def validate_ServerVariable(cls, s: "ServerVariable"): 21 | assert isinstance(s.enum, (list, None.__class__)) 22 | # default value must be in enum 23 | assert s.default is None or s.default in (s.enum or [s.default]) 24 | return s 25 | 26 | 27 | class Server(ObjectExtended): 28 | """ 29 | The Server object, as described `here`_ 30 | 31 | .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-object 32 | """ 33 | 34 | url: str = Field(...) 35 | description: str | None = Field(default=None) 36 | variables: dict[str, ServerVariable] = Field(default_factory=dict) 37 | 38 | @model_validator(mode="after") 39 | def validate_server_url_parameters(self) -> "Server": 40 | if (p := frozenset(re.findall(r"\{([^\}]+)\}", self.url))) != (r := frozenset(self.variables.keys())): 41 | raise ValueError(f"Missing Server Variables {sorted(p-r)} in {self.url}") 42 | return self 43 | 44 | def validate_parameter_enum(self, parameters: dict[str, str]): 45 | for name, value in parameters.items(): 46 | if v := self.variables.get(name): 47 | if v.enum and value not in v.enum: 48 | raise ValueError(f"Server Variable {name} value {value} not allowed ({v.enum})") 49 | 50 | def createUrl(self, variables: dict[str, str]) -> str: 51 | self.validate_parameter_enum(variables) 52 | vars: dict[str, str | None] = dict(map(lambda x: (x[0], x[1].default), self.variables.items())) 53 | vars.update(variables) 54 | url: str = self.url.format(**vars) 55 | return url 56 | -------------------------------------------------------------------------------- /src/aiopenapi3/v31/servers.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from pydantic import Field, model_validator 4 | 5 | from ..base import ObjectExtended 6 | 7 | 8 | class ServerVariable(ObjectExtended): 9 | """ 10 | A ServerVariable object as defined `here`_. 11 | 12 | .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#server-variable-object 13 | """ 14 | 15 | enum: list[str] | None = Field(default=None) 16 | default: str | None = Field(...) 17 | description: str | None = Field(default=None) 18 | 19 | @model_validator(mode="after") 20 | def validate_ServerVariable(cls, s: "ServerVariable"): 21 | assert isinstance(s.enum, (list, None.__class__)) 22 | # default value must be in enum 23 | assert s.default is None or s.default in (s.enum or [s.default]) 24 | return s 25 | 26 | 27 | class Server(ObjectExtended): 28 | """ 29 | The Server object, as described `here`_ 30 | 31 | .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#server-object 32 | """ 33 | 34 | url: str = Field(...) 35 | description: str | None = Field(default=None) 36 | variables: dict[str, ServerVariable] = Field(default_factory=dict) 37 | 38 | @model_validator(mode="after") 39 | def validate_server_url_parameters(self) -> "Server": 40 | if (p := frozenset(re.findall(r"\{([^\}]+)\}", self.url))) != (r := frozenset(self.variables.keys())): 41 | raise ValueError(f"Missing Server Variables {sorted(p-r)} in {self.url}") 42 | return self 43 | 44 | def validate_parameter_enum(self, parameters: dict[str, str]): 45 | for name, value in parameters.items(): 46 | if v := self.variables.get(name): 47 | if v.enum and value not in v.enum: 48 | raise ValueError(f"Server Variable {name} value {value} not allowed ({v.enum})") 49 | 50 | def createUrl(self, variables: dict[str, str]) -> str: 51 | self.validate_parameter_enum(variables) 52 | vars: dict[str, str | None] = dict(map(lambda x: (x[0], x[1].default), self.variables.items())) 53 | vars.update(variables) 54 | url: str = self.url.format(**vars) 55 | return url 56 | -------------------------------------------------------------------------------- /tests/fixtures/paths-callbacks.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Callback Example 4 | version: 1.0.0 5 | paths: 6 | /streams: 7 | post: 8 | description: subscribes a client to receive out-of-band data 9 | parameters: 10 | - name: callbackUrl 11 | in: query 12 | required: true 13 | description: | 14 | the location where data will be sent. Must be network accessible 15 | by the source server 16 | schema: 17 | type: string 18 | format: uri 19 | example: https://tonys-server.com 20 | responses: 21 | '201': 22 | description: subscription successfully created 23 | content: 24 | application/json: 25 | schema: 26 | description: subscription information 27 | required: 28 | - subscriptionId 29 | properties: 30 | subscriptionId: 31 | description: this unique identifier allows management of the subscription 32 | type: string 33 | example: 2531329f-fb09-4ef7-887e-84e648214436 34 | callbacks: 35 | # the name `onData` is a convenience locator 36 | onData: 37 | # when data is sent, it will be sent to the `callbackUrl` provided 38 | # when making the subscription PLUS the suffix `/data` 39 | '{$request.query.callbackUrl}/data': 40 | post: 41 | requestBody: 42 | description: subscription payload 43 | content: 44 | application/json: 45 | schema: 46 | type: object 47 | properties: 48 | timestamp: 49 | type: string 50 | format: date-time 51 | userData: 52 | type: string 53 | responses: 54 | '202': 55 | description: | 56 | Your server implementation should return this HTTP status code 57 | if the data was received successfully 58 | '204': 59 | description: | 60 | Your server should return this HTTP status code if no longer interested 61 | in further updates 62 | -------------------------------------------------------------------------------- /tests/fixtures/schema-allof-oneof-combined.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # taken from https://github.com/OAI/OpenAPI-Specification/issues/2141 3 | # 4 | 5 | openapi: "3.0.0" 6 | info: 7 | version: 1.0.0 8 | title: OpenAPI JSON Schema applicator combinations 9 | 10 | # Top-level schema included so you can play with this in a standard JSON Schema validator, 11 | # like http://jsonschmalint.com 12 | #$ref: "#/components/schemas/Object" 13 | 14 | components: 15 | schemas: 16 | Response: 17 | type: object 18 | additionalProperties: false 19 | properties: 20 | retCode: 21 | type: integer 22 | data: 23 | type: object 24 | additionalProperties: false 25 | required: 26 | - retCode 27 | - data 28 | 29 | AllOfResponse: 30 | type: object 31 | additionalProperties: false 32 | allOf: 33 | - $ref: "#/components/schemas/Response" 34 | oneOf: 35 | - type: object 36 | additionalProperties: false 37 | properties: 38 | data: 39 | type: object 40 | additionalProperties: false 41 | properties: 42 | string: 43 | type: string 44 | required: 45 | - string 46 | 47 | AuthenticatedRequest: 48 | type: object 49 | additionalProperties: false 50 | properties: 51 | token: 52 | type: string 53 | cmd: 54 | type: string 55 | required: 56 | - token 57 | - command 58 | 59 | ShutdownRequest: 60 | type: object 61 | additionalProperties: false 62 | allOf: 63 | - $ref: "#/components/schemas/AuthenticatedRequest" 64 | properties: 65 | cmd: 66 | type: string 67 | enum: 68 | - "shutdown" 69 | default: shutdown 70 | data: 71 | type: object 72 | additionalProperties: false 73 | properties: 74 | delay: 75 | type: integer 76 | required: 77 | - delay 78 | required: 79 | - data 80 | - cmd 81 | 82 | Request: 83 | type: object 84 | additionalProperties: false 85 | discriminator: 86 | propertyName: cmd 87 | mapping: 88 | shutdown: "#/components/schemas/ShutdownRequest" 89 | oneOf: 90 | - $ref: "#/components/schemas/ShutdownRequest" 91 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/hadialqattan/pycln 3 | rev: v2.6.0 # Possible releases: https://github.com/hadialqattan/pycln/releases 4 | hooks: 5 | - id: pycln 6 | - repo: 'https://github.com/psf/black' 7 | rev: 25.11.0 8 | hooks: 9 | - id: black 10 | args: 11 | - "--line-length=120" 12 | - repo: 'https://github.com/pre-commit/pre-commit-hooks' 13 | rev: v6.0.0 14 | hooks: 15 | - id: end-of-file-fixer 16 | exclude: '^docs/[^/]*\.svg$' 17 | - id: requirements-txt-fixer 18 | - id: trailing-whitespace 19 | - id: file-contents-sorter 20 | files: | 21 | .gitignore 22 | - id: check-case-conflict 23 | - id: check-xml 24 | - id: check-executables-have-shebangs 25 | - id: debug-statements 26 | - id: check-added-large-files 27 | - id: check-symlinks 28 | - id: debug-statements 29 | - repo: 'https://github.com/PyCQA/flake8' 30 | rev: 7.3.0 31 | hooks: 32 | - id: flake8 33 | args: 34 | - "--max-line-length=120" 35 | - "--ignore=E203,W503" 36 | - "--select=W504" 37 | - repo: https://github.com/asottile/pyupgrade 38 | rev: v3.21.2 39 | hooks: 40 | - id: pyupgrade 41 | args: [--py39-plus, --keep-runtime-typing] 42 | 43 | - repo: https://github.com/adrienverge/yamllint.git 44 | rev: v1.37.1 45 | hooks: 46 | - id: yamllint 47 | args: ["-d", "{extends: relaxed, rules: {empty-lines: disable, line-length: {max: 1500}}}", --strict, --format, parsable] 48 | 49 | - repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt 50 | rev: 0.2.3 51 | hooks: 52 | - id: yamlfmt 53 | args: [--mapping, '2', --sequence, '4', --offset, '2', --preserve-quotes, --implicit_start, --width, '1500'] 54 | exclude: tests/fixtures/schema-enum.yaml 55 | 56 | - repo: https://github.com/astral-sh/uv-pre-commit 57 | # uv version. 58 | rev: 0.9.13 59 | hooks: 60 | # Update the uv lockfile 61 | - id: uv-lock 62 | - id: uv-export 63 | args: ["--no-dev", "--no-hashes", "--no-editable", "-o", "requirements.txt"] 64 | 65 | ci: 66 | autofix_commit_msg: | 67 | [pre-commit.ci] auto fixes from pre-commit.ci hooks 68 | autofix_prs: true 69 | autoupdate_branch: '' 70 | autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' 71 | autoupdate_schedule: weekly 72 | skip: ["uv-lock", "uv-export"] 73 | submodules: false 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aiopenapi3 2 | 3 | A Python [OpenAPI 3 Specification](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md) client and validator for Python 3. 4 | 5 | [![Test](https://github.com/commonism/aiopenapi3/actions/workflows/codecov.yml/badge.svg?event=push&branch=master)](https://github.com/commonism/aiopenapi3/actions?query=workflow%3ACodecov+event%3Apush+branch%3Amaster) 6 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/commonism/aiopenapi3/master.svg)](https://results.pre-commit.ci/latest/github/commonism/aiopenapi3/master) 7 | [![Coverage](https://img.shields.io/codecov/c/github/commonism/aiopenapi3)](https://codecov.io/gh/commonism/aiopenapi3) 8 | [![Supported Python versions](https://img.shields.io/pypi/pyversions/aiopenapi3.svg)](https://pypi.org/project/aiopenapi3) 9 | [![Documentation Status](https://readthedocs.org/projects/aiopenapi3/badge/?version=latest)](https://aiopenapi3.readthedocs.io/en/latest/?badge=latest) 10 | 11 | 12 | This project is a fork of [Dorthu/openapi3](https://github.com/Dorthu/openapi3/). 13 | 14 | ## Features 15 | * implements … 16 | * Swagger 2.0 17 | * OpenAPI 3.0.3 18 | * OpenAPI 3.1.0 19 | * description document parsing via [pydantic](https://github.com/samuelcolvin/pydantic) 20 | * recursive schemas (A.a -> A) 21 | * request body model creation via pydantic 22 | * pydantic compatible "format"-type coercion (e.g. datetime.interval) 23 | * additionalProperties (limited to string-to-any dictionaries without properties) 24 | * response body & header parsing via pydantic 25 | * blocking and nonblocking (asyncio) interface via [httpx](https://www.python-httpx.org/) 26 | * SOCKS5 via httpx_socks 27 | * tests with pytest & [fastapi](https://fastapi.tiangolo.com/) 28 | * providing access to methods and arguments via the sad smiley ._. interface 29 | * Plugin Interface/api to modify description documents/requests/responses to adapt to non compliant services 30 | * YAML type coercion hints for not well formatted description documents 31 | * Description Document dependency downloads (using the WebLoader) 32 | * logging 33 | * `export AIOPENAPI3_LOGGING_HANDLERS=debug` to get /tmp/aiopenapi3-debug.log 34 | 35 | 36 | ## Documentation 37 | [API Documentation](https://aiopenapi3.readthedocs.io/en/latest/) 38 | 39 | 40 | ## Running Tests 41 | 42 | This project includes a test suite, run via ``pytest``. To run the test suite, 43 | ensure that you've installed the dependencies and then run ``pytest`` in the root 44 | of this project. 45 | 46 | ```shell 47 | PYTHONPATH=. pytest --cov=./ --cov-report=xml . 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. aiopenapi3 documentation master file, created by 2 | sphinx-quickstart on Sun Dec 25 15:28:14 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. include:: links.rst 7 | 8 | ############ 9 | |aiopenapi3| 10 | ############ 11 | 12 | *If you can't make it perfect, make it adjustable.* 13 | 14 | |aiopenapi3| is a client library to interface RESTful services using OpenAPI_/Swagger description documents, 15 | built upon pydantic_ for data validation/coercion and httpx_ for transport. 16 | Located on `github `_. 17 | 18 | 19 | .. rubric:: No code generation 20 | 21 | Suits the code-first pattern for REST services used by major frameworks 22 | (`FastAPI `_, `Django REST framework `_) as well as 23 | design first APIs. 24 | 25 | .. rubric:: Features & Limitations 26 | 27 | While aiopenapi3 supports some of the more exotic features of the Swagger/OpenAPI specification, e.g.: 28 | 29 | * multilingual 30 | 31 | * Swagger 2.0 32 | * OpenAPI 3.0 33 | * OpenAPI 3.1 34 | 35 | * multi file description documents 36 | * recursive schemas 37 | * additionalProperties mixed with properties 38 | * `additionalProperties `_ 39 | * `Discriminator/Polymorphism `_ 40 | * :doc:`plugin interface ` to mangle description documents and messages 41 | * :ref:`api:Parameter Encoding` 42 | * :ref:`advanced:Forms` 43 | * :ref:`advanced:mutualTLS` authentication 44 | * :ref:`Request ` and :ref:`Response ` streaming to reduce memory usage 45 | * Culling :ref:`extra:Large Description Documents` 46 | 47 | some aspects of the specifications are implemented loose 48 | 49 | * `Schema Composition `_ 50 | 51 | * oneOf - validation does not care if more than one matches 52 | * anyOf - implemented as oneOf 53 | * allOf - merging Schemas is limited wrt. to merge conflicts 54 | 55 | and other aspects of the specification are not implemented at all 56 | 57 | * `Conditional Subschemas `_ 58 | 59 | * dependentRequired 60 | * dependentSchemas 61 | * If-Then-Else 62 | * Implication 63 | 64 | * non-unique parameter names in an operations headers/path/query 65 | -------------------------------------------------------------------------------- /tests/cli_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shlex 3 | from pathlib import Path 4 | import json 5 | 6 | import pytest 7 | 8 | from aiopenapi3.cli import main 9 | import aiopenapi3.log 10 | 11 | import pydantic 12 | 13 | 14 | def test_validate_cli(): 15 | main(shlex.split("-v validate tests/fixtures/schema-yaml12-tags.yaml")) 16 | 17 | 18 | def test_convert_cli(): 19 | main(shlex.split("-v convert --format json tests/fixtures/schema-yaml12-tags.yaml /dev/null")) 20 | 21 | 22 | def test_profile(): 23 | main(shlex.split("--profile validate tests/fixtures/petstore-expanded.yaml")) 24 | 25 | 26 | def test_plugins(): 27 | main( 28 | shlex.split( 29 | "-L tests/fixtures -P tests/plugin_test.py:OnInit,OnDocument,OnMessage --tracemalloc -v validate plugin-base.yaml" 30 | ) 31 | ) 32 | 33 | 34 | def test_tracemalloc(): 35 | main(shlex.split("--tracemalloc validate tests/fixtures/petstore-expanded.yaml")) 36 | 37 | 38 | def test_logging(): 39 | os.environ["AIOPENAPI3_LOGGING_HANDLERS"] = "console" 40 | aiopenapi3.log.handlers = None 41 | main(shlex.split("validate tests/fixtures/petstore-expanded.yaml")) 42 | 43 | aiopenapi3.log.handlers = None 44 | aiopenapi3.log.init(True) 45 | 46 | 47 | def test_call(): 48 | cache = Path("tests/data/cache.pickle") 49 | if cache.exists(): 50 | cache.unlink() 51 | 52 | main( 53 | shlex.split( 54 | """-C tests/data/cache.pickle -P tests/petstore_test.py:OnDocument call https://petstore.swagger.io/v2/swagger.json --method post /user --authenticate '{"api_key":"special-key"}' --data '{"id":1, "username": "bozo", "firstName": "Bozo", "lastName": "Smith", "email": "bozo@email.com", "password": "letmemin", "phone": "111-222-333", "userStatus": 3 }' """ 55 | ) 56 | ) 57 | 58 | auth = Path("tests/data/auth.json") 59 | auth.write_text(json.dumps({"petstore_auth": "test"})) 60 | 61 | main( 62 | shlex.split( 63 | """-C tests/data/cache.pickle -P tests/petstore_test.py:OnDocument call https://petstore.swagger.io/v2/swagger.json findPetsByStatus --parameters '{"status": ["available", "pending"]}' --authenticate @tests/data/auth.json --format "[? name=='doggie' && status == 'available'].{name:name, photo:photoUrls} | [0:2]" """ 64 | ) 65 | ) 66 | main( 67 | shlex.split( 68 | """-P tests/petstore_test.py:OnDocument call https://petstore.swagger.io/v2/swagger.json findPetsByStatus --parameters '{"status": ["available", "pending"]}' --authenticate '{"petstore_auth":"test"}' --format "[? name=='doggie' && status == 'available'].{name:name, photo:photoUrls} | [0:2]" """ 69 | ) 70 | ) 71 | auth.unlink() 72 | -------------------------------------------------------------------------------- /tests/loader_test.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from pathlib import Path 4 | 5 | import yarl 6 | import pytest 7 | from aiopenapi3 import OpenAPI, FileSystemLoader, ReferenceResolutionError 8 | from aiopenapi3.loader import Loader, Plugins, NullLoader 9 | 10 | SPECTPL = """ 11 | openapi: "3.0.0" 12 | info: 13 | title: spec01 14 | version: 1.0.0 15 | description: | 16 | {description} 17 | 18 | paths: 19 | /load: 20 | get: 21 | responses: 22 | '200': 23 | description: unexpected error 24 | content: 25 | application/json: 26 | schema: 27 | $ref: {jsonref} 28 | 29 | components: 30 | schemas: 31 | Example: 32 | type: string 33 | Object: 34 | type: object 35 | properties: 36 | name: 37 | type: string 38 | value: 39 | type: boolean 40 | """ 41 | 42 | data = [ 43 | ("petstore-expanded.yaml#/components/schemas/Pet", None), 44 | ("no-such.file.yaml#/components/schemas/Pet", ReferenceResolutionError), 45 | ("petstore-expanded.yaml#/components/schemas/NoSuchPet", ReferenceResolutionError), 46 | ] 47 | 48 | 49 | @pytest.mark.parametrize("jsonref, exception", data) 50 | def test_loader_jsonref(jsonref, exception): 51 | loader = FileSystemLoader(Path("tests/fixtures")) 52 | values = {"jsonref": jsonref, "description": ""} 53 | if exception is None: 54 | api = OpenAPI.loads("loader.yaml", SPECTPL.format(**values), loader=loader) 55 | else: 56 | with pytest.raises(exception): 57 | api = OpenAPI.loads("loader.yaml", SPECTPL.format(**values), loader=loader) 58 | 59 | 60 | def test_loader_decode(): 61 | with pytest.raises(ValueError, match="encoding"): 62 | Loader.decode(b"rvice.\r\n \xa9 2020, 3GPP Organ", codec="utf-8") 63 | 64 | 65 | def test_loader_format(): 66 | values = {"jsonref": "'#/components/schemas/Example'", "description": ""} 67 | spec = SPECTPL.format(**values) 68 | api = OpenAPI.loads("loader.yaml", spec) 69 | loader = NullLoader() 70 | spec = loader.parse(Plugins([]), yarl.URL("loader.yaml"), spec) 71 | spec = json.dumps(spec) 72 | api = OpenAPI.loads("loader.json", spec) 73 | 74 | 75 | @pytest.mark.skip_env("GITHUB_ACTIONS") 76 | def test_webload(): 77 | # FIXME https://github.com/pydantic/pydantic/issues/5730 78 | pytest.skip() 79 | name = "https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/network/resource-manager/Microsoft.Network/stable/2018-10-01/serviceEndpointPolicy.json" 80 | from aiopenapi3.loader import WebLoader 81 | import yarl 82 | 83 | loader = WebLoader(yarl.URL(name)) 84 | api = OpenAPI.load_sync(name, loader=loader) 85 | -------------------------------------------------------------------------------- /tests/fixtures/schema-discriminated-union-deep.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: enum test 5 | 6 | paths: {} 7 | servers: [] 8 | 9 | components: 10 | schemas: 11 | BlackCat: 12 | title: BlackCat 13 | type: object 14 | required: 15 | - name 16 | - black_name 17 | properties: &catproperties 18 | color: 19 | title: Color 20 | const: black 21 | default: black 22 | black_name: 23 | title: Black Name 24 | type: string 25 | identifier: 26 | title: Identifier 27 | type: [string, 'null'] 28 | name: 29 | title: Name 30 | type: string 31 | pet_type: 32 | title: Pet Type 33 | type: string 34 | default: cat 35 | const: cat 36 | tags: 37 | title: Tags 38 | type: array 39 | items: 40 | type: string 41 | WhiteCat: 42 | title: WhiteCat 43 | type: object 44 | required: 45 | - name 46 | - white_name 47 | properties: 48 | <<: *catproperties 49 | color: 50 | title: Color 51 | const: white 52 | default: white 53 | white_name: 54 | title: White Name 55 | type: string 56 | 57 | Cat: 58 | type: object 59 | title: Cat 60 | discriminator: 61 | mapping: 62 | black: '#/components/schemas/BlackCat' 63 | white: '#/components/schemas/WhiteCat' 64 | propertyName: color 65 | oneOf: 66 | - $ref: '#/components/schemas/BlackCat' 67 | - $ref: '#/components/schemas/WhiteCat' 68 | 69 | Dog: 70 | properties: 71 | age: 72 | format: duration 73 | title: Age 74 | type: string 75 | identifier: 76 | type: [string, 'null'] 77 | title: Identifier 78 | name: 79 | title: Name 80 | type: string 81 | pet_type: 82 | const: dog 83 | default: dog 84 | title: Pet Type 85 | tags: 86 | items: 87 | type: string 88 | title: Tags 89 | type: array 90 | required: 91 | - name 92 | - age 93 | title: Dog 94 | type: object 95 | Pet: 96 | discriminator: 97 | mapping: 98 | cat: '#/components/schemas/Cat' 99 | dog: '#/components/schemas/Dog' 100 | propertyName: pet_type 101 | oneOf: 102 | - $ref: '#/components/schemas/Cat' 103 | - $ref: '#/components/schemas/Dog' 104 | title: Pet 105 | -------------------------------------------------------------------------------- /tests/formdata_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import httpx 3 | 4 | from aiopenapi3 import OpenAPI 5 | from aiopenapi3.v30.formdata import encode_multipart_parameters 6 | 7 | 8 | def test_encode_formdata(): 9 | with Path("/dev/urandom").open("rb") as f: 10 | data = f.read(512) 11 | 12 | from aiopenapi3.v30 import Schema 13 | 14 | schema = Schema() 15 | ITEMS = [ 16 | ("text", "text/plain", "bar", dict(), schema), 17 | ("text", "text/plain", "bar", {"X-HEAD": "text"}, schema), 18 | ("audio", "audio/wav", b"jd", dict(), schema), 19 | ("image", "image/png", b"jd", dict(), schema), 20 | ("data", "application/octet-stream", data, dict(), schema), 21 | ("rbh", "application/octet-stream", data, {"X-HEAD": "rbh"}, schema), 22 | ] 23 | 24 | for i in ITEMS: 25 | m = encode_multipart_parameters([i]) 26 | ct = m["Content-Type"] 27 | data = m.as_string() 28 | assert data 29 | 30 | m = encode_multipart_parameters(ITEMS) 31 | data = m.as_string() 32 | assert data 33 | 34 | 35 | def test_formdata_encoding(httpx_mock, with_paths_requestbody_formdata_encoding): 36 | api = OpenAPI("http://localhost/api", with_paths_requestbody_formdata_encoding, session_factory=httpx.Client) 37 | 38 | httpx_mock.add_response( 39 | headers={"Content-Type": "application/json"}, 40 | json="ok", 41 | ) 42 | 43 | cls = api._.encoding.operation.requestBody.content["multipart/form-data"].schema_.get_type() 44 | data = cls( 45 | id="3b26b56a-b58a-4855-b26d-0c1ca5c4d071", 46 | address={"state": "ny"}, 47 | # historyMetadata={"a": "a"}, 48 | profileImage=b"\x00\01\0x2", 49 | ) 50 | result = api._.encoding(data=data) 51 | request = httpx_mock.get_request() 52 | 53 | import email 54 | 55 | content = request.content.decode() 56 | 57 | msg = email.message_from_string( 58 | f"""\ 59 | MIME-Version: 1.0 60 | Content-Type: {request.headers["content-type"]} 61 | 62 | {content} 63 | 64 | """ 65 | ) 66 | assert msg.defects == [] and msg.is_multipart() 67 | 68 | r = dict() 69 | for p in msg.get_payload(): 70 | name = p.get_param("name", header="content-disposition") 71 | payload = p.get_payload(decode=True) 72 | r[name] = payload 73 | 74 | assert r["address"] == b"state,ny" 75 | assert r["id"] == str(data.id).encode() 76 | assert r["profileImage"] == data.profileImage.encode() 77 | assert result == "ok" 78 | 79 | 80 | def _test_speed(): 81 | import timeit 82 | 83 | value = "text/plain; a=b" 84 | a = timeit.timeit( 85 | f"decode_content_type('{value}')", 86 | number=1000000, 87 | setup="from aiopenapi3.v30.formdata import decode_content_type", 88 | ) 89 | print(f"{a}") 90 | -------------------------------------------------------------------------------- /src/aiopenapi3/_types.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import TYPE_CHECKING, Union, TypeAlias, Optional, Literal 3 | from collections.abc import Sequence 4 | 5 | import yaml 6 | 7 | from httpx._types import RequestContent, FileTypes, RequestFiles, AuthTypes # noqa 8 | from pydantic import BaseModel 9 | 10 | 11 | from . import v20, v30, v31 12 | 13 | if TYPE_CHECKING: 14 | pass 15 | 16 | 17 | RequestFileParameter = tuple[str, FileTypes] 18 | RequestFilesParameter = Sequence[RequestFileParameter] 19 | 20 | JSON: TypeAlias = Optional[Union[dict[str, "JSON"], list["JSON"], str, int, float, bool]] 21 | """ 22 | Define a JSON type 23 | https://github.com/python/typing/issues/182#issuecomment-1320974824 24 | """ 25 | 26 | RequestData = Union[JSON, BaseModel, RequestFilesParameter] 27 | RequestParameter = Union[str, BaseModel] 28 | RequestParameters = dict[str, RequestParameter] 29 | 30 | RootType = Union[v20.Root, v30.Root, v31.Root] 31 | ServerType = Union[v30.Server, v31.Server] 32 | ReferenceType = Union[v20.Reference, v30.Reference, v31.Reference] 33 | SchemaType = Union[v20.Schema, v30.Schema, v31.Schema] 34 | v3xSchemaType = Union[v30.Schema, v31.Schema] 35 | DiscriminatorType = Union[v30.Discriminator, v31.Discriminator] 36 | PathItemType = Union[v20.PathItem, v30.PathItem, v31.PathItem] 37 | OperationType = Union[v20.Operation, v30.Operation, v31.Operation] 38 | ParameterType = Union[v20.Parameter, v30.Parameter, v31.Parameter] 39 | HeaderType = Union[v20.Header, v30.Header, v31.Header] 40 | RequestType = Union[v20.Request, v30.Request] 41 | MediaTypeType = Union[v30.MediaType, v31.MediaType] 42 | ExpectedType = Union[v20.Response, MediaTypeType] 43 | ResponseHeadersType = dict[str, Union[str, BaseModel, list[BaseModel]]] 44 | ResponseDataType = Union[BaseModel, bytes, str] 45 | 46 | 47 | YAMLLoaderType = Union[type[yaml.Loader], type[yaml.CLoader], type[yaml.SafeLoader], type[yaml.CSafeLoader]] 48 | 49 | PrimitiveTypes = Union[str, float, int, bool] 50 | 51 | HTTPMethodType = Literal["get", "put", "post", "delete", "options", "head", "patch", "trace"] 52 | HTTPMethodMatchType = Union[re.Pattern, HTTPMethodType] 53 | 54 | __all__: list[str] = [ 55 | "RootType", 56 | "ServerType", 57 | "SchemaType", 58 | "v3xSchemaType", 59 | "DiscriminatorType", 60 | "PathItemType", 61 | "OperationType", 62 | "ParameterType", 63 | "HeaderType", 64 | "RequestType", 65 | "ExpectedType", 66 | "MediaTypeType", 67 | "ResponseHeadersType", 68 | "ResponseDataType", 69 | "RequestData", 70 | "RequestParameters", 71 | "ReferenceType", 72 | "PrimitiveTypes", 73 | # 74 | "YAMLLoaderType", 75 | # httpx forwards 76 | "RequestContent", 77 | "RequestFiles", 78 | "AuthTypes", 79 | # 80 | "JSON", 81 | "RequestFilesParameter", 82 | "RequestFileParameter", 83 | "HTTPMethodType", 84 | "HTTPMethodMatchType", 85 | ] 86 | -------------------------------------------------------------------------------- /src/aiopenapi3/log.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging.config 3 | import os 4 | from typing import Any 5 | 6 | from pathlib import Path 7 | 8 | handlers: list[str] | None = None 9 | 10 | 11 | def init(force: bool = False) -> None: 12 | global handlers 13 | 14 | if handlers is not None: 15 | return 16 | 17 | handlers = [] 18 | 19 | if os.environ.get("AIOPENAPI3_LOGGING_HANDLERS", None) is None: 20 | return 21 | 22 | if force: 23 | if sys.stdin.isatty() and sys.stdout.isatty(): 24 | handlers.append("console") 25 | else: 26 | if Path("/dev/log").resolve().is_socket(): 27 | handlers.append("syslog") 28 | else: 29 | print("panic now!") 30 | 31 | """export AIOPENAPI3_LOGGING_HANDLERS=debug to get /tmp/aiopenapi3-debug.log""" 32 | handlers.extend(filter(lambda x: len(x), os.environ.get("AIOPENAPI3_LOGGING_HANDLERS", "").split(","))) 33 | 34 | config: dict[str, Any] = { 35 | "version": 1, 36 | "formatters": { 37 | "notimestamp": { 38 | "class": "logging.Formatter", 39 | "format": "%(name)-9s %(levelname)-4s %(message)s", 40 | }, 41 | "detailed": { 42 | "class": "logging.Formatter", 43 | "format": "%(asctime)s %(name)-9s %(levelname)-4s %(message)s", 44 | }, 45 | "plain": { 46 | "class": "logging.Formatter", 47 | "format": "%(message)s", 48 | }, 49 | }, 50 | "handlers": { 51 | "console": { 52 | "class": "logging.StreamHandler", 53 | "level": "DEBUG", 54 | "formatter": "plain", 55 | }, 56 | "syslog": { 57 | "class": "logging.handlers.SysLogHandler", 58 | "level": "DEBUG", 59 | "formatter": "notimestamp", 60 | "address": "/dev/log", 61 | "facility": "user", 62 | }, 63 | "debug": { 64 | "class": "logging.handlers.WatchedFileHandler", 65 | "level": "DEBUG", 66 | "formatter": "detailed", 67 | "filename": "/tmp/aiopenapi3-debug.log", 68 | }, 69 | }, 70 | # "root": {"level": "DEBUG", "handlers": handlers}, 71 | "loggers": { 72 | "aiopenapi3": {"level": "DEBUG", "handlers": handlers}, 73 | "httpx": {"level": "DEBUG", "propagate": False, "handlers": handlers}, 74 | }, 75 | } 76 | 77 | # remove unused 78 | for i in frozenset(config["handlers"].keys()) - frozenset(handlers): 79 | del config["handlers"][i] 80 | 81 | for i in frozenset(config["formatters"]) - frozenset(map(lambda x: x["formatter"], config["handlers"].values())): 82 | del config["formatters"][i] 83 | 84 | logging.config.dictConfig(config) 85 | -------------------------------------------------------------------------------- /src/aiopenapi3/v20/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Any, Optional 2 | 3 | from pydantic import Field, PrivateAttr, model_validator 4 | 5 | from .general import Reference 6 | from .xml import XML 7 | from ..base import ObjectExtended, SchemaBase 8 | 9 | 10 | class Schema(ObjectExtended, SchemaBase): 11 | """ 12 | The Schema Object allows the definition of input and output data types. 13 | 14 | https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md##schema-object 15 | """ 16 | 17 | ref: str | None = Field(default=None, alias="$ref") 18 | format: str | None = Field(default=None) 19 | title: str | None = Field(default=None) 20 | description: str | None = Field(default=None) 21 | default: Any | None = Field(default=None) 22 | 23 | multipleOf: int | None = Field(default=None) 24 | maximum: float | None = Field(default=None) # FIXME Field(discriminator='type') would be better 25 | exclusiveMaximum: bool | None = Field(default=None) 26 | minimum: float | None = Field(default=None) 27 | exclusiveMinimum: bool | None = Field(default=None) 28 | maxLength: int | None = Field(default=None) 29 | minLength: int | None = Field(default=None) 30 | pattern: str | None = Field(default=None) 31 | maxItems: int | None = Field(default=None) 32 | minItems: int | None = Field(default=None) 33 | uniqueItems: bool | None = Field(default=None) 34 | maxProperties: int | None = Field(default=None) 35 | minProperties: int | None = Field(default=None) 36 | required: list[str] = Field(default_factory=list) 37 | enum: list[Any] | None = Field(default=None) 38 | type: str | None = Field(default=None) 39 | 40 | items: list[Union["Schema", Reference]] | Union["Schema", Reference] | None = Field(default=None) 41 | allOf: list[Union["Schema", Reference]] = Field(default_factory=list) 42 | properties: dict[str, Union["Schema", Reference]] = Field(default_factory=dict) 43 | additionalProperties: Union["Schema", Reference, bool] | None = Field(default=None) 44 | 45 | discriminator: str | None = Field(default=None) # 'Discriminator' 46 | readOnly: bool | None = Field(default=None) 47 | xml: XML | None = Field(default=None) # 'XML' 48 | externalDocs: dict | None = Field(default=None) # 'ExternalDocs' 49 | example: Any | None = Field(default=None) 50 | 51 | @model_validator(mode="wrap") 52 | @classmethod 53 | def is_boolean_schema(cls, data: Any, handler: "ValidatorFunctionWrapHandler", info: "ValidationInfo") -> Any: 54 | if not isinstance(data, bool): 55 | return handler(data) 56 | if data: 57 | return handler(cls.model_validate({})) 58 | else: 59 | return handler(_Not.model_validate({"not": {}})) 60 | 61 | def __getstate__(self): 62 | return SchemaBase.__getstate__(self) 63 | 64 | 65 | class _Not(Schema): 66 | not_: Optional["Schema"] = Field(alias="not") 67 | -------------------------------------------------------------------------------- /src/aiopenapi3/v30/security.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Annotated, Literal 2 | from pydantic import Field, RootModel, constr 3 | 4 | from ..base import ObjectExtended 5 | 6 | 7 | class OAuthFlow(ObjectExtended): 8 | """ 9 | Configuration details for a supported OAuth Flow 10 | 11 | .. here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flow-object 12 | """ 13 | 14 | authorizationUrl: str | None = Field(default=None) 15 | tokenUrl: str | None = Field(default=None) 16 | refreshUrl: str | None = Field(default=None) 17 | scopes: dict[str, str] = Field(default_factory=dict) 18 | 19 | 20 | class OAuthFlows(ObjectExtended): 21 | """ 22 | Allows configuration of the supported OAuth Flows. 23 | 24 | .. here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flows-object 25 | """ 26 | 27 | implicit: OAuthFlow | None = Field(default=None) 28 | password: OAuthFlow | None = Field(default=None) 29 | clientCredentials: OAuthFlow | None = Field(default=None) 30 | authorizationCode: OAuthFlow | None = Field(default=None) 31 | 32 | 33 | class _SecuritySchemes: 34 | class _SecurityScheme(ObjectExtended): 35 | type: Literal["apiKey", "http", "oauth2", "openIdConnect"] 36 | description: str | None = Field(default=None) 37 | 38 | def validate_authentication_value(self, value): 39 | pass 40 | 41 | class apiKey(_SecurityScheme): 42 | type: Literal["apiKey"] 43 | in_: str = Field(alias="in") 44 | name: str 45 | 46 | class http(_SecurityScheme): 47 | type: Literal["http"] 48 | scheme_: constr(to_lower=True) = Field(default=None, alias="scheme") # type: ignore[valid-type] 49 | bearerFormat: str | None = Field(default=None) 50 | 51 | class oauth2(_SecurityScheme): 52 | type: Literal["oauth2"] 53 | flows: OAuthFlows 54 | 55 | class openIdConnect(_SecurityScheme): 56 | type: Literal["openIdConnect"] 57 | openIdConnectUrl: str 58 | 59 | 60 | class SecurityScheme( 61 | RootModel[ 62 | Annotated[ 63 | Union[ 64 | _SecuritySchemes.apiKey, _SecuritySchemes.http, _SecuritySchemes.oauth2, _SecuritySchemes.openIdConnect 65 | ], 66 | Field(discriminator="type"), 67 | ] 68 | ] 69 | ): 70 | """ 71 | A `Security Scheme`_ defines a security scheme that can be used by the operations. 72 | 73 | .. _Security Scheme: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object 74 | """ 75 | 76 | pass 77 | 78 | 79 | class SecurityRequirement(RootModel[dict[str, list[str]]]): 80 | """ 81 | A `SecurityRequirement`_ object describes security schemes for API access. 82 | 83 | .. _SecurityRequirement: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-requirement-object 84 | """ 85 | 86 | pass 87 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: ["master"] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ["master"] 20 | schedule: 21 | - cron: '26 23 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ['python'] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v3 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | -------------------------------------------------------------------------------- /tests/api/v1/main.py: -------------------------------------------------------------------------------- 1 | import errno 2 | 3 | import starlette.status 4 | from fastapi import FastAPI, APIRouter, Body, Response, Path 5 | from fastapi.responses import JSONResponse 6 | 7 | from .schema import Pets, Pet, PetCreate, Error 8 | 9 | 10 | from fastapi_versioning import versioned_api_route, version 11 | 12 | router = APIRouter(route_class=versioned_api_route(1)) 13 | 14 | 15 | ZOO = dict() 16 | 17 | 18 | def _idx(l): 19 | yield from range(l) 20 | 21 | 22 | idx = _idx(100) 23 | 24 | 25 | @router.post( 26 | "/pet", 27 | operation_id="createPet", 28 | response_model=Pet, 29 | responses={ 30 | 201: { 31 | "model": Pet, 32 | "headers": { 33 | "X-Limit-Remain": { 34 | "schema": {"type": "integer"}, 35 | "required": True, 36 | "description": "Remain Request limit for next 60 minutes", 37 | }, 38 | "X-Limit-Done": {"schema": {"type": "integer"}, "required": False, "description": "Requests done"}, 39 | }, 40 | }, 41 | 409: {"model": Error}, 42 | }, 43 | ) 44 | def createPet( 45 | response: Response, 46 | pet: PetCreate = Body(..., embed=True), 47 | ) -> None: 48 | if pet.name in ZOO: 49 | return JSONResponse( 50 | status_code=starlette.status.HTTP_409_CONFLICT, 51 | content=Error(code=errno.EEXIST, message=f"{pet.name} already exists").model_dump(), 52 | ) 53 | ZOO[pet.name] = r = Pet(id=next(idx), **pet.model_dump()) 54 | response.status_code = starlette.status.HTTP_201_CREATED 55 | response.headers["X-Limit-Remain"] = "5" 56 | # response.headers["model"] = r 57 | return r 58 | 59 | 60 | @router.get("/pet", operation_id="listPet", response_model=Pets) 61 | def listPet(limit: int | None = None) -> Pets: 62 | return list(ZOO.values()) 63 | 64 | 65 | @router.get("/pets/{petId}", operation_id="getPet", response_model=Pet, responses={404: {"model": Error}}) 66 | def getPet(pet_id: int = Path(..., alias="petId")) -> Pets: 67 | for k, v in ZOO.items(): 68 | if pet_id == v.id: 69 | return v 70 | else: 71 | return JSONResponse( 72 | status_code=starlette.status.HTTP_404_NOT_FOUND, 73 | content=Error(code=errno.ENOENT, message=f"{pet_id} not found").model_dump(), 74 | ) 75 | 76 | 77 | @router.delete("/pets/{petId}", operation_id="deletePet", responses={204: {"model": None}, 404: {"model": Error}}) 78 | def deletePet(response: Response, pet_id: int = Path(..., alias="petId")) -> Pets: 79 | for k, v in ZOO.items(): 80 | if pet_id == v.id: 81 | del ZOO[k] 82 | response.status_code = starlette.status.HTTP_204_NO_CONTENT 83 | return response 84 | else: 85 | return JSONResponse( 86 | status_code=starlette.status.HTTP_404_NOT_FOUND, 87 | content=Error(code=errno.ENOENT, message=f"{pet_id} not found").model_dump(), 88 | media_type="application/json; utf-8", 89 | ) 90 | -------------------------------------------------------------------------------- /src/aiopenapi3/v20/paths.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from pydantic import Field, model_validator 4 | 5 | from .general import ExternalDocumentation 6 | from .general import Reference 7 | from .parameter import Header, Parameter 8 | from .schemas import Schema 9 | from .security import SecurityRequirement 10 | from ..base import ObjectExtended, PathsBase, OperationBase, PathItemBase 11 | 12 | 13 | class Response(ObjectExtended): 14 | """ 15 | Describes a single response from an API Operation. 16 | 17 | .. _Response Object: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#response-object 18 | """ 19 | 20 | description: str = Field(...) 21 | schema_: Schema | None = Field(default=None, alias="schema") 22 | headers: dict[str, Header] = Field(default_factory=dict) 23 | examples: dict[str, Any] | None = Field(default=None) 24 | 25 | 26 | class Operation(ObjectExtended, OperationBase): 27 | """ 28 | An Operation object as defined `here`_ 29 | 30 | .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#operation-object 31 | """ 32 | 33 | tags: list[str] | None = Field(default=None) 34 | summary: str | None = Field(default=None) 35 | description: str | None = Field(default=None) 36 | externalDocs: ExternalDocumentation | None = Field(default=None) 37 | operationId: str | None = Field(default=None) 38 | consumes: list[str] = Field(default_factory=list) 39 | produces: list[str] = Field(default_factory=list) 40 | parameters: list[Parameter | Reference] = Field(default_factory=list) 41 | responses: dict[str, Reference | Response] = Field(default_factory=dict) 42 | schemes: list[str] = Field(default_factory=list) 43 | deprecated: bool | None = Field(default=None) 44 | security: list[SecurityRequirement] | None = Field(default=None) 45 | 46 | 47 | class PathItem(ObjectExtended, PathItemBase): 48 | """ 49 | A Path Item, as defined `here`_. 50 | Describes the operations available on a single path. 51 | 52 | .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#path-item-object 53 | """ 54 | 55 | ref: str | None = Field(default=None, alias="$ref") 56 | get: Operation | None = Field(default=None) 57 | put: Operation | None = Field(default=None) 58 | post: Operation | None = Field(default=None) 59 | delete: Operation | None = Field(default=None) 60 | options: Operation | None = Field(default=None) 61 | head: Operation | None = Field(default=None) 62 | patch: Operation | None = Field(default=None) 63 | parameters: list[Parameter | Reference] = Field(default_factory=list) 64 | 65 | 66 | class Paths(PathsBase): 67 | paths: dict[str, PathItem] 68 | 69 | @model_validator(mode="before") 70 | def validate_Paths(cls, values): 71 | assert values is not None 72 | p = {} 73 | e = {} 74 | for k, v in values.items(): 75 | if k[:2] == "x-": 76 | e[k] = v 77 | else: 78 | p[k] = v 79 | return {"paths": p, "extensions": e} 80 | -------------------------------------------------------------------------------- /tests/fixtures/schema-allof-discriminator.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # taken from https://github.com/OAI/OpenAPI-Specification/issues/2141 3 | # 4 | 5 | openapi: "3.0.0" 6 | info: 7 | version: 1.0.0 8 | title: Any OpenAPI validator that properly validates discriminator against data? 9 | 10 | # Top-level schema included so you can play with this in a standard JSON Schema validator, 11 | # like http://jsonschmalint.com 12 | #$ref: "#/components/schemas/Object" 13 | 14 | components: 15 | schemas: 16 | # This schema ensures that the object is one of the known subtypes. 17 | # We can't put this in ObjectBaseType, because that creates a cycle with allOf, 18 | # causing infinite recursion. 19 | Object: 20 | oneOf: 21 | - $ref: "#/components/schemas/Object1" 22 | - $ref: "#/components/schemas/Object2" 23 | 24 | # Abstract supertype 25 | ObjectBaseType: 26 | type: "object" 27 | required: 28 | - "id" 29 | - "type" 30 | properties: 31 | id: 32 | type: "string" 33 | type: 34 | type: "string" 35 | discriminator: 36 | propertyName: "type" 37 | mapping: 38 | obj1: "#/components/schemas/Object1" 39 | obj2: "#/components/schemas/Object2" 40 | 41 | # Concrete Subtypes 42 | Object1: 43 | # Inherit all constraints defined in the base type 44 | allOf: 45 | - $ref: "#/components/schemas/ObjectBaseType" 46 | # Additionally require subtype-specific properties. 47 | required: 48 | - subtypeProperties 49 | # Disallow properties that are not defined here. 50 | additionalProperties: false 51 | properties: 52 | # Redeclare inherited properties to allow them with additionalProperties: false 53 | # No additional constraints on id 54 | id: {} 55 | # Require type==obj1, so a standard JSON schema validator can discriminate. 56 | # This duplicates the discriminator logic, so should not be required with 57 | # an OAS-compliant validator. 58 | type: 59 | enum: 60 | - obj1 61 | # Define subtype-specific properties 62 | subtypeProperties: 63 | type: object 64 | additionalProperties: false 65 | required: 66 | - "property1a" 67 | - "property1b" 68 | properties: 69 | property1a: 70 | type: "string" 71 | property1b: 72 | type: "string" 73 | 74 | Object2: 75 | # Same idea here... 76 | allOf: 77 | - $ref: "#/components/schemas/ObjectBaseType" 78 | required: 79 | - subtypeProperties 80 | additionalProperties: false 81 | properties: 82 | id: {} 83 | type: 84 | enum: 85 | - obj2 86 | subtypeProperties: 87 | type: object 88 | additionalProperties: false 89 | required: 90 | - "property2a" 91 | - "property2b" 92 | properties: 93 | property2a: 94 | type: "string" 95 | property2b: 96 | type: "string" 97 | -------------------------------------------------------------------------------- /tests/content_length_test.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | 4 | import uvloop 5 | from hypercorn.asyncio import serve 6 | from hypercorn.config import Config 7 | import pydantic 8 | from fastapi import FastAPI, Request, Response, Query 9 | from fastapi.responses import PlainTextResponse 10 | 11 | import pytest 12 | import pytest_asyncio 13 | 14 | 15 | import aiopenapi3 16 | 17 | 18 | app = FastAPI(version="1.0.0", title="TLS tests", servers=[{"url": "/", "description": "Default, relative server"}]) 19 | 20 | 21 | @pytest.fixture(scope="session") 22 | def config(unused_tcp_port_factory): 23 | c = Config() 24 | c.bind = [f"localhost:{unused_tcp_port_factory()}"] 25 | return c 26 | 27 | 28 | @pytest_asyncio.fixture(loop_scope="session") 29 | async def server(config): 30 | event_loop = asyncio.get_event_loop() 31 | try: 32 | sd = asyncio.Event() 33 | task = event_loop.create_task(serve(app, config, shutdown_trigger=sd.wait)) 34 | yield config 35 | finally: 36 | sd.set() 37 | await task 38 | 39 | 40 | @pytest.fixture(scope="session") 41 | def event_loop_policy(): 42 | return uvloop.EventLoopPolicy() 43 | 44 | 45 | @pytest_asyncio.fixture(loop_scope="session") 46 | async def client(server): 47 | api = await aiopenapi3.OpenAPI.load_async(f"http://{server.bind[0]}/openapi.json") 48 | 49 | return api 50 | 51 | 52 | @app.get("/content_length", operation_id="content_length", response_class=PlainTextResponse) 53 | def content_length(request: Request, response: Response, content_length: int = Query()): 54 | return PlainTextResponse(content=b" " * content_length) 55 | 56 | 57 | @pytest.mark.asyncio(loop_scope="session") 58 | async def test_content_length_exceeded(server, client): 59 | cl = random.randint(1, client._max_response_content_length) 60 | r = await client._.content_length(parameters=dict(content_length=cl)) 61 | assert len(r) == cl 62 | 63 | cl = client._max_response_content_length 64 | r = await client._.content_length(parameters=dict(content_length=cl)) 65 | assert len(r) == cl 66 | 67 | with pytest.raises(aiopenapi3.errors.ContentLengthExceededError): 68 | cl = client._max_response_content_length + 1 69 | await client._.content_length(parameters=dict(content_length=cl)) 70 | 71 | 72 | @pytest.mark.asyncio(loop_scope="session") 73 | async def test_sync_content_length_exceeded(server): 74 | client = await asyncio.to_thread( 75 | aiopenapi3.OpenAPI.load_sync, 76 | f"http://{server.bind[0]}/openapi.json", 77 | ) 78 | 79 | cl = random.randint(1, client._max_response_content_length) 80 | r = await asyncio.to_thread(client._.content_length, parameters=dict(content_length=cl)) 81 | assert len(r) == cl 82 | 83 | cl = client._max_response_content_length 84 | r = await asyncio.to_thread(client._.content_length, parameters=dict(content_length=cl)) 85 | assert len(r) == cl 86 | 87 | with pytest.raises(aiopenapi3.errors.ContentLengthExceededError): 88 | cl = client._max_response_content_length + 1 89 | await asyncio.to_thread(client._.content_length, parameters=dict(content_length=cl)) 90 | -------------------------------------------------------------------------------- /tests/api/v2/main.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import uuid 3 | from typing import Annotated 4 | 5 | import starlette.status 6 | from fastapi import Body, Response, Header, APIRouter, Path 7 | from fastapi.responses import JSONResponse 8 | from fastapi_versioning import version 9 | 10 | from . import schema 11 | 12 | from fastapi_versioning import versioned_api_route 13 | 14 | from pydantic import RootModel 15 | 16 | router = APIRouter(route_class=versioned_api_route(2)) 17 | 18 | ZOO = dict() 19 | 20 | 21 | def _idx(l): 22 | yield from range(l) 23 | 24 | 25 | idx = _idx(100) 26 | 27 | 28 | @router.post( 29 | "/pet", 30 | operation_id="createPet", 31 | response_model=schema.Pet, 32 | responses={201: {"model": schema.Pet}, 409: {"model": schema.Error}}, 33 | ) 34 | def createPet( 35 | response: Response, 36 | pet: schema.Pet = Body(..., embed=True), 37 | ) -> schema.Pet: 38 | # if isinstance(pet, Cat): 39 | # pet = pet.__root__ 40 | # elif isinstance(pet, Dog): 41 | # pass 42 | # name = getattr(pet, "name") or getattr(pet, "white_name") or getattr(pet, "black_name") 43 | if pet.name in ZOO: 44 | return JSONResponse( 45 | status_code=starlette.status.HTTP_409_CONFLICT, 46 | content=schema.Error(code=errno.EEXIST, message=f"{pet.name} already exists").model_dump(), 47 | ) 48 | pet.identifier = str(uuid.uuid4()) 49 | ZOO[pet.name] = r = pet 50 | response.status_code = starlette.status.HTTP_201_CREATED 51 | return r 52 | 53 | 54 | @router.get("/pet", operation_id="listPet", response_model=schema.Pets) 55 | def listPet(limit: int | None = None) -> schema.Pets: 56 | return list(ZOO.values()) 57 | 58 | 59 | @router.get("/pets/{petId}", operation_id="getPet", response_model=schema.Pet, responses={404: {"model": schema.Error}}) 60 | def getPet(pet_id: str = Path(..., alias="petId")) -> schema.Pets: 61 | for k, pet in ZOO.items(): 62 | if pet_id == pet.identifier: 63 | return pet 64 | else: 65 | return JSONResponse( 66 | status_code=starlette.status.HTTP_404_NOT_FOUND, 67 | content=schema.Error(code=errno.ENOENT, message=f"{pet_id} not found").model_dump(), 68 | ) 69 | 70 | 71 | @router.delete( 72 | "/pets/{petId}", operation_id="deletePet", responses={204: {"model": None}, 404: {"model": schema.Error}} 73 | ) 74 | def deletePet( 75 | response: Response, 76 | x_raise_nonexist: Annotated[bool | None, Header()], 77 | pet_id: str = Path(..., alias="petId"), 78 | ) -> None: 79 | for k, pet in ZOO.items(): 80 | if pet_id == pet.identifier: 81 | del ZOO[k] 82 | response.status_code = starlette.status.HTTP_204_NO_CONTENT 83 | return response 84 | else: 85 | return JSONResponse( 86 | status_code=starlette.status.HTTP_404_NOT_FOUND, 87 | content=schema.Error(code=errno.ENOENT, message=f"{pet_id} not found").model_dump(), 88 | ) 89 | 90 | 91 | @router.patch("/pets", operation_id="patchPets", responses={200: {"model": schema.Pets}}) 92 | def patchPets(response: Response, pets: schema.Pets): 93 | print(pets) 94 | return pets + list(ZOO.values()) 95 | -------------------------------------------------------------------------------- /tests/fixtures/paths-parameter-format-v20.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: with parameter format 4 | description: with parameter format 5 | version: 1.0.0 6 | host: api.example.com 7 | basePath: /v1 8 | schemes: 9 | - https 10 | 11 | consumes: 12 | - application/json 13 | produces: 14 | - application/json 15 | 16 | definitions: {} 17 | 18 | paths: 19 | /path/{string}/{array}/{default}: 20 | parameters: 21 | - in: path 22 | name: string 23 | type: string 24 | required: true 25 | - in: path 26 | name: array 27 | type: array 28 | items: 29 | type: string 30 | required: true 31 | collectionFormat: pipes 32 | - in: path 33 | name: default 34 | type: string 35 | default: "default" 36 | get: 37 | operationId: path 38 | responses: 39 | "200": 40 | description: OK 41 | schema: 42 | type: string 43 | 44 | /query: 45 | get: 46 | parameters: 47 | - in: query 48 | name: string 49 | type: string 50 | required: true 51 | - in: query 52 | name: array 53 | type: array 54 | items: 55 | type: string 56 | required: true 57 | collectionFormat: tsv 58 | - in: query 59 | name: default 60 | type: string 61 | default: "default" 62 | operationId: query 63 | responses: 64 | "200": 65 | description: OK 66 | schema: 67 | type: string 68 | 69 | '/formdata': 70 | post: 71 | consumes: 72 | - multipart/form-data 73 | description: "" 74 | operationId: formdata 75 | parameters: 76 | - description: The JSON file containing the RunDefinition 77 | in: formData 78 | name: file0 79 | required: true 80 | type: file 81 | - description: The zip archive of the project folder containing the source code to use for the run. 82 | in: formData 83 | name: file1 84 | required: true 85 | type: file 86 | produces: 87 | - application/json 88 | responses: 89 | '200': 90 | description: ok 91 | schema: 92 | type: string 93 | summary: Start a run on a remote compute target. 94 | 95 | '/urlencoded': 96 | post: 97 | operationId: urlencoded 98 | consumes: 99 | - application/x-www-form-urlencoded 100 | parameters: 101 | - in: formData 102 | name: A 103 | type: string 104 | description: A 105 | - in: formData 106 | name: B 107 | type: number 108 | description: B 109 | responses: 110 | "200": 111 | description: OK 112 | schema: 113 | type: string 114 | 115 | '/file': 116 | get: 117 | operationId: getfile 118 | produces: 119 | - application/octet-stream 120 | responses: 121 | "200": 122 | description: OK 123 | schema: 124 | type: file 125 | -------------------------------------------------------------------------------- /tests/apiv1_test.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import uuid 3 | 4 | import pytest 5 | import pytest_asyncio 6 | 7 | import uvloop 8 | from hypercorn.asyncio import serve 9 | from hypercorn.config import Config 10 | 11 | import aiopenapi3 12 | 13 | # pytest.skip(allow_module_level=True) 14 | 15 | from api.main import app 16 | 17 | 18 | @pytest.fixture(scope="session") 19 | def config(unused_tcp_port_factory): 20 | c = Config() 21 | c.bind = [f"localhost:{unused_tcp_port_factory()}"] 22 | return c 23 | 24 | 25 | @pytest_asyncio.fixture(loop_scope="session") 26 | async def server(config): 27 | event_loop = asyncio.get_running_loop() 28 | try: 29 | sd = asyncio.Event() 30 | task = event_loop.create_task(serve(app, config, shutdown_trigger=sd.wait)) 31 | yield config 32 | finally: 33 | sd.set() 34 | await task 35 | 36 | 37 | @pytest.fixture(scope="session") 38 | def event_loop_policy(): 39 | return uvloop.EventLoopPolicy() 40 | 41 | 42 | @pytest_asyncio.fixture(loop_scope="session") 43 | async def client(server): 44 | api = await asyncio.to_thread(aiopenapi3.OpenAPI.load_sync, f"http://{server.bind[0]}/v1/openapi.json") 45 | return api 46 | 47 | 48 | def randomPet(name=None): 49 | return {"data": {"pet": {"name": str(name or uuid.uuid4()), "pet_type": "dog"}}, "return_headers": True} 50 | 51 | 52 | @pytest.mark.asyncio(loop_scope="session") 53 | async def test_createPet(server, client): 54 | h, r = await asyncio.to_thread(client._.createPet, **randomPet()) 55 | assert type(r).model_json_schema() == client.components.schemas["Pet"].get_type().model_json_schema() 56 | assert h["X-Limit-Remain"] == 5 57 | 58 | with pytest.raises(aiopenapi3.errors.HTTPClientError) as e: 59 | await asyncio.to_thread(client._.createPet, data={"pet": {"name": r.name}}) 60 | assert type(e.value.data).model_json_schema() == client.components.schemas["Error"].get_type().model_json_schema() 61 | 62 | 63 | @pytest.mark.asyncio(loop_scope="session") 64 | async def test_listPet(server, client): 65 | h, r = await asyncio.to_thread(client._.createPet, **randomPet(uuid.uuid4())) 66 | l = await asyncio.to_thread(client._.listPet) 67 | assert len(l) > 0 68 | 69 | 70 | @pytest.mark.asyncio(loop_scope="session") 71 | async def test_getPet(server, client): 72 | h, pet = await asyncio.to_thread(client._.createPet, **randomPet(uuid.uuid4())) 73 | r = await asyncio.to_thread(client._.getPet, parameters={"petId": pet.id}) 74 | # FastAPI 0.101 Serialization changes 75 | # assert type(r).model_json_schema() == type(pet).model_json_schema() 76 | assert r.id == pet.id 77 | 78 | with pytest.raises(aiopenapi3.errors.HTTPClientError) as e: 79 | await asyncio.to_thread(client._.getPet, parameters={"petId": -1}) 80 | 81 | assert type(e.value.data).model_json_schema() == client.components.schemas["Error"].get_type().model_json_schema() 82 | 83 | 84 | @pytest.mark.asyncio(loop_scope="session") 85 | async def test_deletePet(server, client): 86 | with pytest.raises(aiopenapi3.errors.HTTPClientError) as e: 87 | await asyncio.to_thread(client._.deletePet, parameters={"petId": -1}) 88 | 89 | assert type(e.value.data).model_json_schema() == client.components.schemas["Error"].get_type().model_json_schema() 90 | 91 | await asyncio.to_thread(client._.createPet, **randomPet(uuid.uuid4())) 92 | zoo = await asyncio.to_thread(client._.listPet) 93 | for pet in zoo: 94 | await asyncio.to_thread(client._.deletePet, parameters={"petId": pet.id}) 95 | -------------------------------------------------------------------------------- /tests/fixtures/paths-security-v20.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: Sample API 4 | description: API description in Markdown. 5 | version: 1.0.0 6 | host: api.example.com 7 | basePath: /v1 8 | schemes: 9 | - https 10 | 11 | consumes: 12 | - application/json 13 | produces: 14 | - application/json 15 | 16 | securityDefinitions: 17 | BasicAuth: 18 | type: basic 19 | HeaderAuth: 20 | type: apiKey 21 | in: header 22 | name: Authorization 23 | QueryAuth: 24 | type: apiKey 25 | in: query 26 | name: auth 27 | user: 28 | type: apiKey 29 | in: header 30 | name: x-user 31 | token: 32 | type: apiKey 33 | in: header 34 | name: x-token 35 | 36 | 37 | security: 38 | - BasicAuth: [] 39 | 40 | paths: 41 | /alternate: 42 | get: 43 | operationId: alternateSecurity 44 | responses: 45 | "200": 46 | description: '' 47 | schema: 48 | type: string 49 | enum: 50 | - alternate 51 | security: 52 | - user: [] 53 | token: [] 54 | - BasicAuth: [] 55 | 56 | /combined: 57 | get: 58 | operationId: combinedSecurity 59 | responses: 60 | "200": 61 | description: '' 62 | schema: 63 | type: string 64 | enum: 65 | - combined 66 | security: 67 | - user: [] 68 | token: [] 69 | 70 | /users/{userId}: 71 | get: 72 | operationId: getUser 73 | summary: Returns a user by ID. 74 | parameters: 75 | - in: path 76 | name: userId 77 | required: true 78 | type: integer 79 | responses: 80 | "200": 81 | description: OK 82 | schema: 83 | $ref: '#/definitions/User' 84 | "400": 85 | description: The specified user ID is invalid (e.g. not a number). 86 | "404": 87 | description: A user with the specified ID was not found. 88 | default: 89 | description: Unexpected error 90 | /users: 91 | get: 92 | security: 93 | - {} 94 | operationId: listUsers 95 | summary: Returns a list of users. 96 | description: Optional extended description in Markdown. 97 | parameters: 98 | - in: header 99 | name: inHeader 100 | type: string 101 | - in: query 102 | name: inQuery 103 | type: string 104 | produces: 105 | - application/json 106 | responses: 107 | "200": 108 | description: OK 109 | schema: 110 | type: array 111 | items: 112 | $ref: '#/definitions/User' 113 | post: 114 | security: 115 | - QueryAuth: [] 116 | - HeaderAuth: [] 117 | operationId: createUser 118 | summary: Creates a new user. 119 | parameters: 120 | - in: body 121 | name: user 122 | required: true 123 | schema: 124 | $ref: '#/definitions/User' 125 | responses: 126 | "200": 127 | description: OK 128 | schema: 129 | $ref: '#/definitions/User' 130 | 131 | definitions: 132 | User: 133 | type: object 134 | properties: 135 | id: 136 | type: integer 137 | name: 138 | type: string 139 | # Both properties are required 140 | required: 141 | - id 142 | - name 143 | -------------------------------------------------------------------------------- /tests/plugin_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from pathlib import Path 3 | 4 | import httpx 5 | import yarl 6 | 7 | from aiopenapi3 import FileSystemLoader, OpenAPI 8 | from aiopenapi3.plugin import Init, Message, Document 9 | 10 | 11 | class OnInit(Init): 12 | def initialized(self, ctx): 13 | # this does not change the operationId on the ssi as the ssi is initialized already 14 | # changing an operationId would be better in OnDocument.parsed 15 | ctx.initialized.paths["/pets"].get.operationId = "listPets" 16 | # therefore update the operation index 17 | self.api._init_operationindex(False) 18 | return ctx 19 | 20 | 21 | class OnDocument(Document): 22 | def __init__(self, url): 23 | self.url = url 24 | super().__init__() 25 | 26 | def loaded(self, ctx): 27 | return ctx 28 | 29 | def parsed(self, ctx): 30 | if ctx.url.path == self.url: 31 | ctx.document["components"] = { 32 | "schemas": {"Pet": {"$ref": "petstore-expanded.yaml#/components/schemas/Pet"}} 33 | } 34 | ctx.document["servers"] = [{"url": "/"}] 35 | elif ctx.url.path == "petstore-expanded.yaml": 36 | ctx.document["components"]["schemas"]["Pet"]["allOf"].append( 37 | { 38 | "type": "object", 39 | "required": ["color"], 40 | "properties": { 41 | "color": {"type": "string", "default": "blue"}, 42 | "weight": {"type": "integer", "default": 10}, 43 | }, 44 | } 45 | ) 46 | ctx.document["components"]["schemas"]["Pet"]["allOf"][1]["properties"]["created"]["format"] = "date-time" 47 | else: 48 | raise ValueError(f"unexpected url {ctx.url.path} expecting {self.url}") 49 | 50 | return ctx 51 | 52 | 53 | class OnMessage(Message): 54 | def marshalled(self, ctx): 55 | return ctx 56 | 57 | def sending(self, ctx): 58 | return ctx 59 | 60 | def received(self, ctx): 61 | ctx.received = """[{"id":1,"name":"theanimal", "created":4711,"weight": null}]""" 62 | return ctx 63 | 64 | def parsed(self, ctx): 65 | if ctx.operationId == "listPets": 66 | if ctx.parsed[0].get("color", None) is None: 67 | ctx.parsed[0]["color"] = "red" 68 | 69 | if ctx.parsed[0]["id"] == 1: 70 | ctx.parsed[0]["id"] = 2 71 | return ctx 72 | 73 | def unmarshalled(self, ctx): 74 | if ctx.operationId == "listPets": 75 | if ctx.unmarshalled[0].id == 2: 76 | ctx.unmarshalled[0].id = 3 77 | return ctx 78 | 79 | 80 | def test_Plugins(httpx_mock, with_plugin_base): 81 | httpx_mock.add_response(headers={"Content-Type": "application/json"}, content=b"[]") 82 | plugins = [OnInit(), OnDocument("plugin-base.yaml"), OnMessage()] 83 | api = OpenAPI.loads( 84 | "plugin-base.yaml", 85 | with_plugin_base, 86 | plugins=plugins, 87 | loader=FileSystemLoader(Path().cwd() / "tests/fixtures"), 88 | session_factory=httpx.Client, 89 | ) 90 | api._base_url = yarl.URL("http://127.0.0.1:80") 91 | r = api._.listPets() 92 | assert r 93 | 94 | item = r[0] 95 | assert item.id == 3 96 | assert item.weight == None # default does not apply as it it unsed 97 | assert item.color == "red" # default does not apply 98 | assert item.created == datetime.datetime.fromtimestamp(4711, tz=datetime.timezone.utc) 99 | -------------------------------------------------------------------------------- /tests/fixtures/paths-security.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: '' 4 | version: 0.0.0 5 | servers: 6 | - url: http://127.0.0.1/api 7 | 8 | security: 9 | - cookieAuth: [] 10 | 11 | paths: 12 | /api/v1/auth/info/: 13 | get: 14 | operationId: api_v1_auth_login_info 15 | responses: 16 | '200': 17 | content: 18 | application/json: 19 | schema: 20 | $ref: '#/components/schemas/Login' 21 | description: '' 22 | 23 | /api/v1/auth/login/: 24 | post: 25 | operationId: api_v1_auth_login_create 26 | description: |- 27 | Check the credentials and return the REST Token 28 | if the credentials are valid and authenticated. 29 | Calls Django Auth login method to register User ID 30 | in Django session framework 31 | 32 | Accept the following POST parameters: username, password 33 | Return the REST Framework Token Object's key. 34 | tags: 35 | - api 36 | requestBody: 37 | content: 38 | application/json: 39 | schema: 40 | $ref: '#/components/schemas/LoginRequest' 41 | required: true 42 | responses: 43 | '200': 44 | content: 45 | application/json: 46 | schema: 47 | $ref: '#/components/schemas/Login' 48 | description: '' 49 | security: 50 | - cookieAuth: [] 51 | - tokenAuth: [] 52 | - paramAuth: [] 53 | - basicAuth: [] 54 | - digestAuth: [] 55 | - bearerAuth: [] 56 | - {} 57 | 58 | /api/v1/auth/combined/: 59 | post: 60 | operationId: api_v1_auth_login_combined 61 | description: '' 62 | tags: 63 | - api 64 | requestBody: 65 | content: 66 | application/json: 67 | schema: 68 | $ref: '#/components/schemas/LoginRequest' 69 | required: true 70 | responses: 71 | '200': 72 | content: 73 | application/json: 74 | schema: 75 | $ref: '#/components/schemas/Login' 76 | description: '' 77 | security: 78 | - user: [] 79 | token: [] 80 | 81 | 82 | /api/v1/auth/null/: 83 | get: 84 | operationId: api_v1_auth_login_null 85 | description: '' 86 | tags: 87 | - api 88 | responses: 89 | '200': 90 | content: 91 | application/json: 92 | schema: 93 | $ref: '#/components/schemas/Login' 94 | description: '' 95 | security: [] 96 | 97 | 98 | components: 99 | schemas: 100 | Login: 101 | type: string 102 | enum: 103 | - user 104 | LoginRequest: 105 | type: string 106 | securitySchemes: 107 | cookieAuth: 108 | type: apiKey 109 | in: cookie 110 | name: Session 111 | tokenAuth: 112 | type: apiKey 113 | in: header 114 | name: Authorization 115 | description: Token-based authentication with required prefix "Token" 116 | paramAuth: 117 | type: apiKey 118 | in: query 119 | name: auth 120 | basicAuth: 121 | type: http 122 | scheme: basic 123 | digestAuth: 124 | type: http 125 | scheme: digest 126 | bearerAuth: 127 | type: http 128 | scheme: bearer 129 | user: 130 | type: apiKey 131 | in: header 132 | name: x-user 133 | token: 134 | type: apiKey 135 | in: header 136 | name: x-token 137 | --------------------------------------------------------------------------------