Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions src/aiopenapi3/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,23 @@
RequestParameter = str | BaseModel
RequestParameters = dict[str, RequestParameter]

RootType = v20.Root | v30.Root | v31.Root
ServerType = v30.Server | v31.Server
ReferenceType = v20.Reference | v30.Reference | v31.Reference
SchemaType = v20.Schema | v30.Schema | v31.Schema
v3xSchemaType = v30.Schema | v31.Schema
DiscriminatorType = v30.Discriminator | v31.Discriminator
PathItemType = v20.PathItem, v30.PathItem | v31.PathItem
OperationType = v20.Operation | v30.Operation | v31.Operation
ParameterType = v20.Parameter | v30.Parameter | v31.Parameter
HeaderType = v20.Header | v30.Header | v31.Header
RootType = v20.Root | v30.Root | v31.Root | v32.Root
ServerType = v30.Server | v31.Server | v32.Server
ReferenceType = v20.Reference | v30.Reference | v31.Reference | v32.Reference
SchemaType = v20.Schema | v30.Schema | v31.Schema | v32.Schema
v3xSchemaType = v30.Schema | v31.Schema | v32.Schema
DiscriminatorType = v30.Discriminator | v31.Discriminator | v32.Discriminator
PathItemType = v20.PathItem, v30.PathItem | v31.PathItem | v32.PathItem
OperationType = v20.Operation | v30.Operation | v31.Operation | v32.Operation
ParameterType = v20.Parameter | v30.Parameter | v31.Parameter | v32.Parameter
HeaderType = v20.Header | v30.Header | v31.Header | v32.Header
RequestType = v20.Request | v30.Request
AsyncRequestType = v20.AsyncRequest | v30.AsyncRequest
MediaTypeType = v30.MediaType | v31.MediaType
MediaTypeType = v30.MediaType | v31.MediaType | v32.MediaType
ExpectedType = v20.Response | MediaTypeType
ResponseHeadersType = dict[str, str | BaseModel | list[BaseModel]]
ResponseDataType = BaseModel, bytes | str
TagType = v20.Tag | v30.Tag | v32.Tag
TagType = v20.Tag | v30.Tag | v31.Tag | v32.Tag

YAMLLoaderType = type[yaml.Loader] | type[yaml.CLoader] | type[yaml.SafeLoader] | type[yaml.CSafeLoader]

Expand Down
16 changes: 8 additions & 8 deletions src/aiopenapi3/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class PathItemBase:
class RootBase:
@staticmethod
def resolve(api: "OpenAPI", root: "RootBase", obj, _PathItem, _Reference):
from . import v20, v30, v31
from . import v20, v30, v31, v32

def replaceSchemaReference(data):
def replace(ivalue):
Expand Down Expand Up @@ -121,14 +121,14 @@ def replace(ivalue):
continue

# v3.1 - Schema $ref
if isinstance(root, (v20.root.Root, v30.root.Root, v31.root.Root)): # noqa: SIM102
if isinstance(root, (v20.root.Root, v30.root.Root, v31.root.Root, v32.root.Root)): # noqa: SIM102
if isinstance(value, SchemaBase): # noqa: SIM102
if (r := getattr(value, "ref", None)) and not isinstance(r, ReferenceBase):
value = _Reference.model_construct(ref=r)
setattr(obj, slot, value)

if isinstance(root, (v30.root.Root, v31.root.Root)): # noqa: SIM102
if isinstance(value, (v30.Discriminator, v31.Discriminator)):
if isinstance(root, (v30.root.Root, v31.root.Root, v32.root.Root)): # noqa: SIM102
if isinstance(value, (v30.Discriminator, v31.Discriminator, v32.Discriminator)):
"""
Discriminated Unions - implementing undefined behavior
sub-schemas not having the discriminated property "const" or enum or mismatching the mapping
Expand Down Expand Up @@ -192,7 +192,7 @@ def replace(ivalue):
PathItem Ref is ambiguous
https://github.com/OAI/OpenAPI-Specification/issues/2635
"""
if isinstance(root, (v20.root.Root, v30.root.Root, v31.root.Root)): # noqa: SIM102
if isinstance(root, (v20.root.Root, v30.root.Root, v31.root.Root, v32.root.Root)): # noqa: SIM102
if isinstance(obj, _PathItem) and slot == "ref":
ref = _Reference.model_construct(ref=value)
ref._target = api.resolve_jr(root, obj, ref)
Expand All @@ -216,7 +216,7 @@ def replace(ivalue):
else:
raise TypeError(type(value), value)
elif isinstance(obj, dict):
if isinstance(root, (v20.root.Root, v31.root.Root)):
if isinstance(root, (v20.root.Root, v31.root.Root, v32.root.Root)):
"""
Resolving/Replacing Swagger 2.0 nested Schema.ref
Schema.properties[name] -> Schema.ref ==> Schema.properties[name] -> Reference
Expand All @@ -231,7 +231,7 @@ def replace(ivalue):
RootBase.resolve(api, root, v, _PathItem, _Reference)

elif isinstance(obj, list):
if isinstance(root, (v20.root.Root, v31.root.Root)):
if isinstance(root, (v20.root.Root, v31.root.Root, v32.root.Root)):
replaceSchemaReference(obj)

# if it's a list, resolve its item's references
Expand Down Expand Up @@ -338,7 +338,7 @@ class SchemaBase(BaseModel):
_model_types is used to store these different model representations of the same schema
"""

_identity: str = PrivateAttr(default=None)
_identity: str | None = PrivateAttr(default=None)
"""
The _identity attribute is set during OpenAPI.__init__ and used to create the class name in get_type()
"""
Expand Down
6 changes: 3 additions & 3 deletions src/aiopenapi3/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ def createClassInfo(
extra: list["SchemaType"] | None,
args: dict[str, Any] | None = None,
) -> _ClassInfo:
from . import v20, v30, v31
from . import v20, v30, v31, v32

type_name = schema._get_identity("L8") # + f"_{type}"

Expand Down Expand Up @@ -345,7 +345,7 @@ def createClassInfo(

if hasattr(schema, "anyOf") and schema.anyOf:
assert all(schema.anyOf)
assert isinstance(schema, (v30.Schema, v31.Schema))
assert isinstance(schema, (v30.Schema, v31.Schema, v32.Schema))
t = tuple(
i.get_type(
names=schemanames + ([cast(str, i.ref)] if isinstance(i, ReferenceBase) else []),
Expand All @@ -363,7 +363,7 @@ def createClassInfo(
if len(t):
classinfo.root = Union[t]
elif hasattr(schema, "oneOf") and schema.oneOf:
assert isinstance(schema, (v30.Schema, v31.Schema))
assert isinstance(schema, (v30.Schema, v31.Schema, v32.Schema))
t = tuple(
i.get_type(
names=schemanames + ([cast(str, i.ref)] if isinstance(i, ReferenceBase) else []),
Expand Down
18 changes: 8 additions & 10 deletions src/aiopenapi3/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
)


def has_components(y: Optional["RootType"]) -> TypeGuard[v30.Root | v31.Root]:
def has_components(y: Optional["RootType"]) -> TypeGuard[v30.Root | v31.Root | v32.Root]:
# return all([typing.cast("RootType", y), typing.cast("RootType", y).components])
# return isinstance(y, (v30.Root, v31.Root))
# return all([y, y.components])
Expand All @@ -47,7 +47,7 @@ def has_components(y: Optional["RootType"]) -> TypeGuard[v30.Root | v31.Root]:


def is_schema(v: tuple[str, "SchemaType"]) -> TypeGuard["SchemaType"]:
return isinstance(v[1], (v20.Schema, v30.Schema, v31.Schema))
return isinstance(v[1], (v20.Schema, v30.Schema, v31.Schema, v32.Schema))


class OpenAPI:
Expand Down Expand Up @@ -414,7 +414,7 @@ def _init_operationindex(self, use_operation_tags: bool) -> bool:
for c, content in response.content.items():
if content.schema_ is None:
continue
if isinstance(content.schema_, (v30.Schema, v31.Schema)):
if isinstance(content.schema_, (v30.Schema, v31.Schema, v32.Schema)):
content.schema_._get_identity("OP", f"{path}.{m}.{r}.{c}")
else:
if isinstance(self._root, v30.Root):
Expand Down Expand Up @@ -480,9 +480,6 @@ def _iterate_schemas(cls, schemas: dict[int, "SchemaType"], next_set: set[int],
def _init_schema_types_collect(self, only_required: bool) -> dict[str, "SchemaType"]:
byname: dict[str, SchemaType] = {}

def is_schema(v: tuple[str, "SchemaType"]) -> bool:
return isinstance(v[1], (v20.Schema, v30.Schema, v31.Schema))

op: Operation
if isinstance(self._root, v20.Root):
documents = cast(list[v20.Root], self._documents.values())
Expand Down Expand Up @@ -520,9 +517,9 @@ def is_schema(v: tuple[str, "SchemaType"]) -> bool:
# assert byname.get(name, None) in [None, response.schema_]
byname[n] = response.schema_

elif isinstance(self._root, (v30.Root, v31.Root)):
elif isinstance(self._root, (v30.Root, v31.Root, v32.Root)):
# Schema
documents = cast(list[v30.Root] | list[v31.Root], self._documents.values())
documents = cast(list[v30.Root] | list[v31.Root] | list[v32.Root], self._documents.values())
components = [x.components for x in filter(has_components, documents) if x.components is not None]
assert components is not None
if only_required is False:
Expand Down Expand Up @@ -570,7 +567,7 @@ def is_schema(v: tuple[str, "SchemaType"]) -> bool:
for r, response in op.responses.items():
if isinstance(response, ReferenceBase):
response = response._target
if isinstance(response, (v30.paths.Response, v31.paths.Response)):
if isinstance(response, (v30.paths.Response, v31.paths.Response, v32.paths.Response)):
assert response.content is not None
for mt, mto in response.content.items():
if mto.schema_ is None:
Expand Down Expand Up @@ -608,6 +605,7 @@ def _init_schema_types(self, only_required: bool) -> None:
"""
Due to Plugins (e.g. Cull/Reduce) byname may be incomplete
"""
# from . import v32
resolved: list[SchemaType] = [
byid[x]._target if isinstance(byid[x], ReferenceBase) else byid[x] for x in todo | data
]
Expand Down Expand Up @@ -692,7 +690,7 @@ def authenticate(self, *args, **kwargs):

if isinstance(self._root, v20.Root):
v = schemes - frozenset(SecuritySchemes := self._root.securityDefinitions)
elif isinstance(self._root, (v30.Root, v31.Root)):
elif isinstance(self._root, (v30.Root, v31.Root, v32.Root)):
v = schemes - frozenset(SecuritySchemes := self._root.components.securitySchemes)
else:
raise TypeError(self._root)
Expand Down
20 changes: 17 additions & 3 deletions src/aiopenapi3/v30/glue.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,17 @@ def _prepare_secschemes_default(self, scheme: str, value: str | Sequence[str]) -
and self.root.components.securitySchemes[scheme].root
)
ss = self.root.components.securitySchemes[scheme].root
from .. import v30, v31
from .. import v30, v31, v32

if ss.type == "http":
assert isinstance(ss, (v30.security._SecuritySchemes.http, v31.security._SecuritySchemes.http))
assert isinstance(
ss,
(
v30.security._SecuritySchemes.http,
v31.security._SecuritySchemes.http,
v32.security._SecuritySchemes.http,
),
)
if ss.scheme_ == "basic":
self.req.auth = httpx2.BasicAuth(*value)
elif ss.scheme_ == "digest":
Expand All @@ -164,7 +171,14 @@ def _prepare_secschemes_default(self, scheme: str, value: str | Sequence[str]) -
value = cast(str, value)

if ss.type == "apiKey":
assert isinstance(ss, (v30.security._SecuritySchemes.apiKey, v31.security._SecuritySchemes.apiKey))
assert isinstance(
ss,
(
v30.security._SecuritySchemes.apiKey,
v31.security._SecuritySchemes.apiKey,
v32.security._SecuritySchemes.apiKey,
),
)
if ss.in_ == "query":
# apiKey in query parameter
self.req.params[ss.name] = value
Expand Down
4 changes: 2 additions & 2 deletions src/aiopenapi3/v32/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ class Header(ParameterBase, _ParameterCodec):
.. _here: https://spec.openapis.org/oas/v3.2.0.html#header-object
"""

allowEmptyValue: None
allowReserved: None
allowEmptyValue: None = None
allowReserved: None = None

def _codec(self):
schema = self.schema_ or self.content.get("application/json").schema_
Expand Down
2 changes: 1 addition & 1 deletion src/aiopenapi3/v32/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class Callback(RootModel):
The key that identifies the Path Item Object is a runtime expression that can be evaluated in the context of a
runtime HTTP request/response to identify the URL to be used for the callback request.
"""
root: dict["RuntimeExpression", PathItem]
root: dict[str, PathItem]


class RuntimeExpression(RootModel):
Expand Down
6 changes: 5 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ def schema(self):
return getattr(aiopenapi3, f"v{self.major}{self.minor}").Schema


@pytest.fixture(scope="session", params=[_Version(3, 0, 3), _Version(3, 1, 0)], ids=("v30", "v31"))
@pytest.fixture(
scope="session",
params=[_Version(3, 0, 3), _Version(3, 1, 0), _Version(3, 2, 0)],
ids=("v30", "v31", "v32"),
)
def openapi_version(request):
return request.param

Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/schema-itemSchema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ paths:
content:
application/jsonl:
itemSchema:
$ref: "#/components/responses/LogEntry"
$ref: "#/components/schemas/LogEntry"

/ndjson:
get:
Expand Down
50 changes: 50 additions & 0 deletions tests/fixtures/schema-nullable-v32.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
openapi: 3.2.0
info:
title: ''
version: 0.0.0
servers:
- url: http://127.0.0.1/api

security:
- {}

paths: {}

components:
schemas:
object:
type: [object, "null"]
additionalProperties: false
properties:
attr:
$ref: '#/components/schemas/nullable'
required:
- attr

array:
type: [array, "null"]
items:
$ref: '#/components/schemas/nullable'

union:
oneOf:
- $ref: '#/components/schemas/string'
- $ref: '#/components/schemas/integer'

string:
type: [string, "null"]

integer:
type: [integer, "null"]

boolean:
type: [boolean, "null"]

nullable:
type: [string, "null"]

multi:
type: [integer, string, "null"]

"null":
type: "null"
33 changes: 33 additions & 0 deletions tests/fixtures/schema-oneOf-nullable-v32.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
openapi: 3.2.0
info:
title: ''
version: 0.0.0
servers:
- url: http://127.0.0.1/api

security:
- {}

paths: {}

components:
schemas:
object:
type: object
additionalProperties: false
properties:
typed:
oneOf:
- type: string
enum:
- "5"
- type: string
enum:
- "4"
- type: "null"

enumed:
oneOf:
- type: string
enum: ["5"]
- enum: [null]
7 changes: 6 additions & 1 deletion tests/parsing_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,13 @@ def test_schema_array(with_schema_array):
def test_parsing_paths_content_nested_array_ref(openapi_version, with_parsing_paths_content_nested_array_ref):
import aiopenapi3.v30.general
import aiopenapi3.v31.general
import aiopenapi3.v32.general

expected = {0: aiopenapi3.v30.general.Reference, 1: aiopenapi3.v31.general.Reference}[openapi_version.minor]
expected = {
0: aiopenapi3.v30.general.Reference,
1: aiopenapi3.v31.general.Reference,
2: aiopenapi3.v32.general.Reference,
}[openapi_version.minor]

OpenAPI("/", with_parsing_paths_content_nested_array_ref)

Expand Down
7 changes: 6 additions & 1 deletion tests/ref_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,13 @@ def is_required(x):
def test_paths_content_schema_array_ref(openapi_version):
import aiopenapi3.v30.general
import aiopenapi3.v31.general
import aiopenapi3.v32.general

expected = {0: aiopenapi3.v30.general.Reference, 1: aiopenapi3.v31.general.Reference}[openapi_version.minor]
expected = {
0: aiopenapi3.v30.general.Reference,
1: aiopenapi3.v31.general.Reference,
2: aiopenapi3.v32.general.Reference,
}[openapi_version.minor]

SPEC = f"""openapi: {openapi_version}
info:
Expand Down