diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a463f5bd..5fb1eb37 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pycln - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.13 + rev: v0.16.5 hooks: # Run the linter. - id: ruff-check @@ -62,7 +62,7 @@ repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.11.15 + rev: 0.12.8 hooks: # Update the uv lockfile - id: uv-lock diff --git a/docs/source/_ext/aioai3.py b/docs/source/_ext/aioai3.py index 7708b8b2..7855a284 100644 --- a/docs/source/_ext/aioai3.py +++ b/docs/source/_ext/aioai3.py @@ -1,7 +1,8 @@ -from sphinx.domains import Domain -from sphinx.roles import XRefRole +from typing import ClassVar from docutils import nodes +from sphinx.domains import Domain +from sphinx.roles import XRefRole def resolve_url(env, name): @@ -15,7 +16,7 @@ class aiopenapi3Domain(Domain): name = "aioai3" label = "aiopenapi3 code linker" - roles = { + roles: ClassVar = { "ref": XRefRole(), } diff --git a/docs/source/conf.py b/docs/source/conf.py index 118180f1..a90aba48 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,13 +6,12 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -import os +import datetime import importlib import inspect -from pathlib import Path +import os import sys -import datetime - +from pathlib import Path sys.path.append(str(p := (Path(".").absolute() / "_ext"))) assert p.exists(), f"{p} {os.getcwd()}" @@ -26,7 +25,7 @@ """ project = "aiopenapi3" -copyright = f"{datetime.datetime.now().date().year}, Markus Kötter" +copyright = f"{datetime.datetime.now().date().year}, Markus Kötter" # noqa: DTZ005 author = "Markus Kötter" # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 69ecf2cf..f828d920 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,14 @@ indent-width = 4 [tool.ruff.lint] fixable = ["ALL"] -ignore = ["E741","F841","E402"] +ignore = [ + "E741", # ambiguous-variable-name + "F841", # unused-variable +] + +[tool.ruff.lint.per-file-ignores] +"src/aiopenapi3/model.py" = ["UP007"] # https://github.com/astral-sh/ruff/issues/21347 +"tests/parsing_test.py" = ["UP007"] [tool.ruff.format] quote-style = "double" diff --git a/src/aiopenapi3/__init__.py b/src/aiopenapi3/__init__.py index 7cb9c190..88dba863 100644 --- a/src/aiopenapi3/__init__.py +++ b/src/aiopenapi3/__init__.py @@ -1,30 +1,29 @@ -from .version import __version__ -from .openapi import OpenAPI -from .loader import FileSystemLoader from .errors import ( - SpecError, - ReferenceResolutionError, + ContentTypeError, HTTPError, - ResponseError, HTTPStatusError, - ContentTypeError, + ReferenceResolutionError, + RequestError, ResponseDecodingError, + ResponseError, ResponseSchemaError, - RequestError, + SpecError, ) - +from .loader import FileSystemLoader +from .openapi import OpenAPI +from .version import __version__ __all__ = [ - "__version__", - "OpenAPI", + "ContentTypeError", "FileSystemLoader", - "SpecError", - "ReferenceResolutionError", "HTTPError", - "ResponseError", "HTTPStatusError", - "ContentTypeError", + "OpenAPI", + "ReferenceResolutionError", + "RequestError", "ResponseDecodingError", + "ResponseError", "ResponseSchemaError", - "RequestError", + "SpecError", + "__version__", ] diff --git a/src/aiopenapi3/__main__.py b/src/aiopenapi3/__main__.py index 7d876942..7d73de00 100644 --- a/src/aiopenapi3/__main__.py +++ b/src/aiopenapi3/__main__.py @@ -1,4 +1,5 @@ import sys + from .cli import main if __name__ == "__main__": diff --git a/src/aiopenapi3/_types.py b/src/aiopenapi3/_types.py index 18cfffa3..eeab9a72 100644 --- a/src/aiopenapi3/_types.py +++ b/src/aiopenapi3/_types.py @@ -1,87 +1,79 @@ import re -from typing import TYPE_CHECKING, Union, TypeAlias, Optional, Literal from collections.abc import Sequence +from typing import Literal, TypeAlias import yaml - -from httpx._types import RequestContent, FileTypes, RequestFiles, AuthTypes # noqa +from httpx2._types import AuthTypes, FileTypes, RequestContent, RequestFiles from pydantic import BaseModel - from . import v20, v30, v31, v32 -if TYPE_CHECKING: - pass - - RequestFileParameter = tuple[str, FileTypes] RequestFilesParameter = Sequence[RequestFileParameter] -JSON: TypeAlias = Optional[Union[dict[str, "JSON"], list["JSON"], str, int, float, bool]] +JSON: TypeAlias = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None """ Define a JSON type https://github.com/python/typing/issues/182#issuecomment-1320974824 """ -RequestData = Union[JSON, BaseModel, RequestFilesParameter] -RequestParameter = Union[str, BaseModel] +RequestData = JSON | BaseModel | RequestFilesParameter +RequestParameter = str | BaseModel RequestParameters = dict[str, RequestParameter] -RootType = Union[v20.Root, v30.Root, v31.Root] -ServerType = Union[v30.Server, v31.Server] -ReferenceType = Union[v20.Reference, v30.Reference, v31.Reference] -SchemaType = Union[v20.Schema, v30.Schema, v31.Schema] -v3xSchemaType = Union[v30.Schema, v31.Schema] -DiscriminatorType = Union[v30.Discriminator, v31.Discriminator] -PathItemType = Union[v20.PathItem, v30.PathItem, v31.PathItem] -OperationType = Union[v20.Operation, v30.Operation, v31.Operation] -ParameterType = Union[v20.Parameter, v30.Parameter, v31.Parameter] -HeaderType = Union[v20.Header, v30.Header, v31.Header] -RequestType = Union[v20.Request, v30.Request] -AsyncRequestType = Union[v20.AsyncRequest, v30.AsyncRequest] -MediaTypeType = Union[v30.MediaType, v31.MediaType] -ExpectedType = Union[v20.Response, MediaTypeType] -ResponseHeadersType = dict[str, Union[str, BaseModel, list[BaseModel]]] -ResponseDataType = Union[BaseModel, bytes, str] -TagType = Union[v20.Tag, v30.Tag, v32.Tag] +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 +RequestType = v20.Request | v30.Request +AsyncRequestType = v20.AsyncRequest | v30.AsyncRequest +MediaTypeType = v30.MediaType | v31.MediaType +ExpectedType = v20.Response | MediaTypeType +ResponseHeadersType = dict[str, str | BaseModel | list[BaseModel]] +ResponseDataType = BaseModel, bytes | str +TagType = v20.Tag | v30.Tag | v32.Tag -YAMLLoaderType = Union[type[yaml.Loader], type[yaml.CLoader], type[yaml.SafeLoader], type[yaml.CSafeLoader]] +YAMLLoaderType = type[yaml.Loader] | type[yaml.CLoader] | type[yaml.SafeLoader] | type[yaml.CSafeLoader] -PrimitiveTypes = Union[str, float, int, bool] +PrimitiveTypes = str | float | int | bool HTTPMethodType = Literal["get", "put", "post", "delete", "options", "head", "patch", "trace"] -HTTPMethodMatchType = Union[re.Pattern, HTTPMethodType] +HTTPMethodMatchType = re.Pattern | HTTPMethodType __all__: list[str] = [ - "RootType", - "ServerType", - "SchemaType", - "v3xSchemaType", + "JSON", + "AuthTypes", "DiscriminatorType", - "PathItemType", - "OperationType", - "ParameterType", - "HeaderType", - "RequestType", "ExpectedType", + "HTTPMethodMatchType", + "HTTPMethodType", + "HeaderType", "MediaTypeType", - "ResponseHeadersType", - "ResponseDataType", - "TagType", - "RequestData", - "RequestParameters", - "ReferenceType", + "OperationType", + "ParameterType", + "PathItemType", "PrimitiveTypes", - # - "YAMLLoaderType", + "ReferenceType", # httpx forwards "RequestContent", + "RequestData", + "RequestFileParameter", "RequestFiles", - "AuthTypes", - # - "JSON", "RequestFilesParameter", - "RequestFileParameter", - "HTTPMethodType", - "HTTPMethodMatchType", + "RequestParameters", + "RequestType", + "ResponseDataType", + "ResponseHeadersType", + "RootType", + "SchemaType", + "ServerType", + "TagType", + "YAMLLoaderType", + "v3xSchemaType", ] diff --git a/src/aiopenapi3/base.py b/src/aiopenapi3/base.py index 2beded1f..60efd4b9 100644 --- a/src/aiopenapi3/base.py +++ b/src/aiopenapi3/base.py @@ -1,25 +1,22 @@ -import typing -import warnings -from typing import Any, ForwardRef, Union, cast -from collections.abc import Sequence - -import re import builtins import keyword +import re +import typing import uuid - +import warnings +from collections.abc import Sequence from pathlib import Path +from typing import Any, ForwardRef, TypeGuard, Union, cast -from typing import TypeGuard - -from pydantic import RootModel, BaseModel, TypeAdapter, Field, AnyUrl, model_validator, PrivateAttr, ConfigDict +from pydantic import AnyUrl, BaseModel, ConfigDict, Field, PrivateAttr, RootModel, TypeAdapter, model_validator +from .errors import OperationParameterValidationError, ReferenceResolutionError from .json import JSONPointer, JSONReference -from .errors import ReferenceResolutionError, OperationParameterValidationError if typing.TYPE_CHECKING: from aiopenapi3 import OpenAPI - from ._types import SchemaType, JSON, PathItemType, ParameterType, ReferenceType, DiscriminatorType + + from ._types import JSON, DiscriminatorType, ParameterType, PathItemType, ReferenceType, SchemaType HTTP_METHODS = frozenset(["get", "delete", "head", "post", "put", "patch", "trace", "query"]) @@ -47,7 +44,7 @@ def validate_ObjectExtended_extensions(cls, values): return None if not isinstance(values, dict): return values - e = dict() + e = {} rm = set() for k, v in values.items(): if k.startswith("x-"): @@ -56,7 +53,7 @@ def validate_ObjectExtended_extensions(cls, values): if len(e): for i in rm: del values[i] - if "extensions" in values.keys(): + if "extensions" in values: raise ValueError("extensions") values["extensions"] = e @@ -106,7 +103,7 @@ def replace(ivalue): data[idx] = n elif isinstance(data, dict): - new = dict() + new = {} for _k, _v in data.items(): n = replace(_v) # Swagger 2.0 Schema.ref resolver … if _v != n: @@ -124,13 +121,13 @@ def replace(ivalue): continue # v3.1 - Schema $ref - if isinstance(root, (v20.root.Root, v30.root.Root, v31.root.Root)): - if isinstance(value, SchemaBase): + if isinstance(root, (v20.root.Root, v30.root.Root, v31.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)): + if isinstance(root, (v30.root.Root, v31.root.Root)): # noqa: SIM102 if isinstance(value, (v30.Discriminator, v31.Discriminator)): """ Discriminated Unions - implementing undefined behavior @@ -144,7 +141,7 @@ def replace(ivalue): """ if not value.mapping: - value.mapping = dict() + value.mapping = {} for v in (obj.oneOf or []) + (obj.anyOf or []): k = Path(JSONReference.split(v.ref)[1]).parts[-1] @@ -156,8 +153,8 @@ def replace(ivalue): else: if v._target is None: continue - from .model import Model from . import errors + from .model import Model if "object" not in (t := sorted(Model.types(v._target))): raise errors.SpecError(f"Discriminated Union on a schema with types {t}") @@ -195,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)): + if isinstance(root, (v20.root.Root, v30.root.Root, v31.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) @@ -354,14 +351,10 @@ def __getstate__(self): :return: """ r = BaseModel.__getstate__(self) - try: - for k, v in {"_model_type": None, "_model_types": list()}.items(): - if k in r["__pydantic_private__"]: - r["__pydantic_private__"] = r["__pydantic_private__"].copy() - r["__pydantic_private__"][k] = v - - except Exception: - pass + for k, v in {"_model_type": None, "_model_types": []}.items(): + if k in r["__pydantic_private__"]: + r["__pydantic_private__"] = r["__pydantic_private__"].copy() + r["__pydantic_private__"][k] = v return r def _get_identity(self, prefix="XLS", name=None): @@ -468,8 +461,8 @@ def parameter_in_path(c: Union["ParameterType", "ReferenceType"]) -> TypeGuard[" assert self.parameters is not None assert pi_.parameters is not None - op: frozenset[str] = frozenset(map(lambda x: x.name, filter(parameter_in_path, self.parameters))) - pi: frozenset[str] = frozenset(map(lambda x: x.name, filter(parameter_in_path, pi_.parameters))) + op: frozenset[str] = frozenset(x.name for x in filter(parameter_in_path, self.parameters)) + pi: frozenset[str] = frozenset(x.name for x in filter(parameter_in_path, pi_.parameters)) invalid = sorted(filter(lambda x: re.match(r"^([a-zA-Z0-9\-\._~]+)$", x) is None or len(x) == 0, op | pi)) if invalid: diff --git a/src/aiopenapi3/cli.py b/src/aiopenapi3/cli.py index 3a01e8e6..9dfabb43 100644 --- a/src/aiopenapi3/cli.py +++ b/src/aiopenapi3/cli.py @@ -1,22 +1,21 @@ import argparse +import cProfile import datetime -import sys -import json -import itertools -import typing -from pstats import SortKey -import pstats -import io import importlib.util -import cProfile -import tracemalloc +import io +import itertools +import json import linecache import logging +import pstats +import sys +import tracemalloc +from pstats import SortKey +import httpx2 import jmespath import yaml import yarl -import httpx2 import aiopenapi3.plugin @@ -25,15 +24,13 @@ from pathlib import Path -from .openapi import OpenAPI - -from .loader import ChainLoader, RedirectLoader, WebLoader import aiopenapi3.loader +import aiopenapi3.request from aiopenapi3.v30.formdata import decode_content_type -from .log import init -if typing.TYPE_CHECKING: - import aiopenapi3.request +from .loader import ChainLoader, RedirectLoader, WebLoader +from .log import init +from .openapi import OpenAPI init() @@ -92,13 +89,13 @@ def tm_display_top(snapshot, key_type="lineno", limit=10): ) top_stats = snapshot.statistics(key_type) - print("Top %s lines" % limit) + print(f"Top {limit} lines") for index, stat in enumerate(top_stats[:limit], 1): frame = stat.traceback[0] print(f"#{index}: {frame.filename}:{frame.lineno}: {stat.size / 1024:.1f} KiB") line = linecache.getline(frame.filename, frame.lineno).strip() if line: - print(" %s" % line) + print(f" {line}") other = top_stats[limit:] if other: @@ -119,10 +116,8 @@ def pr_display_top(pr): def schema_display_stats(api, duration): operations = list( itertools.chain.from_iterable( - map( - lambda x: list(filter(lambda x: x, [x.delete, x.get, x.head, x.options, x.patch, x.post, x.put])), - api.paths._paths.values(), - ) + list(filter(lambda x: x, [x.delete, x.get, x.head, x.options, x.patch, x.post, x.put])) + for x in api.paths._paths.values() ) ) print(f"… {duration} (processing time)") @@ -236,7 +231,7 @@ def prepare_arg(value): if auth: api.authenticate(**auth) - req: "aiopenapi3.request.RequestBase" + req: aiopenapi3.request.RequestBase if args.method: req = api.createRequest((args.operationId, args.method)) else: @@ -247,7 +242,7 @@ def prepare_arg(value): req.data.get_type().model_validate(data) try: - headers, ret, response = req.request(parameters=parameters, data=data) + _headers, ret, response = req.request(parameters=parameters, data=data) except aiopenapi3.errors.ResponseSchemaError as e: print(e.response.json()) print(e.response.headers) @@ -274,16 +269,16 @@ def cmd_validate(args: argparse.Namespace) -> None: loader = loader_prepare(args, session_factory) try: - begin = datetime.datetime.now() + begin = datetime.datetime.now(tz=datetime.timezone.utc) try: api = OpenAPI.load_file(args.input, yarl.URL(args.input), plugins=plugins, loader=loader) except aiopenapi3.errors.ReferenceResolutionError as e0: print(f"{e0} {e0.document} {e0.element}") return - end = datetime.datetime.now() + end = datetime.datetime.now(tz=datetime.timezone.utc) duration = end - begin - except ValueError as e: - logg.exception(e) + except ValueError: + logg.exception("error") else: if args.verbose: schema_display_stats(api, duration) diff --git a/src/aiopenapi3/debug.py b/src/aiopenapi3/debug.py index 8346fc12..7b9dfe09 100644 --- a/src/aiopenapi3/debug.py +++ b/src/aiopenapi3/debug.py @@ -1,7 +1,9 @@ -from aiopenapi3.plugin import Document -import yaml -from pathlib import Path import json +from pathlib import Path + +import yaml + +from aiopenapi3.plugin import Document class DescriptionDocumentDumper(Document): @@ -36,7 +38,7 @@ def log_response(response): try: if data := request.read(): print(json.dumps(json.loads(data.decode()), indent=4)) - except Exception as e: + except Exception as e: # noqa: BLE001 print(e) diff --git a/src/aiopenapi3/errors.py b/src/aiopenapi3/errors.py index bd42ae52..07919ed5 100644 --- a/src/aiopenapi3/errors.py +++ b/src/aiopenapi3/errors.py @@ -1,20 +1,20 @@ +import dataclasses import typing from typing import Optional -import dataclasses import httpx2 import pydantic if typing.TYPE_CHECKING: from ._types import ( - SchemaType, - RequestType, + ExpectedType, + HeaderType, + OperationType, RequestData, RequestParameters, + RequestType, + SchemaType, ServerType, - HeaderType, - ExpectedType, - OperationType, ) @@ -79,8 +79,6 @@ class ParameterFormatError(SpecError): The specified parameter encoding is invalid for the parameter family """ - pass - class HTTPError(ErrorBase): pass @@ -204,11 +202,7 @@ def __str__(self): class HTTPClientError(HTTPStatusIndicatedError): """response code 4xx""" - pass - @dataclasses.dataclass(repr=False) class HTTPServerError(HTTPStatusIndicatedError): """response code 5xx""" - - pass diff --git a/src/aiopenapi3/extra/__init__.py b/src/aiopenapi3/extra/__init__.py index 5f3c500f..9dec5359 100644 --- a/src/aiopenapi3/extra/__init__.py +++ b/src/aiopenapi3/extra/__init__.py @@ -1,4 +1,4 @@ -from .reduce import Cull, Reduce from .cookies import Cookies +from .reduce import Cull, Reduce -__all__ = ["Cull", "Reduce", "Cookies"] +__all__ = ["Cookies", "Cull", "Reduce"] diff --git a/src/aiopenapi3/extra/cookies.py b/src/aiopenapi3/extra/cookies.py index c77f228b..6818a520 100644 --- a/src/aiopenapi3/extra/cookies.py +++ b/src/aiopenapi3/extra/cookies.py @@ -1,7 +1,7 @@ -from typing import Literal import email.message import http.cookiejar import urllib.request +from typing import Literal import aiopenapi3.plugin @@ -41,7 +41,7 @@ def info(self) -> email.message.Message: return info def __init__( - self, cookiejar: http.cookiejar.CookieJar = None, policy: Literal["jar", "securitySchemes"] = "jar" + self, cookiejar: http.cookiejar.CookieJar | None = None, policy: Literal["jar", "securitySchemes"] = "jar" ) -> None: """ @@ -55,7 +55,7 @@ def __init__( """ self.cookiejar: http.cookiejar.CookieJar = cookiejar or http.cookiejar.CookieJar() self.policy: Literal["jar", "securitySchemes"] = policy - self.schemes: dict[str, str] = dict() + self.schemes: dict[str, str] = {} if policy not in ["jar", "securitySchemes"]: raise ValueError(f"policy {self.policy} is not a valid policy") diff --git a/src/aiopenapi3/extra/reduce.py b/src/aiopenapi3/extra/reduce.py index d52e790a..4e179b67 100644 --- a/src/aiopenapi3/extra/reduce.py +++ b/src/aiopenapi3/extra/reduce.py @@ -1,15 +1,15 @@ -import typing -from typing import Union import logging import re +import typing +from typing import Union from ..plugin import Document, Init if typing.TYPE_CHECKING: from .._types import HTTPMethodMatchType, PathItemType - PathMatchType = Union[re.Pattern, str] - OperationIdMatchType = Union[re.Pattern, str] + PathMatchType = re.Pattern | str + OperationIdMatchType = re.Pattern | str class Reduce(Document, Init): @@ -35,7 +35,7 @@ def __init__( super().__init__() def _reduced_paths(self, ctx: "Document.Context") -> dict: - reduced: dict[str, dict[str, "PathItemType"]] = {} + reduced: dict[str, dict[str, PathItemType]] = {} if "paths" not in ctx.document: return reduced keep_keys = {"summary", "description", "servers", "parameters"} @@ -55,7 +55,7 @@ def _reduced_paths(self, ctx: "Document.Context") -> dict: reduced[path_key][operation_key] = operation_value elif isinstance(operation, tuple) and len(operation) == 2: pattern, operation_patterns = operation - for path_key in ctx.document["paths"].keys(): + for path_key in ctx.document["paths"]: if (isinstance(pattern, str) and pattern == path_key) or ( isinstance(pattern, re.Pattern) and re.match(pattern, path_key) ): @@ -97,13 +97,13 @@ def initialized(self, ctx: "Init.Context") -> "Init.Context": del ctx.initialized.components.schemas[name] for name, response in list(ctx.initialized.components.responses.items()): - for k, v in response.content.items(): + for v in response.content.values(): if v.schema_._model_type is None: del ctx.initialized.components.responses[name] break for name, requestBody in list(ctx.initialized.components.requestBodies.items()): - for k, v in requestBody.content.items(): + for v in requestBody.content.values(): if v.schema_._model_type is None: del ctx.initialized.components.requestBodies[name] break diff --git a/src/aiopenapi3/loader.py b/src/aiopenapi3/loader.py index 1fe8ed7f..e66553b9 100644 --- a/src/aiopenapi3/loader.py +++ b/src/aiopenapi3/loader.py @@ -1,12 +1,13 @@ import abc +import importlib import logging +import re import typing -import yaml +from typing import ClassVar + import httpx2 +import yaml import yarl -import re - -import importlib # prefer a fast json library here as we may parse large documents for i in ["orjson", "simdjson", "ujson", "json"]: @@ -19,12 +20,14 @@ assert json is not None -from pathlib import Path # noqa:E402 +import functools +import operator +from pathlib import Path from .plugin import Plugins if typing.TYPE_CHECKING: - from ._types import YAMLLoaderType, JSON + from ._types import JSON, YAMLLoaderType log = logging.getLogger("aiopenapi3.loader") @@ -41,8 +44,8 @@ class YAML12Loader(yaml.SafeLoader): add the YAML 1.2 core tags """ - _core_resolvers = [ - ["bool", re.compile(r"""^(?:|true|True|TRUE|false|False|FALSE)$""", re.X), list("tTfF")], + _core_resolvers: ClassVar = [ + ["bool", re.compile(r"""^(?:|true|True|TRUE|false|False|FALSE)$""", re.VERBOSE), list("tTfF")], [ "int", re.compile( @@ -51,7 +54,7 @@ class YAML12Loader(yaml.SafeLoader): |[-+]?(?:[0-9]+) |0x[0-9a-fA-F]+ )$""", - re.X, + re.VERBOSE, ), list("-+0123456789"), ], @@ -61,11 +64,11 @@ class YAML12Loader(yaml.SafeLoader): r"""^(?:[-+]?(?:\.[0-9]+|[0-9]+(\.[0-9]*)?)(?:[eE][-+]?[0-9]+)? |[-+]?\.(?:inf|Inf|INF) |\.(?:nan|NaN|NAN))$""", - re.X, + re.VERBOSE, ), list("-+0123456789."), ], - ["null", re.compile(r"""^(?:~||null|Null|NULL)$""", re.X), ["~", "n", "N", ""]], + ["null", re.compile(r"""^(?:~||null|Null|NULL)$""", re.VERBOSE), ["~", "n", "N", ""]], ] """ core tags from @@ -75,7 +78,9 @@ class YAML12Loader(yaml.SafeLoader): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) tags = set( - sum(list(map(lambda x: list(map(lambda y: y[0], x)), YAML12Loader.yaml_implicit_resolvers.values())), []) + functools.reduce( + operator.iadd, [[y[0] for y in x] for x in YAML12Loader.yaml_implicit_resolvers.values()], [] + ) ) for tag in tags: YAML12Loader.remove_implicit_resolver(tag) @@ -166,11 +171,11 @@ def parse(self, plugins: Plugins, url: yarl.URL, data: str): if file.suffix not in (".yaml", ".json"): try: return self.parse(plugins, url.with_path("/test.yaml"), data) - except Exception as e: + except Exception as e: # noqa: BLE001, S110 pass try: return self.parse(plugins, url.with_path("/test.json"), data) - except Exception as e: + except Exception as e: # noqa: BLE001, S110 pass if file.suffix == ".yaml": @@ -294,7 +299,7 @@ def load(self, plugins: "Plugins", url: yarl.URL, codec: str | None = None): r = i.load(plugins, url, codec) log.debug(f"using {i}") return r - except Exception as exc: + except Exception as exc: # noqa: BLE001 errors.append((i, str(exc))) for l, e in errors: log.debug(f"{l} {e}") diff --git a/src/aiopenapi3/log.py b/src/aiopenapi3/log.py index 78b5cd11..3428512d 100644 --- a/src/aiopenapi3/log.py +++ b/src/aiopenapi3/log.py @@ -1,9 +1,8 @@ -import sys import logging.config import os -from typing import Any - +import sys from pathlib import Path +from typing import Any handlers: list[str] | None = None @@ -78,7 +77,7 @@ def init(force: bool = False) -> None: for i in frozenset(config["handlers"].keys()) - frozenset(handlers): del config["handlers"][i] - for i in frozenset(config["formatters"]) - frozenset(map(lambda x: x["formatter"], config["handlers"].values())): + for i in frozenset(config["formatters"]) - frozenset(x["formatter"] for x in config["handlers"].values()): del config["formatters"][i] logging.config.dictConfig(config) diff --git a/src/aiopenapi3/model.py b/src/aiopenapi3/model.py index 15a9779e..1c9be864 100644 --- a/src/aiopenapi3/model.py +++ b/src/aiopenapi3/model.py @@ -4,21 +4,18 @@ import logging import re import sys -from typing import Any, cast, TypeVar import typing +from typing import Annotated, Any, ClassVar, Literal, Optional, TypeGuard, TypeVar, Union, cast -from typing import TypeGuard - -from typing import Optional, Union, Annotated, Literal -from pydantic import BaseModel, Field, RootModel, ConfigDict import pydantic +from pydantic import BaseModel, ConfigDict, Field, RootModel -from .base import ReferenceBase, SchemaBase from . import me -from .pydanticv2 import field_class_to_schema, create_model +from .base import ReferenceBase, SchemaBase +from .pydanticv2 import create_model, field_class_to_schema if typing.TYPE_CHECKING: - from ._types import SchemaType, ReferenceType, DiscriminatorType + from ._types import DiscriminatorType, ReferenceType, SchemaType type_format_to_class: dict[str, dict[str | None, type]] = collections.defaultdict(dict) @@ -42,7 +39,6 @@ def generate_type_format_to_class(): initialize type_format_to_class :return: None """ - global type_format_to_class for cls, spec in field_class_to_schema: if "type" not in spec: # FIXME Decimal is anyOf now @@ -76,6 +72,9 @@ def class_from_schema(s, _type): return b +import functools +import operator + import pydantic_core @@ -84,9 +83,7 @@ class ConfiguredRootModel(RootModel): def is_basemodel(m) -> bool: - if inspect.isclass(m) and issubclass(m, pydantic.BaseModel): - return True - return False + return bool(inspect.isclass(m) and issubclass(m, pydantic.BaseModel)) if sys.version_info < (3, 11): @@ -96,9 +93,7 @@ def is_basemodel(m) -> bool: if isinstance(m, typing.GenericAlias): return False - if inspect.isclass(m) and issubclass(m, pydantic.BaseModel): - return True - return False + return bool(inspect.isclass(m) and issubclass(m, pydantic.BaseModel)) @dataclasses.dataclass @@ -123,7 +118,7 @@ def validate(self): @property def fields(self): - r = list() + r = [] for k, v in self.properties.items(): r.append((k, (v.annotation, v.default))) return dict(r) @@ -142,7 +137,7 @@ def createFields(self, schema: "SchemaType", overwrite=False): ): continue - args: dict[str, Any] = dict() + args: dict[str, Any] = {} assert schema.required is not None if (v := getattr(f, "default", None)) is not None: args["default"] = v @@ -180,7 +175,7 @@ def _createAnnotations( if _type == "array": v = Model.createAnnotation(schema) if Model.is_nullable(schema): - v = Optional[v] # type: ignore[assignment] + v = Optional[v] # noqa: UP045 # type: ignore[assignment] self.root = v elif _type == "object": if ( @@ -206,7 +201,7 @@ def _createAnnotations( """ v = dict[str, Model.createAnnotation(schema.additionalProperties)] # type: ignore[misc,index] if Model.is_nullable(schema): - v = Optional[v] # type: ignore[assignment] + v = Optional[v] # noqa: UP045 # type: ignore[assignment] self.root = v else: assert schema.properties is not None @@ -219,12 +214,11 @@ def _createAnnotations( if typing.get_origin(r) == Literal: canbenull = False - if canbenull: - if getattr(f, "const", None) is None: - """not const""" - if name not in schema.required or Model.is_nullable(f): - """not required - or nullable""" - r = Optional[r] # type: ignore[assignment] + if canbenull and getattr(f, "const", None) is None: + """not const""" + if name not in schema.required or Model.is_nullable(f): + """not required - or nullable""" + r = Optional[r] # noqa: UP045 # type: ignore[assignment] self.properties[Model.nameof(name)].annotation = r @@ -232,7 +226,6 @@ def _createAnnotations( pass else: raise ValueError() - return def model(self) -> type[BaseModel] | type[None]: if self.root: @@ -251,7 +244,7 @@ def model(self) -> type[BaseModel] | type[None]: @classmethod def collapse(cls, schema: "SchemaType", items: list["_ClassInfo"]) -> type[BaseModel]: - r: list[type[BaseModel] | type[None]] + r: list[type[BaseModel | None]] r = [i.model() for i in items] type_name = schema._get_identity("L8") @@ -279,7 +272,7 @@ def _follow(r: "ReferenceType", t: type[_T]) -> TypeGuard[_T]: class Model: # (BaseModel): - ALIASES: dict[str, str] = dict() + ALIASES: ClassVar[dict[str, str]] = {} @classmethod def from_schema( @@ -295,12 +288,12 @@ def from_schema( if discriminators is None: discriminators = [] - r: list[_ClassInfo] = list() + r: list[_ClassInfo] = [] types: list[str] = list(Model.types(schema)) multi: bool = len(types) > 1 for _type in types: - args = dict() if multi else None + args = {} if multi else None """ for schema with multiple types, the default value needs to be attached to the RootModel providing empty args creates a FieldInfo without a default value for the subtypes @@ -319,7 +312,7 @@ def createClassInfo( schemanames: list[str], discriminators: list["DiscriminatorType"], extra: list["SchemaType"] | None, - args: dict[str, Any] = None, + args: dict[str, Any] | None = None, ) -> _ClassInfo: from . import v20, v30, v31 @@ -411,7 +404,7 @@ def get_patternProperty(self_, item): def mkx(): def get_patternProperties(self_): patterns = typing.get_args(self_.aio3_patternProperty.__annotations__["item"]) - r = {k: list() for k in patterns} + r = {k: [] for k in patterns} for name, value in self_.model_extra.items(): for pattern in patterns: if re.match(pattern, name): @@ -431,7 +424,7 @@ def get_patternProperties(self_): def mkx(): def validate_patternProperties(self_): patterns = typing.get_args(self_.aio3_patternProperty.__annotations__["item"]) - for name, value in self_.model_extra.items(): + for name in self_.model_extra: for pattern in patterns: if re.match(pattern, name): break @@ -464,9 +457,8 @@ def validate_patternProperties(self_): else: raise ValueError(_type) - if _type in ("array", "object"): - if schema.enum or getattr(schema, "const", None): - raise NotImplementedError("complex enums/const are not supported") + if _type in ("array", "object") and (schema.enum or getattr(schema, "const", None)): + raise NotImplementedError("complex enums/const are not supported") classinfo.config = Model.createConfigDict(schema) @@ -533,7 +525,7 @@ def createAnnotation( Required, can be None: Optional[str] Not required, can be None, is … by default: f4: Optional[str] = … """ - r: list[type] = list() + r: list[type] = [] rr: type if (v := getattr(schema, "const", None)) is not None: """ @@ -547,37 +539,37 @@ def createAnnotation( _names = tuple(filter(lambda x: x, _names)) r = [Literal[_names]] # type: ignore[assignment,list-item] else: - for _type in Model.types(schema) if not _type else [_type]: - if _type in ("boolean", "integer", "number", "string"): - oneOf = [i for i in getattr(schema, "oneOf", []) if _type in Model.types(i)] - anyOf = [i for i in getattr(schema, "anyOf", []) if _type in Model.types(i)] - allOf = [i for i in getattr(schema, "allOf", []) if _type in Model.types(i)] + for _t in Model.types(schema) if not _type else [_type]: + if _t in ("boolean", "integer", "number", "string"): + oneOf = [i for i in getattr(schema, "oneOf", []) if _t in Model.types(i)] + anyOf = [i for i in getattr(schema, "anyOf", []) if _t in Model.types(i)] + allOf = [i for i in getattr(schema, "allOf", []) if _t in Model.types(i)] if not (anyOf or oneOf or allOf): - v = class_from_schema(schema, _type) + v = class_from_schema(schema, _t) r.append(v) else: - v = [Model.createAnnotation(i, _type=_type) for i in oneOf] + v = [Model.createAnnotation(i, _type=_t) for i in oneOf] r.extend(v) - v = [Model.createAnnotation(i, _type=_type) for i in anyOf] + v = [Model.createAnnotation(i, _type=_t) for i in anyOf] r.extend(v) - v = [Model.createAnnotation(i, _type=_type) for i in allOf] + v = [Model.createAnnotation(i, _type=_t) for i in allOf] r.extend(v) - elif _type == "array": + elif _t == "array": r.extend( - list( - Model.createAnnotation(i, _type=_type) + [ + Model.createAnnotation(i, _type=_t) for i in getattr(schema, "oneOf", []) - if Model.is_type(i, _type) - ) + if Model.is_type(i, _t) + ] ) r.extend( - list( - Model.createAnnotation(i, _type=_type) + [ + Model.createAnnotation(i, _type=_t) for i in getattr(schema, "anyOf", []) - if Model.is_type(i, _type) - ) + if Model.is_type(i, _t) + ] ) if isinstance(schema.items, list): @@ -595,12 +587,12 @@ def createAnnotation( else: raise TypeError(schema.items) r.append(v) # type: ignore[arg-type] - elif _type == "object": + elif _t == "object": r.append(schema.get_type(fwdref=fwdref)) - elif _type == "null": + elif _t == "null": nullable = True else: - raise ValueError(_type) + raise ValueError(_t) if len(r) == 1: rr = r[0] @@ -609,7 +601,7 @@ def createAnnotation( else: rr = None # type: ignore[assignment] if nullable is True: - rr = Optional[rr] # type: ignore[assignment] + rr = rr | None # type: ignore[assignment] elif isinstance(schema, ReferenceBase): rr = Model.createAnnotation(schema._target, fwdref=True) else: @@ -652,26 +644,26 @@ def types(schema: "SchemaType") -> typing.Generator[str, None, None]: ) # allOf - intersection of types - allOfs: list["SchemaType"] - if allOfs := sum([getattr(schema, "allOf", [])], []): + allOfs: list[SchemaType] + if allOfs := functools.reduce(operator.iadd, [getattr(schema, "allOf", [])], []): for x in allOfs: allOf &= set(Model.types(x)) # anyOf - union of types - anyOfs: list["SchemaType"] - if anyOfs := sum([getattr(schema, "anyOf", [])], []): + anyOfs: list[SchemaType] + if anyOfs := functools.reduce(operator.iadd, [getattr(schema, "anyOf", [])], []): anyOf = set.union(*[set(Model.types(x)) for x in anyOfs]) if anyOfs else set() # oneOf - union of types - oneOfs: list["SchemaType"] - if oneOfs := sum([getattr(schema, "oneOf", [])], []): + oneOfs: list[SchemaType] + if oneOfs := functools.reduce(operator.iadd, [getattr(schema, "oneOf", [])], []): oneOf = set.union(*[set(Model.types(x)) for x in oneOfs]) if oneOfs else set() if allOfs or anyOfs or oneOfs: tmp = oneOf & allOf & anyOf typesfilter |= tmp else: - raise StopIteration + return if typesfilter: values = values & typesfilter @@ -728,7 +720,7 @@ def booleanFalse(schema: Optional["SchemaType"]) -> bool: @staticmethod def createField(schema: "SchemaType", _type=None, args=None) -> Field: if args is None: - args = dict(default=getattr(schema, "default", None)) + args = {"default": getattr(schema, "default", None)} # """ # readOnly & writeOnly are Optional default None @@ -814,10 +806,7 @@ def nameof(name: str, args=None): if name.startswith("model_"): rename = f"x{name}" - try: - rename = re.sub(r"[#@\.-]", "_", rename) - except Exception as e: - print(e) + rename = re.sub(r"[#@\.-]", "_", rename) if rename[0] == "_": rename = rename.lstrip("_") + "_" diff --git a/src/aiopenapi3/openapi.py b/src/aiopenapi3/openapi.py index e43e67f5..8ea5629a 100644 --- a/src/aiopenapi3/openapi.py +++ b/src/aiopenapi3/openapi.py @@ -1,49 +1,38 @@ -import typing - -from typing import Any, Union, cast, Optional, ForwardRef -from collections.abc import Callable -import logging import copy +import logging +import pathlib import pickle import random - -import pathlib - - -from typing import TypeGuard - +import typing +from collections.abc import Callable +from typing import Any, ForwardRef, Optional, TypeGuard, cast import httpx2 import yarl from pydantic import BaseModel -from aiopenapi3.v30.general import Reference import aiopenapi3.request +from aiopenapi3.v30.general import Reference + +from . import log, v20, v30, v31, v32 +from .base import DiscriminatorBase, ReferenceBase, RootBase, SchemaBase +from .errors import HTTPClientError, HTTPServerError, ReferenceResolutionError from .json import JSONReference -from . import v20 -from . import v30 -from . import v31 -from . import v32 -from . import log -from .request import OperationIndex, HTTP_METHODS -from .errors import ReferenceResolutionError, HTTPClientError, HTTPServerError from .loader import Loader, NullLoader +from .model import Model, is_basemodel from .plugin import Plugin, Plugins -from .base import RootBase, ReferenceBase, SchemaBase, DiscriminatorBase -from .request import RequestBase +from .request import HTTP_METHODS, OperationIndex, RequestBase from .v30.paths import Operation -from .model import is_basemodel, Model - if typing.TYPE_CHECKING: from ._types import ( - RootType, JSON, - PathItemType, - SchemaType, + HTTPMethodType, OperationType, + PathItemType, RequestType, - HTTPMethodType, + RootType, + SchemaType, ServerType, ) @@ -54,9 +43,7 @@ def has_components(y: Optional["RootType"]) -> TypeGuard[v30.Root | v31.Root]: # return all([y, y.components]) if y is None: return False - if y.components is None: - return False - return True + return y.components is not None def is_schema(v: tuple[str, "SchemaType"]) -> TypeGuard["SchemaType"]: @@ -262,7 +249,7 @@ def __init__( Loader - loading referenced documents """ - self._createRequest: Callable[["OpenAPI", str, str, "OperationType", list["ServerType"] | None], "RequestBase"] + self._createRequest: Callable[[OpenAPI, str, str, OperationType, list[ServerType] | None], RequestBase] """ creates the Async/Request for the protocol required """ @@ -280,23 +267,23 @@ def __init__( Raise for http status code """ - self._security: dict[str, tuple[str]] = dict() + self._security: dict[str, tuple[str]] = {} """ authorization informations e.g. {"BasicAuth": ("user","secret")} """ - self._documents: dict[yarl.URL, "RootType"] = dict() + self._documents: dict[yarl.URL, RootType] = {} """ the related documents """ - self._server_variables: dict[str, str] = dict() + self._server_variables: dict[str, str] = {} """ server variable mapping """ - self._server_select: Callable[[list["ServerType"]], "ServerType"] = random.choice + self._server_select: Callable[[list[ServerType]], ServerType] = random.choice self._init_plugins(plugins) """ @@ -369,7 +356,6 @@ def _init_references(self): e.document = names[i] raise processed = set(values.keys()) - return # for i in self._documents.values(): # i._resolve_references(self) @@ -380,10 +366,10 @@ def _init_operationindex(self, use_operation_tags: bool) -> bool: if isinstance(self._root, v20.Root): if self.paths: - obj: "PathItemType" + obj: PathItemType for path, obj in self.paths.items(): for m in obj.model_fields_set & HTTP_METHODS: - op: "Operation" = getattr(obj, m) + op: Operation = getattr(obj, m) op._validate_path_parameters(obj, path, (m, cast(str, op.operationId))) if op.operationId is None: continue @@ -408,7 +394,7 @@ def _init_operationindex(self, use_operation_tags: bool) -> bool: for schemas in allschemas: name: str - schema: "SchemaType" + schema: SchemaType for name, schema in filter(is_schema, schemas.items()): schema._get_identity(name=name, prefix="OP") @@ -438,9 +424,9 @@ def _init_operationindex(self, use_operation_tags: bool) -> bool: elif isinstance(self._root, v32.Root): self._root.paths = v32.Paths(paths={}, extensions={}) else: - raise ValueError(self._root) + raise TypeError(self._root) else: - raise ValueError(self._root) + raise TypeError(self._root) self._operationindex = OperationIndex(self, use_operation_tags) return p is None @@ -453,7 +439,7 @@ def _get_combined_attributes(schema): return ( getattr(schema, "oneOf", []) # Swagger compat + ( - list(getattr(schema, "discriminator").mapping.values()) + list(schema.discriminator.mapping.values()) if isinstance(getattr(schema, "discriminator", {}), DiscriminatorBase) else [] ) @@ -492,7 +478,7 @@ def _iterate_schemas(cls, schemas: dict[int, "SchemaType"], next_set: set[int], return processed def _init_schema_types_collect(self, only_required: bool) -> dict[str, "SchemaType"]: - byname: dict[str, "SchemaType"] = dict() + byname: dict[str, SchemaType] = {} def is_schema(v: tuple[str, "SchemaType"]) -> bool: return isinstance(v[1], (v20.Schema, v30.Schema, v31.Schema)) @@ -502,7 +488,7 @@ def is_schema(v: tuple[str, "SchemaType"]) -> bool: documents = cast(list[v20.Root], self._documents.values()) # Schema if only_required is False: - for byid in map(lambda x: x.definitions, documents): + for byid in (x.definitions for x in documents): assert byid is not None and isinstance(byid, dict) for name, schema in filter(is_schema, byid.items()): n = schema._get_identity(name=name) @@ -510,7 +496,7 @@ def is_schema(v: tuple[str, "SchemaType"]) -> bool: byname[n] = schema # PathItems - for path, obj in (self.paths or dict()).items(): + for path, obj in (self.paths or {}).items(): for m in obj.model_fields_set & HTTP_METHODS: op = getattr(obj, m) @@ -526,7 +512,7 @@ def is_schema(v: tuple[str, "SchemaType"]) -> bool: raise TypeError(f"{type(response)} at {path}") # Response - for byid in map(lambda x: x.responses, documents): + for byid in (x.responses for x in documents): assert byid is not None and isinstance(byid, dict) for name, response in filter(is_schema, byid.items()): assert response.schema_ @@ -536,11 +522,11 @@ def is_schema(v: tuple[str, "SchemaType"]) -> bool: elif isinstance(self._root, (v30.Root, v31.Root)): # Schema - documents = cast(Union[list[v30.Root], list[v31.Root]], self._documents.values()) + documents = cast(list[v30.Root] | list[v31.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: - for byid in map(lambda x: x.schemas, components): + for byid in (x.schemas for x in components): assert byid is not None and isinstance(byid, dict) for name, schema in filter(is_schema, byid.items()): n = schema._get_identity(name=name) @@ -548,7 +534,7 @@ def is_schema(v: tuple[str, "SchemaType"]) -> bool: byname[n] = schema # PathItems - for path, obj in (self.paths or dict()).items(): + for path, obj in (self.paths or {}).items(): for m in obj.model_fields_set & HTTP_METHODS: op = getattr(obj, m) @@ -599,9 +585,9 @@ def is_schema(v: tuple[str, "SchemaType"]) -> bool: # Response if only_required is False: - for responses in map(lambda x: x.responses, components): + for responses in (x.responses for x in components): assert responses is not None - for rname, response in responses.items(): + for response in responses.values(): for mt, mto in response.content.items(): if mto.schema_ is None: continue @@ -613,18 +599,18 @@ def is_schema(v: tuple[str, "SchemaType"]) -> bool: return byname def _init_schema_types(self, only_required: bool) -> None: - byname: dict[str, "SchemaType"] = self._init_schema_types_collect(only_required) - byid: dict[int, "SchemaType"] = {id(i): i for i in byname.values()} + byname: dict[str, SchemaType] = self._init_schema_types_collect(only_required) + byid: dict[int, SchemaType] = {id(i): i for i in byname.values()} data: set[int] = set(byid.keys()) todo: set[int] = self._iterate_schemas(byid, data, set()) - types: dict[str, ForwardRef | type[BaseModel] | type[int] | type[str] | type[float] | type[bool]] = dict() + types: dict[str, type[BaseModel | int | str | float | bool] | ForwardRef] = {} """ Due to Plugins (e.g. Cull/Reduce) byname may be incomplete """ - resolved: list["SchemaType"] = list( - map(lambda x: byid[x]._target if isinstance(byid[x], ReferenceBase) else byid[x], todo | data) - ) + resolved: list[SchemaType] = [ + byid[x]._target if isinstance(byid[x], ReferenceBase) else byid[x] for x in todo | data + ] self.plugins.init.resolved(initialized=self._root, resolved=resolved) # print(f"{len(todo | data)} {only_required=}") @@ -659,8 +645,8 @@ def _init_schema_types(self, only_required: bool) -> None: for v in byid[id(thes)]._model_types: assert v.__name__ in types, v.__name__ v.model_rebuild(_types_namespace={"__types": types}) - except Exception as e: - raise e + except Exception: # noqa: TRY203 + raise @property def url(self) -> yarl.URL: @@ -688,7 +674,7 @@ def url(self) -> yarl.URL: return r elif isinstance(self._root, (v30.Root, v31.Root, v32.Root)): assert self._root.servers - server: "ServerType" = self._server_select(self._root.servers) + server: ServerType = self._server_select(self._root.servers) return self._base_url.join(yarl.URL(server.createUrl(self._server_variables))) def authenticate(self, *args, **kwargs): @@ -700,7 +686,7 @@ def authenticate(self, *args, **kwargs): :param kwargs: scheme=value """ if len(args) == 1 and args[0] is None: - self._security = dict() + self._security = {} schemes = frozenset(kwargs.keys()) @@ -709,7 +695,7 @@ def authenticate(self, *args, **kwargs): elif isinstance(self._root, (v30.Root, v31.Root)): v = schemes - frozenset(SecuritySchemes := self._root.components.securitySchemes) else: - raise TypeError(self._root) # noqa + raise TypeError(self._root) if v: raise ValueError(f"{self.info.title} does not accept security schemes {sorted(v)}") @@ -755,8 +741,8 @@ def createRequest(self, operationId: str | tuple[str, "HTTPMethodType"]) -> "Req :return: the returned Request is either :class:`aiopenapi3.request.RequestBase` or - in case of a httpx2.AsyncClient session_factory - :class:`aiopenapi3.request.AsyncRequestBase` """ - operation: Optional["OperationType"] = None - request: Optional["RequestType"] = None + operation: OperationType | None = None + request: RequestType | None = None try: if isinstance(operationId, str): *tags, opn = operationId.split(".") diff --git a/src/aiopenapi3/plugin.py b/src/aiopenapi3/plugin.py index b5f2acee..5032e291 100644 --- a/src/aiopenapi3/plugin.py +++ b/src/aiopenapi3/plugin.py @@ -1,18 +1,15 @@ -import dataclasses -from typing import TYPE_CHECKING, Any, Optional import abc - -from typing import TypeGuard - - -from pydantic import BaseModel +import dataclasses +from typing import TYPE_CHECKING, Any, ClassVar, Optional, TypeGuard import yarl +from pydantic import BaseModel if TYPE_CHECKING: + import httpx2 + from aiopenapi3 import OpenAPI - import httpx2 from .base import PathItemBase, SchemaBase from .request import RequestBase @@ -26,7 +23,7 @@ class Plugin(abc.ABC): class Context: ... def __init__(self) -> None: - self._api: Optional["OpenAPI"] = None + self._api: OpenAPI | None = None @property def api(self): @@ -53,19 +50,19 @@ class Context: def schemas(self, ctx: "Init.Context") -> "Init.Context": # pragma: no cover """modify the Schema before creating Models""" - return ctx # noqa + return ctx def resolved(self, ctx: "Init.Context") -> "Init.Context": # pragma: no cover """modify the resolved paths/PathItems before initializing the Operations""" - return ctx # noqa + return ctx def paths(self, ctx: "Init.Context") -> "Init.Context": # pragma: no cover """modify the paths/PathItems before initializing the Operations""" - return ctx # noqa + return ctx def initialized(self, ctx: "Init.Context") -> "Init.Context": # pragma: no cover """it is initialized""" - return ctx # noqa + return ctx class Document(Plugin): @@ -82,11 +79,11 @@ class Context: def loaded(self, ctx: "Document.Context") -> "Document.Context": # pragma: no cover """modify the text before parsing""" - return ctx # noqa + return ctx def parsed(self, ctx: "Document.Context") -> "Document.Context": # pragma: no cover """modify the parsed dict before …""" - return ctx # noqa + return ctx class Message(Plugin): @@ -131,31 +128,31 @@ def marshalled(self, ctx: "Message.Context") -> "Message.Context": # pragma: no """ modify the dict before sending """ - return ctx # noqa + return ctx def sending(self, ctx: "Message.Context") -> "Message.Context": # pragma: no cover """ modify the text before sending """ - return ctx # noqa + return ctx def received(self, ctx: "Message.Context") -> "Message.Context": # pragma: no cover """ modify the received text """ - return ctx # noqa + return ctx def parsed(self, ctx: "Message.Context") -> "Message.Context": # pragma: no cover """ modify the parsed dict structure """ - return ctx # noqa + return ctx def unmarshalled(self, ctx: "Message.Context") -> "Message.Context": # pragma: no cover """ modify the object """ - return ctx # noqa + return ctx class Domain: @@ -193,7 +190,7 @@ def __call__(self, **kwargs): class Plugins: - _domains: dict[str, type[Plugin]] = {"init": Init, "document": Document, "message": Message} + _domains: ClassVar[dict[str, type[Plugin]]] = {"init": Init, "document": Document, "message": Message} def __init__(self, plugins: list[Plugin]): for p in plugins: @@ -206,7 +203,7 @@ def __init__(self, plugins: list[Plugin]): def _get_domain(self, name: str, plugins: list[Plugin]) -> "Domain": domain: type[Plugin] | None if (domain := self._domains.get(name)) is None: - raise ValueError(name) # noqa + raise ValueError(name) def domain_type_f(p: Plugin) -> TypeGuard[Plugin]: return isinstance(p, domain) diff --git a/src/aiopenapi3/pydanticv2.py b/src/aiopenapi3/pydanticv2.py index 2a2af8cf..22df0395 100644 --- a/src/aiopenapi3/pydanticv2.py +++ b/src/aiopenapi3/pydanticv2.py @@ -1,9 +1,9 @@ +from datetime import date, datetime, time, timedelta from decimal import Decimal -from datetime import datetime, date, time, timedelta -from ipaddress import IPv4Network, IPv6Network, IPv4Interface, IPv6Interface, IPv4Address, IPv6Address +from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network from pathlib import Path -from typing import Any from re import Pattern +from typing import Any from uuid import UUID from pydantic import TypeAdapter @@ -39,15 +39,16 @@ (field_class, TypeAdapter(field_class).json_schema()) for field_class in field_classes_to_support ) -from pydantic import ConfigDict, BaseModel, PydanticUserError -from pydantic.main import ModelT -from typing import cast -from collections.abc import Callable import sys import types +from collections.abc import Callable +from typing import cast + +from pydantic import BaseModel, ConfigDict, PydanticUserError +from pydantic.main import ModelT -def create_model( # noqa: C901 +def create_model( model_name: str, /, *, diff --git a/src/aiopenapi3/request.py b/src/aiopenapi3/request.py index 7c2ee7dd..448f1e10 100644 --- a/src/aiopenapi3/request.py +++ b/src/aiopenapi3/request.py @@ -1,14 +1,12 @@ import abc import collections import contextlib -import typing import json import logging -from contextlib import closing +import typing +from collections.abc import AsyncGenerator, AsyncIterator, Generator, Iterator +from contextlib import aclosing, closing from typing import Any, NamedTuple, Optional, Union, cast -from collections.abc import AsyncIterator, AsyncGenerator, Generator -from collections.abc import Iterator -from contextlib import aclosing import httpx2 import pydantic @@ -16,32 +14,32 @@ from aiopenapi3.errors import ContentLengthExceededError - from .base import HTTP_METHODS, ReferenceBase +from .errors import OperationIdDuplicationError, RequestError from .version import __version__ -from .errors import RequestError, OperationIdDuplicationError if typing.TYPE_CHECKING: + from aiopenapi3 import OpenAPI + from ._types import ( - RequestParameters, - RequestData, - RequestFiles, - RequestContent, - RequestType, + JSON, AuthTypes, - SchemaType, + HTTPMethodType, + OperationType, ParameterType, PathItemType, - OperationType, - JSON, - RootType, - ServerType, + RequestContent, + RequestData, + RequestFiles, + RequestParameters, + RequestType, ResponseDataType, ResponseHeadersType, - HTTPMethodType, + RootType, + SchemaType, + ServerType, TagType, ) - from aiopenapi3 import OpenAPI log = logging.getLogger("aiopenapi3.request") @@ -49,14 +47,14 @@ class RequestParameter: def __init__(self, url: yarl.URL | str): self.url: str = str(url) - self.auth: Optional["AuthTypes"] = None + self.auth: AuthTypes | None = None self.cookies: dict[str, str] = {} # self.path = {} self.params: dict[str, str] = {} - self.content: Optional["RequestContent"] = None + self.content: RequestContent | None = None self.headers: dict[str, str] = {} self.data: dict[str, str] = {} # form-data - self.files: Optional["RequestFiles"] = {} # form-data files + self.files: RequestFiles | None = {} # form-data files self.cert: Any = None @@ -70,7 +68,7 @@ class StreamResponse(NamedTuple): class Sequencer: def __init__(self, headers: "ResponseHeadersType", stream: Iterator["JSON"], model: pydantic.BaseModel) -> None: self.headers: ResponseHeadersType = headers - self.stream: Iterator["JSON"] = stream + self.stream: Iterator[JSON] = stream self.model = model def __iter__(self) -> Iterator: @@ -115,7 +113,7 @@ def __init__( operation: "OperationType", servers: list["ServerType"] | None, ): - self.api: "OpenAPI" = api + self.api: OpenAPI = api """ OpenAPI object """ @@ -125,7 +123,7 @@ def __init__( API document root """ - self.method: "HTTPMethodType" = method + self.method: HTTPMethodType = method """ HTTP method """ @@ -135,12 +133,12 @@ def __init__( HTTP path """ - self.vars: Optional["RequestBase.Vars"] = None + self.vars: RequestBase.Vars | None = None """ Parameter & Data """ - self.operation: "OperationType" = operation + self.operation: OperationType = operation """ associated OpenAPI Operation """ @@ -150,7 +148,7 @@ def __init__( RequestParameter """ - self.servers: list["ServerType"] | None = servers + self.servers: list[ServerType] | None = servers """ Servers to use for this request """ @@ -164,7 +162,7 @@ def __call__( :param kwargs: :return: body or (header, body) """ - headers, data, result = self.request(*args, context=context, **kwargs) # type: ignore[misc] + headers, data, _result = self.request(*args, context=context, **kwargs) # type: ignore[misc] if return_headers: return headers, data return data @@ -221,7 +219,7 @@ def _build_req(self, session: httpx2.Client | httpx2.AsyncClient) -> httpx2.Requ url: yarl.URL = self.api.url if self.servers: - server: "ServerType" = self.api._server_select(self.servers) + server: ServerType = self.api._server_select(self.servers) url = self.api._base_url.join(yarl.URL(server.createUrl(self.api._server_variables))) req = session.build_request( @@ -352,7 +350,7 @@ def iter_json(response: httpx2.Response) -> Iterator["JSON"]: if not data_.endswith(("\r\r", "\n\n", "\r\n\r\n")): continue - v = dict() + v = {} for l in data_.splitlines(keepends=False): if l == "": continue @@ -423,8 +421,8 @@ class Sequencer: def __init__( self, headers: "ResponseHeadersType", stream: AsyncIterator["JSON"], model: pydantic.BaseModel ) -> None: - self.headers: "ResponseHeadersType" = headers - self.stream: AsyncIterator["JSON"] = stream + self.headers: ResponseHeadersType = headers + self.stream: AsyncIterator[JSON] = stream self.model = model def __aiter__(self) -> AsyncIterator: @@ -440,7 +438,7 @@ async def __anext__(self) -> pydantic.BaseModel: async def __call__( # type: ignore[override] self, *args, return_headers: bool = False, context: Any = None, **kwargs ) -> Union["JSON", tuple[dict[str, str], "JSON"]]: - headers, data, result = await self.request(*args, context=context, **kwargs) # type: ignore [misc] + headers, data, _result = await self.request(*args, context=context, **kwargs) # type: ignore [misc] if return_headers: return headers, data return data @@ -452,7 +450,7 @@ async def _send( try: result = await session.send(req, stream=True) except Exception as e: - raise RequestError(self.operation, self, data, parameters or dict()) from e + raise RequestError(self.operation, self, data, parameters or {}) from e return result async def request( # type: ignore[override] @@ -543,7 +541,7 @@ async def aiter_json(response: httpx2.Response) -> AsyncIterator["JSON"]: if not data_.endswith(("\r\r", "\n\n", "\r\n\r\n")): continue - v = dict() + v = {} for l in data_.splitlines(keepends=False): if l == "": continue @@ -591,10 +589,8 @@ class OperationIndex: class OperationTag: def __init__(self, oi: "OperationIndex") -> None: self._oi = oi - self._operations: dict[str, tuple["HTTPMethodType", str, "OperationType", list["ServerType"] | None]] = ( - dict() - ) - self._tags: dict[str, "OperationIndex.OperationTag"] = dict() + self._operations: dict[str, tuple[HTTPMethodType, str, OperationType, list[ServerType] | None]] = {} + self._tags: dict[str, OperationIndex.OperationTag] = {} def __getattr__(self, item) -> RequestBase: if item in self._operations: @@ -607,9 +603,9 @@ class Iter: def __init__(self, api: "OpenAPI", use_operation_tags: bool): self.operations = [] self.r: Iterator[int] - pi: "PathItemType" - for path, pi in api.paths.items(): - op: "OperationType" + pi: PathItemType + for pi in api.paths.values(): + op: OperationType if pi.ref: # pi = pi.ref._target pi = cast("PathItemType", cast(ReferenceBase, pi.ref)._target) @@ -620,7 +616,7 @@ def __init__(self, api: "OpenAPI", use_operation_tags: bool): continue if use_operation_tags and op.tags: for tag in op.tags: - tags = list() + tags = [] while tag: tags.append(tag) tag = api._operationindex.tag(tag) @@ -629,12 +625,12 @@ def __init__(self, api: "OpenAPI", use_operation_tags: bool): else: self.operations.append(op.operationId) - if hasattr(pi, "additionalOperations"): # v32 + if hasattr(pi, "additionalOperations"): # v32 # noqa: SIM102 if pi.additionalOperations: for method, op in pi.additionalOperations.items(): if use_operation_tags and op.tags: for tag in op.tags: - tags = list() + tags = [] while tag: tags.append(tag) tag = api._operationindex.tag(tag) @@ -653,17 +649,17 @@ def __next__(self): return self.operations[next(self.r)] def __init__(self, api: "OpenAPI", use_operation_tags: bool): - self._api: "OpenAPI" = api - self._root: "RootType" = api._root + self._api: OpenAPI = api + self._root: RootType = api._root - self._operations: dict[str, tuple["HTTPMethodType", str, "OperationType", list["ServerType"] | None]] = dict() - self._tags: dict[str, "OperationIndex.OperationTag"] = collections.defaultdict( + self._operations: dict[str, tuple[HTTPMethodType, str, OperationType, list[ServerType] | None]] = {} + self._tags: dict[str, OperationIndex.OperationTag] = collections.defaultdict( lambda: OperationIndex.OperationTag(self) ) - pi: "PathItemType" + pi: PathItemType for path, pi in self._root.paths.items(): - op: "OperationType" - servers: list["ServerType"] | None + op: OperationType + servers: list[ServerType] | None if pi.ref: pi = pi.ref._target for method in pi.model_fields_set & HTTP_METHODS: @@ -680,7 +676,7 @@ def __init__(self, api: "OpenAPI", use_operation_tags: bool): if use_operation_tags and op.tags: for tag in op.tags: - tree: list[str] = list() + tree: list[str] = [] t: str | None = tag v: TagType | None while t: @@ -706,7 +702,7 @@ def __init__(self, api: "OpenAPI", use_operation_tags: bool): raise OperationIdDuplicationError(operationId, [item, other]) self._operations[operationId] = item - if hasattr(pi, "additionalOperations"): # v32 + if hasattr(pi, "additionalOperations"): # v32 # noqa: SIM102 if pi.additionalOperations: for method, op in pi.additionalOperations.items(): if op.operationId is None: diff --git a/src/aiopenapi3/v20/__init__.py b/src/aiopenapi3/v20/__init__.py index 1168cb14..09699ee1 100644 --- a/src/aiopenapi3/v20/__init__.py +++ b/src/aiopenapi3/v20/__init__.py @@ -1,18 +1,17 @@ -from .glue import Request, AsyncRequest - from .general import ExternalDocumentation, Reference -from .info import Contact, License, Info -from .parameter import Parameter, Header -from .paths import Response, Operation, PathItem, Paths +from .glue import AsyncRequest, Request +from .info import Contact, Info, License +from .parameter import Header, Parameter +from .paths import Operation, PathItem, Paths, Response from .root import Root from .schemas import Schema -from .security import SecurityScheme, SecurityRequirement +from .security import SecurityRequirement, SecurityScheme from .tag import Tag from .xml import XML def __init(): - r = dict() + r = {} CLASSES = [ ExternalDocumentation, Reference, @@ -41,23 +40,23 @@ def __init(): __init() __all__ = [ - "Request", + "XML", "AsyncRequest", - "ExternalDocumentation", - "Reference", "Contact", - "License", - "Info", - "Parameter", + "ExternalDocumentation", "Header", - "Response", + "Info", + "License", "Operation", + "Parameter", "PathItem", "Paths", + "Reference", + "Request", + "Response", "Root", "Schema", - "SecurityScheme", "SecurityRequirement", + "SecurityScheme", "Tag", - "XML", ] diff --git a/src/aiopenapi3/v20/general.py b/src/aiopenapi3/v20/general.py index 75551136..77f1f572 100644 --- a/src/aiopenapi3/v20/general.py +++ b/src/aiopenapi3/v20/general.py @@ -1,14 +1,13 @@ import typing from typing import Any, Union -from pydantic import Field, ConfigDict, PrivateAttr +from pydantic import ConfigDict, Field, PrivateAttr - -from ..base import ObjectExtended, ObjectBase, ReferenceBase +from ..base import ObjectBase, ObjectExtended, ReferenceBase if typing.TYPE_CHECKING: - from .schemas import Schema from .parameter import Parameter + from .schemas import Schema class ExternalDocumentation(ObjectExtended): diff --git a/src/aiopenapi3/v20/glue.py b/src/aiopenapi3/v20/glue.py index 217872d6..768b7f53 100644 --- a/src/aiopenapi3/v20/glue.py +++ b/src/aiopenapi3/v20/glue.py @@ -1,18 +1,13 @@ +import json import typing -from typing import Union, cast, Optional from collections.abc import Sequence -import json - -from typing import TypeGuard - +from typing import Optional, TypeGuard, Union, cast import httpx2 import pydantic -from ..request import RequestBase, AsyncRequestBase -from ..errors import HTTPStatusError, ContentTypeError, ResponseSchemaError, ResponseDecodingError, HeadersMissingError - - +from ..errors import ContentTypeError, HeadersMissingError, HTTPStatusError, ResponseDecodingError, ResponseSchemaError +from ..request import AsyncRequestBase, RequestBase from .parameter import Parameter from .root import Root @@ -23,15 +18,15 @@ if typing.TYPE_CHECKING: from .._types import ( - RequestParameters, + HeaderType, RequestData, - ResponseHeadersType, + RequestParameters, ResponseDataType, - HeaderType, + ResponseHeadersType, ) - from .schemas import Schema from .general import Reference from .paths import Response as v20ResponseType + from .schemas import Schema def in_body(x: Union["Parameter", "Reference"]) -> TypeGuard["Parameter"]: @@ -92,25 +87,23 @@ def _prepare_security(self): return if not self.security: - if any([{} == i.root for i in security]): + if any({} == i.root for i in security): return else: options = " or ".join( - sorted(map(lambda x: f"{{{x}}}", [" and ".join(sorted(i.root.keys())) for i in security])) + sorted(f"{{{x}}}" for x in [" and ".join(sorted(i.root.keys())) for i in security]) ) raise ValueError(f"No security requirement provided (accepts {options})") for s in security: if frozenset(s.root.keys()) - frozenset(self.security.keys()): continue - for scheme, _ in s.root.items(): + for scheme in s.root: value = self.security[scheme] self._prepare_secschemes(scheme, value) break else: - options = " or ".join( - sorted(map(lambda x: f"{{{x}}}", [" and ".join(sorted(i.root.keys())) for i in security])) - ) + options = " or ".join(sorted(f"{{{x}}}" for x in [" and ".join(sorted(i.root.keys())) for i in security])) raise ValueError( f"No security requirement satisfied (accepts {options} given {{{' and '.join(sorted(self.security.keys()))}}})" ) @@ -158,7 +151,7 @@ def _prepare_secschemes_extra(self, scheme: str, value: str | Sequence[str]) -> self.req.auth = httpx2_auth.HeaderApiKey(value, ss.name) def _prepare_parameters(self, provided: Optional["RequestParameters"]): - provided = provided or dict() + provided = provided or {} possible = {_.name: _ for _ in self.operation.parameters + self.root.paths[self.path].parameters} parameters = {i.name: i.default for i in filter(lambda x: x.default is not None, possible.values())} @@ -166,9 +159,7 @@ def _prepare_parameters(self, provided: Optional["RequestParameters"]): available = frozenset(parameters.keys()) accepted = frozenset(possible.keys()) - required = frozenset( - map(lambda x: x[0], filter(lambda y: y[1].required and y[1].in_ != "body", possible.items())) - ) + required = frozenset(x[0] for x in filter(lambda y: y[1].required and y[1].in_ != "body", possible.items())) if available - accepted: raise ValueError(f"Parameter {sorted(available - accepted)} unknown (accepted {sorted(accepted)})") if required - available: @@ -265,16 +256,16 @@ def _process__status_code(self, result: httpx2.Response, status_code: str) -> "v def _process__headers( self, result: httpx2.Response, headers: dict[str, str], expected_response: "v20ResponseType" ) -> "ResponseHeadersType": - rheaders = dict() + rheaders = {} if expected_response.headers: - required = dict(map(lambda x: (x[0].lower(), x[1]), expected_response.headers.items())) + required = {x[0].lower(): x[1] for x in expected_response.headers.items()} """ Swagger 2.0 does not have optional header - all defined headers are required https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#header-object """ available = frozenset(result.headers.keys()) if missing := (required.keys() - available): - report: dict[str, "HeaderType"] = {k: required[k] for k in missing} + report: dict[str, HeaderType] = {k: required[k] for k in missing} raise HeadersMissingError(self.operation, report, result) for name, header in expected_response.headers.items(): data = headers.get(name, None) @@ -289,7 +280,7 @@ def _process_stream(self, result: httpx2.Response) -> tuple["ResponseHeadersType return headers, expected_response.schema_ def _process_request(self, result: httpx2.Response) -> tuple["ResponseHeadersType", Optional["ResponseDataType"]]: - rheaders: "ResponseHeadersType" + rheaders: ResponseHeadersType # spec enforces these are strings status_code = str(result.status_code) content_type = result.headers.get("Content-Type", None) diff --git a/src/aiopenapi3/v20/parameter.py b/src/aiopenapi3/v20/parameter.py index 6b01214c..e087561a 100644 --- a/src/aiopenapi3/v20/parameter.py +++ b/src/aiopenapi3/v20/parameter.py @@ -1,17 +1,17 @@ -import io import enum -from typing import Optional, Any +import io +from typing import Any, ClassVar, Optional from pydantic import Field -from .general import Reference -from .schemas import Schema from ..base import ObjectExtended, ParameterBase from ..errors import ParameterFormatError +from .general import Reference +from .schemas import Schema class _ParameterCodec: - SEPERATOR_VALUES = {"csv": ",", "ssv": " ", "tsv": "\t", "pipes": "|"} + SEPERATOR_VALUES: ClassVar = {"csv": ",", "ssv": " ", "tsv": "\t", "pipes": "|"} """ Describing Parameters @@ -33,11 +33,10 @@ def _encode__collection(self, values): def _encode(self, name, value): if self.type == "array": value = self._encode__collection(value) - elif self.in_ == "formData": - if self.type == "file": - # https://www.python-httpx.org/quickstart/#sending-multipart-file-uploads - # we expect (filename, data, content-type) - assert isinstance(value, tuple) and len(value) == 3 and isinstance(value[1], io.IOBase) + elif self.in_ == "formData" and self.type == "file": + # https://www.python-httpx.org/quickstart/#sending-multipart-file-uploads + # we expect (filename, data, content-type) + assert isinstance(value, tuple) and len(value) == 3 and isinstance(value[1], io.IOBase) return {name: value} diff --git a/src/aiopenapi3/v20/paths.py b/src/aiopenapi3/v20/paths.py index 2927544d..40e23935 100644 --- a/src/aiopenapi3/v20/paths.py +++ b/src/aiopenapi3/v20/paths.py @@ -2,12 +2,11 @@ from pydantic import Field, model_validator -from .general import ExternalDocumentation -from .general import Reference +from ..base import ObjectExtended, OperationBase, PathItemBase, PathsBase +from .general import ExternalDocumentation, Reference from .parameter import Header, Parameter from .schemas import Schema from .security import SecurityRequirement -from ..base import ObjectExtended, PathsBase, OperationBase, PathItemBase class Response(ObjectExtended): diff --git a/src/aiopenapi3/v20/root.py b/src/aiopenapi3/v20/root.py index b585c712..0308d4bd 100644 --- a/src/aiopenapi3/v20/root.py +++ b/src/aiopenapi3/v20/root.py @@ -1,13 +1,13 @@ from pydantic import Field -from .general import Reference, ExternalDocumentation +from ..base import ObjectExtended, RootBase +from .general import ExternalDocumentation, Reference from .info import Info from .parameter import Parameter -from .paths import Response, Paths, PathItem +from .paths import PathItem, Paths, Response from .schemas import Schema -from .security import SecurityScheme, SecurityRequirement +from .security import SecurityRequirement, SecurityScheme from .tag import Tag -from ..base import ObjectExtended, RootBase class Root(ObjectExtended, RootBase): diff --git a/src/aiopenapi3/v20/schemas.py b/src/aiopenapi3/v20/schemas.py index dde8c23f..097ba370 100644 --- a/src/aiopenapi3/v20/schemas.py +++ b/src/aiopenapi3/v20/schemas.py @@ -1,10 +1,10 @@ -from typing import Union, Any, Optional +from typing import Any, Optional, Union -from pydantic import Field, model_validator, ValidatorFunctionWrapHandler, ValidationInfo +from pydantic import Field, ValidationInfo, ValidatorFunctionWrapHandler, model_validator +from ..base import ObjectExtended, SchemaBase from .general import Reference from .xml import XML -from ..base import ObjectExtended, SchemaBase class Schema(ObjectExtended, SchemaBase): diff --git a/src/aiopenapi3/v20/security.py b/src/aiopenapi3/v20/security.py index 8d227eb2..10d62997 100644 --- a/src/aiopenapi3/v20/security.py +++ b/src/aiopenapi3/v20/security.py @@ -1,4 +1,4 @@ -from typing import Union, Annotated, Literal +from typing import Annotated, Literal from pydantic import Field, RootModel @@ -32,11 +32,7 @@ class oauth2(_SecurityScheme): class SecurityScheme( RootModel[ Annotated[ - Union[ - _SecuritySchemes.basic, - _SecuritySchemes.apiKey, - _SecuritySchemes.oauth2, - ], + _SecuritySchemes.basic | _SecuritySchemes.apiKey | _SecuritySchemes.oauth2, Field(discriminator="type"), ] ] @@ -47,8 +43,6 @@ class SecurityScheme( https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#security-scheme-object """ - pass - class SecurityRequirement(RootModel): """ diff --git a/src/aiopenapi3/v30/__init__.py b/src/aiopenapi3/v30/__init__.py index 8983630a..2666ca63 100644 --- a/src/aiopenapi3/v30/__init__.py +++ b/src/aiopenapi3/v30/__init__.py @@ -1,22 +1,21 @@ -from .glue import Request, AsyncRequest - from .components import Components from .example import Example from .general import ExternalDocumentation, Reference -from .info import Contact, License, Info +from .glue import AsyncRequest, Request +from .info import Contact, Info, License from .media import Encoding, MediaType -from .parameter import Parameter, Header -from .paths import RequestBody, Link, Response, Operation, PathItem, Paths, Callback, RuntimeExpression +from .parameter import Header, Parameter +from .paths import Callback, Link, Operation, PathItem, Paths, RequestBody, Response, RuntimeExpression from .root import Root from .schemas import Discriminator, Schema -from .security import OAuthFlow, OAuthFlows, SecurityScheme, SecurityRequirement -from .servers import ServerVariable, Server +from .security import OAuthFlow, OAuthFlows, SecurityRequirement, SecurityScheme +from .servers import Server, ServerVariable from .tag import Tag from .xml import XML def __init(): - r = dict() + r = {} CLASSES = [ Components, Example, @@ -58,36 +57,36 @@ def __init(): __init() __all__ = [ - "Request", + "XML", "AsyncRequest", + "Callback", "Components", - "Example", - "ExternalDocumentation", - "Reference", "Contact", - "License", - "Info", + "Discriminator", "Encoding", - "MediaType", - "Parameter", + "Example", + "ExternalDocumentation", "Header", - "RequestBody", + "Info", + "License", "Link", - "Response", + "MediaType", + "OAuthFlow", + "OAuthFlows", "Operation", + "Parameter", "PathItem", "Paths", - "Callback", - "RuntimeExpression", + "Reference", + "Request", + "RequestBody", + "Response", "Root", - "Discriminator", + "RuntimeExpression", "Schema", - "OAuthFlow", - "OAuthFlows", - "SecurityScheme", "SecurityRequirement", - "ServerVariable", + "SecurityScheme", "Server", + "ServerVariable", "Tag", - "XML", ] diff --git a/src/aiopenapi3/v30/components.py b/src/aiopenapi3/v30/components.py index 5ec859bf..071a8e61 100644 --- a/src/aiopenapi3/v30/components.py +++ b/src/aiopenapi3/v30/components.py @@ -1,11 +1,10 @@ from pydantic import Field from ..base import ObjectExtended - from .example import Example -from .paths import RequestBody, Link, Response, Callback from .general import Reference from .parameter import Header, Parameter +from .paths import Callback, Link, RequestBody, Response from .schemas import Schema from .security import SecurityScheme diff --git a/src/aiopenapi3/v30/formdata.py b/src/aiopenapi3/v30/formdata.py index 2698457d..4e9651da 100644 --- a/src/aiopenapi3/v30/formdata.py +++ b/src/aiopenapi3/v30/formdata.py @@ -1,15 +1,15 @@ import base64 +import collections import quopri -from typing import TYPE_CHECKING, NamedTuple -from email.mime import multipart, nonmultipart from email.message import Message -import collections +from email.mime import multipart, nonmultipart +from typing import TYPE_CHECKING, NamedTuple from .parameter import encode_parameter - if TYPE_CHECKING: from pydantic import BaseModel + from .._types import MediaTypeType, SchemaType @@ -40,7 +40,7 @@ class MultipartParameter(NamedTuple): def parameters_from_multipart( data: "BaseModel", media: "MediaTypeType", mph: dict[str, str] ) -> list[MultipartParameter]: - params: list[MultipartParameter] = list() + params: list[MultipartParameter] = [] for k in data.model_fields_set: v = getattr(data, k) ct = "text/plain" @@ -59,14 +59,14 @@ def parameters_from_multipart( if (e := media.encoding.get(k, None)) is not None: ct = e.contentType or ct style = e.style or "form" - explode = e.explode if e.explode is not None else (True if style == "form" else False) + explode = e.explode if e.explode is not None else (style == "form") allowReserved = e.allowReserved or False - headers = {name: mph[name] for name in e.headers.keys() if name in mph} + headers = {name: mph[name] for name in e.headers if name in mph} else: allowReserved = False style = "form" explode = True - headers = dict() + headers = {} m = media.schema_.properties[k] """ @@ -165,7 +165,6 @@ def encode_multipart_parameters( f.headers["Content-Encoding"] = codec else: """OpenAPI 3.0""" - pass data = encode_content(v, codec) diff --git a/src/aiopenapi3/v30/general.py b/src/aiopenapi3/v30/general.py index b4ce57a9..fa519208 100644 --- a/src/aiopenapi3/v30/general.py +++ b/src/aiopenapi3/v30/general.py @@ -1,15 +1,13 @@ import typing -from typing import Union, Any +from typing import Any, Union -from pydantic import Field, PrivateAttr, ConfigDict - - -from ..base import ObjectExtended, ObjectBase, ReferenceBase +from pydantic import ConfigDict, Field, PrivateAttr +from ..base import ObjectBase, ObjectExtended, ReferenceBase if typing.TYPE_CHECKING: - from .schemas import Schema from .parameter import Parameter + from .schemas import Schema class ExternalDocumentation(ObjectExtended): diff --git a/src/aiopenapi3/v30/glue.py b/src/aiopenapi3/v30/glue.py index 461c3d49..cb4d6218 100644 --- a/src/aiopenapi3/v30/glue.py +++ b/src/aiopenapi3/v30/glue.py @@ -1,15 +1,16 @@ import io -from typing import Union, TYPE_CHECKING, Optional, cast, Any -from collections.abc import Sequence import json import urllib.parse +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, Optional, cast import httpx2 try: + import inspect + import httpx2_auth from httpx2_auth import SupportMultiAuth - import inspect except ImportError: httpx2_auth = None else: @@ -23,36 +24,36 @@ import pydantic # import pydantic.json - import aiopenapi3.v30.media -from ..request import RequestBase, AsyncRequestBase -from ..errors import HTTPStatusError, ContentTypeError, ResponseDecodingError, ResponseSchemaError, HeadersMissingError + +from ..errors import ContentTypeError, HeadersMissingError, HTTPStatusError, ResponseDecodingError, ResponseSchemaError +from ..request import AsyncRequestBase, RequestBase +from ..v31.root import Root as v31Root from .formdata import ( + MultipartParameter, + encode_multipart_parameters, parameters_from_multipart, parameters_from_urlencoded, - encode_multipart_parameters, - MultipartParameter, ) - from .root import Root as v30Root -from ..v31.root import Root as v31Root if TYPE_CHECKING: from .._types import ( - SchemaType, - RequestParameters, - RequestData, ParameterType, + RequestData, RequestFileParameter, - ResponseHeadersType, + RequestParameters, ResponseDataType, + ResponseHeadersType, + SchemaType, ) + from ..v31.paths import MediaType as v31MediaType + from ..v31.paths import Response as v31Response + from .paths import MediaType as v30MediaType + from .paths import Response as v30Response - from .paths import Response as v30Response, MediaType as v30MediaType - from ..v31.paths import Response as v31Response, MediaType as v31MediaType - - v3xResponseType = Union[v30Response, v31Response] - v3xMediaTypeType = Union[v30MediaType, v31MediaType] + v3xResponseType = v30Response | v31Response + v3xMediaTypeType = v30MediaType | v31MediaType class Request(RequestBase): @@ -91,7 +92,7 @@ def args(self, content_type: str = "application/json") -> dict[str, Any]: def return_value(self, http_status: int = 200, content_type: str = "application/json") -> Optional["SchemaType"]: status_key = str(http_status) - if a := self.operation.responses.get(status_key) or self.operation.responses.get(status_key[0] + "XX"): + if a := self.operation.responses.get(status_key) or self.operation.responses.get(status_key[0] + "XX"): # noqa: SIM102 if b := a.content.get(content_type): return b.schema_ return None @@ -103,25 +104,23 @@ def _prepare_security(self) -> None: return if not self.security: - if any([{} == i.root for i in security]): + if any({} == i.root for i in security): return else: options = " or ".join( - sorted(map(lambda x: f"{{{x}}}", [" and ".join(sorted(i.root.keys())) for i in security])) + sorted(f"{{{x}}}" for x in [" and ".join(sorted(i.root.keys())) for i in security]) ) raise ValueError(f"No security requirement satisfied (accepts {options})") for s in security: if frozenset(s.root.keys()) - frozenset(self.security.keys()): continue - for scheme, _ in s.root.items(): + for scheme in s.root: value = self.security[scheme] self._prepare_secschemes(scheme, value) break else: - options = " or ".join( - sorted(map(lambda x: f"{{{x}}}", [" and ".join(sorted(i.root.keys())) for i in security])) - ) + options = " or ".join(sorted(f"{{{x}}}" for x in [" and ".join(sorted(i.root.keys())) for i in security])) raise ValueError( f"No security requirement satisfied (accepts {options} given {{{' and '.join(sorted(self.security.keys()))}}}" ) @@ -271,7 +270,7 @@ def _prepare_parameters(self, provided: Optional["RequestParameters"]) -> dict[s A unique parameter is defined by a combination of a name and location. """ - provided = provided or dict() + provided = provided or {} possible = {_.name: _ for _ in self.operation.parameters + self.root.paths[self.path].parameters} from .. import v30, v31, v32 @@ -279,11 +278,11 @@ def _prepare_parameters(self, provided: Optional["RequestParameters"]) -> dict[s assert isinstance(self.operation, (v30.Operation, v31.Operation, v32.Operation)) if self.operation.requestBody: - rbq: dict[str, str] = dict() # requestBody Parameters + rbq: dict[str, str] = {} # requestBody Parameters ct = "multipart/form-data" if ct in self.operation.requestBody.content: assert self.operation.requestBody.content[ct].encoding is not None - for k, v in self.operation.requestBody.content[ct].encoding.items(): + for v in self.operation.requestBody.content[ct].encoding.values(): assert v.headers is not None and isinstance(v.headers, dict) rbq.update(v.headers) possible.update(rbq) @@ -305,7 +304,7 @@ def _prepare_parameters(self, provided: Optional["RequestParameters"]) -> dict[s available = frozenset(parameters.keys()) accepted = frozenset(possible.keys()) - required = frozenset(map(lambda x: x[0], filter(lambda y: y[1].required, possible.items()))) + required = frozenset(x[0] for x in filter(lambda y: y[1].required, possible.items())) if available - accepted: raise ValueError(f"Parameter {sorted(available - accepted)} unknown (accepted {sorted(accepted)})") if required - available: @@ -314,7 +313,7 @@ def _prepare_parameters(self, provided: Optional["RequestParameters"]) -> dict[s ) path_parameters = {} - mph = dict() + mph = {} for name, value in parameters.items(): spec = possible[name] values = spec._encode(name, value) @@ -394,8 +393,8 @@ def _prepare_body(self, data_: Optional["RequestData"], mph: dict[str, str]) -> self.req.content = msg.as_string() self.req.headers["Content-Type"] = f'{msg.get_content_type()}; boundary="{msg.get_boundary()}"' elif isinstance(data_, list): - rfiles = list() - rdata: dict[str, str] = dict() + rfiles = [] + rdata: dict[str, str] = {} name: str value: tuple[str, Any] for name, value in cast(Sequence[tuple[str, Any]], data_): @@ -414,13 +413,13 @@ def _prepare_body(self, data_: Optional["RequestData"], mph: dict[str, str]) -> assert media.encoding is not None if (e := media.encoding.get(name)) is not None: assert e.headers - headers.update({name: mph[name] for name in e.headers.keys() if name in mph}) + headers.update({name: mph[name] for name in e.headers if name in mph}) _value = (alias, fh, content_type, headers) rfiles.append((name, _value)) elif isinstance(value, str): rdata[name] = value else: - raise TypeError(type(value)) # noqa + raise TypeError(type(value)) self.req.files = rfiles self.req.data = rdata else: @@ -463,7 +462,7 @@ def _prepare_body(self, data_: Optional["RequestData"], mph: dict[str, str]) -> elif (ct := "application/octet-stream") in self.operation.requestBody.content: self.req.headers["Content-Type"] = ct - value: "RequestFileParameter" + value: RequestFileParameter if isinstance(data_, tuple) and len(data_) >= 2: # (name, file-like-object, …) self.req.content = data_[1] @@ -510,14 +509,11 @@ def _process__status_code(self, result: httpx2.Response, status_code: str) -> "v def _process__headers( self, result: httpx2.Response, headers: dict[str, str], expected_response: "v3xResponseType" ) -> "ResponseHeadersType": - rheaders = dict() + rheaders = {} if expected_response.headers: - required = dict( - map( - lambda x: (x[0].lower(), x[1]), - filter(lambda x: x[1].required is True, expected_response.headers.items()), - ) - ) + required = { + x[0].lower(): x[1] for x in filter(lambda x: x[1].required is True, expected_response.headers.items()) + } available = frozenset(headers.keys()) if missing := (required.keys() - available): missed = {k: required[k] for k in missing} @@ -539,8 +535,8 @@ def _process__content_type( https://datatracker.ietf.org/doc/html/rfc7231#appendix-D media-range = ( "*/*" / ( type "/*" ) / ( type "/" subtype ) ) *( OWS ";" OWS parameter ) """ - content_type, _, encoding = content_type.partition(";") - expected_media: Optional["v3xMediaTypeType"] = ( + content_type, _, _encoding = content_type.partition(";") + expected_media: v3xMediaTypeType | None = ( expected_response.content.get(content_type, None) or expected_response.content.get(content_type.partition("/")[0] + "/*", None) or expected_response.content.get("*/*", None) @@ -583,7 +579,7 @@ def _process_sequence(self, result: httpx2.Response) -> tuple["ResponseHeadersTy return headers, expected_media.itemSchema, content_type def _process_request(self, result: httpx2.Response) -> tuple["ResponseHeadersType", "ResponseDataType"]: - rheaders = dict() + rheaders = {} # spec enforces these are strings status_code = str(result.status_code) content_type = result.headers.get("Content-Type", None) diff --git a/src/aiopenapi3/v30/media.py b/src/aiopenapi3/v30/media.py index 9ef6f58b..351744e3 100644 --- a/src/aiopenapi3/v30/media.py +++ b/src/aiopenapi3/v30/media.py @@ -1,10 +1,9 @@ import typing -from typing import Union, Any +from typing import Any, Union from pydantic import Field from ..base import ObjectExtended - from .example import Example from .general import Reference from .schemas import Schema diff --git a/src/aiopenapi3/v30/parameter.py b/src/aiopenapi3/v30/parameter.py index 9456af09..dca44709 100644 --- a/src/aiopenapi3/v30/parameter.py +++ b/src/aiopenapi3/v30/parameter.py @@ -1,26 +1,26 @@ -import enum import datetime import decimal +import enum +import json import typing import uuid -import json -from typing import Union, Any from collections.abc import MutableMapping +from typing import Any, Union -from pydantic import BaseModel, Field, model_validator import more_itertools +from pydantic import BaseModel, Field, model_validator -from ..base import ObjectExtended, ParameterBase as ParameterBase_, ReferenceBase +from ..base import ObjectExtended, ReferenceBase +from ..base import ParameterBase as ParameterBase_ from ..errors import ParameterFormatError - +from ..model import TYPES_SCHEMA_MAP from .example import Example from .general import Reference from .schemas import Schema -from ..model import TYPES_SCHEMA_MAP if typing.TYPE_CHECKING: - from .paths import MediaType from .._types import v3xSchemaType + from .paths import MediaType class _ParameterCodec: @@ -36,11 +36,11 @@ def _codec(self): elif self.in_ == "query": style = self.style or "form" assert style in frozenset(["form", "spaceDelimited", "pipeDelimited", "deepObject"]) - explode = self.explode if self.explode is not None else (False if style != "form" else True) + explode = self.explode if self.explode is not None else (style == "form") elif self.in_ == "cookie": style = self.style or "form" assert style in frozenset(["form"]) - explode = self.explode if self.explode is not None else (False if style != "form" else True) + explode = self.explode if self.explode is not None else (style == "form") elif self.in_ == "querystring": style = "querystring" explode = None @@ -121,7 +121,7 @@ def _encode__label(self, name: str, type_: str, value, schema: "v3xSchemaType", # .blue value = f".{value}" elif type_ in frozenset(["number", "integer"]): - value = f".{str(value)}" + value = f".{value!s}" elif type_ == "boolean": value = f".{json.dumps(value)}" elif type_ == "null": @@ -197,7 +197,7 @@ def _encode__simple(self, name: str, type_: str, value, schema: "v3xSchemaType", elif type_ == "boolean": return {name: json.dumps(value)} elif type_ == "null": - return dict() + return {} elif type_ == "array": assert isinstance(value, (list, tuple)) # blue,black,brown @@ -229,7 +229,7 @@ def _encode__Delimited(self, sep: str, name: str, type_: str, value, schema: "v3 assert explode is False if value is None: - return dict() + return {} if type_ == "array": value = sep.join(value) @@ -245,7 +245,7 @@ def _encode__deepObject(self, name: str, type_: str, value, schema: "v3xSchemaTy assert type_ == "object" and explode is True if not value: - return dict() + return {} values = value if isinstance(value, dict) else value.model_dump() # color[R]=100&color[G]=200&color[B]=150 @@ -266,7 +266,7 @@ def flatten_dict(d: MutableMapping, key: str = ""): def _encode__querystring(self, name: str, type_: str, value, schema: "v3xSchemaType", explode: bool): print(name, type_, value, schema, explode) - values = dict() + values = {} ct = next(iter(self.content.keys())) media = self.content[ct] if ct == "application/x-www-form-urlencoded": @@ -298,7 +298,7 @@ def _decode_simple(self, value, schema: "v3xSchemaType", explode: bool): return dict(more_itertools.chunked(value.split(","), 2)) else: # R=100,G=200,B=150 - return dict(map(lambda y: (y[0], y[2]), map(lambda x: x.partition("="), value.split(",")))) + return {y[0]: y[2] for y in (x.partition("=") for x in value.split(","))} else: # convert basic type return value @@ -340,7 +340,7 @@ class Parameter(ParameterBase, _ParameterCodec): @model_validator(mode="after") def validate_Parameter(self): assert self.in_ != "path" or self.required is True, ( - "Parameter '%s' must be required since it is in the path" % self.name + f"Parameter '{self.name}' must be required since it is in the path" ) return self diff --git a/src/aiopenapi3/v30/paths.py b/src/aiopenapi3/v30/paths.py index d2b48008..d166646c 100644 --- a/src/aiopenapi3/v30/paths.py +++ b/src/aiopenapi3/v30/paths.py @@ -1,14 +1,13 @@ -from typing import Union, Any +from typing import Any, Union -from pydantic import Field, model_validator, RootModel +from pydantic import Field, RootModel, model_validator -from ..base import ObjectExtended, PathsBase, OperationBase, PathItemBase -from .general import ExternalDocumentation -from .general import Reference +from ..base import ObjectExtended, OperationBase, PathItemBase, PathsBase +from .general import ExternalDocumentation, Reference from .media import MediaType from .parameter import Header, Parameter -from .servers import Server from .security import SecurityRequirement +from .servers import Server class RequestBody(ObjectExtended): diff --git a/src/aiopenapi3/v30/root.py b/src/aiopenapi3/v30/root.py index 8e25e964..7bb6d598 100644 --- a/src/aiopenapi3/v30/root.py +++ b/src/aiopenapi3/v30/root.py @@ -1,11 +1,8 @@ from typing import Any - from pydantic import Field - from ..base import ObjectExtended, RootBase - from .components import Components from .general import Reference from .info import Info diff --git a/src/aiopenapi3/v30/schemas.py b/src/aiopenapi3/v30/schemas.py index c901ab76..ea4b0829 100644 --- a/src/aiopenapi3/v30/schemas.py +++ b/src/aiopenapi3/v30/schemas.py @@ -1,8 +1,8 @@ -from typing import Union, Any +from typing import Any, Union -from pydantic import Field, model_validator, ConfigDict +from pydantic import ConfigDict, Field, model_validator -from ..base import ObjectExtended, SchemaBase, DiscriminatorBase +from ..base import DiscriminatorBase, ObjectExtended, SchemaBase from .general import Reference from .xml import XML diff --git a/src/aiopenapi3/v30/security.py b/src/aiopenapi3/v30/security.py index fd0cca5e..edc3df27 100644 --- a/src/aiopenapi3/v30/security.py +++ b/src/aiopenapi3/v30/security.py @@ -1,4 +1,5 @@ -from typing import Union, Annotated, Literal +from typing import Annotated, Literal + from pydantic import Field, RootModel, constr from ..base import ObjectExtended @@ -60,9 +61,7 @@ class openIdConnect(_SecurityScheme): class SecurityScheme( RootModel[ Annotated[ - Union[ - _SecuritySchemes.apiKey, _SecuritySchemes.http, _SecuritySchemes.oauth2, _SecuritySchemes.openIdConnect - ], + _SecuritySchemes.apiKey | _SecuritySchemes.http | _SecuritySchemes.oauth2 | _SecuritySchemes.openIdConnect, Field(discriminator="type"), ] ] @@ -73,8 +72,6 @@ class SecurityScheme( .. _Security Scheme: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object """ - pass - class SecurityRequirement(RootModel[dict[str, list[str]]]): """ @@ -82,5 +79,3 @@ class SecurityRequirement(RootModel[dict[str, list[str]]]): .. _SecurityRequirement: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-requirement-object """ - - pass diff --git a/src/aiopenapi3/v30/servers.py b/src/aiopenapi3/v30/servers.py index 635e37a0..a23f659e 100644 --- a/src/aiopenapi3/v30/servers.py +++ b/src/aiopenapi3/v30/servers.py @@ -43,13 +43,12 @@ def validate_server_url_parameters(self) -> "Server": def validate_parameter_enum(self, parameters: dict[str, str]): for name, value in parameters.items(): - if v := self.variables.get(name): - if v.enum and value not in v.enum: - raise ValueError(f"Server Variable {name} value {value} not allowed ({v.enum})") + if (v := self.variables.get(name)) and v.enum and value not in v.enum: + raise ValueError(f"Server Variable {name} value {value} not allowed ({v.enum})") def createUrl(self, variables: dict[str, str]) -> str: self.validate_parameter_enum(variables) - vars: dict[str, str | None] = dict(map(lambda x: (x[0], x[1].default), self.variables.items())) + vars: dict[str, str | None] = {x[0]: x[1].default for x in self.variables.items()} vars.update(variables) url: str = self.url.format(**vars) return url diff --git a/src/aiopenapi3/v31/__init__.py b/src/aiopenapi3/v31/__init__.py index 8a8a4a4a..ce0877f5 100644 --- a/src/aiopenapi3/v31/__init__.py +++ b/src/aiopenapi3/v31/__init__.py @@ -1,20 +1,20 @@ from .components import Components from .example import Example from .general import ExternalDocumentation, Reference -from .info import Contact, License, Info +from .info import Contact, Info, License from .media import Encoding, MediaType -from .parameter import Parameter, Header -from .paths import RequestBody, Link, Response, Operation, PathItem, Paths, Callback, RuntimeExpression +from .parameter import Header, Parameter +from .paths import Callback, Link, Operation, PathItem, Paths, RequestBody, Response, RuntimeExpression from .root import Root from .schemas import Discriminator, Schema -from .security import OAuthFlow, OAuthFlows, SecurityScheme, SecurityRequirement -from .servers import ServerVariable, Server +from .security import OAuthFlow, OAuthFlows, SecurityRequirement, SecurityScheme +from .servers import Server, ServerVariable from .tag import Tag from .xml import XML def __init(): - r = dict() + r = {} CLASSES = [ Components, Example, diff --git a/src/aiopenapi3/v31/components.py b/src/aiopenapi3/v31/components.py index 03ce1e2d..40be317a 100644 --- a/src/aiopenapi3/v31/components.py +++ b/src/aiopenapi3/v31/components.py @@ -1,11 +1,10 @@ from pydantic import Field from ..base import ObjectExtended - from .example import Example -from .paths import RequestBody, Link, Response, Callback, PathItem from .general import Reference from .parameter import Header, Parameter +from .paths import Callback, Link, PathItem, RequestBody, Response from .schemas import Schema from .security import SecurityScheme diff --git a/src/aiopenapi3/v31/general.py b/src/aiopenapi3/v31/general.py index cab70d30..bb789bb3 100644 --- a/src/aiopenapi3/v31/general.py +++ b/src/aiopenapi3/v31/general.py @@ -1,14 +1,13 @@ import typing -from typing import Union, Any +from typing import Any, Union -from pydantic import Field, AnyUrl, PrivateAttr, ConfigDict +from pydantic import AnyUrl, ConfigDict, Field, PrivateAttr - -from ..base import ObjectExtended, ObjectBase, ReferenceBase +from ..base import ObjectBase, ObjectExtended, ReferenceBase if typing.TYPE_CHECKING: - from .schemas import Schema from .paths import Parameter, PathItem + from .schemas import Schema class ExternalDocumentation(ObjectExtended): diff --git a/src/aiopenapi3/v31/info.py b/src/aiopenapi3/v31/info.py index ee7c5c17..95fb0abe 100644 --- a/src/aiopenapi3/v31/info.py +++ b/src/aiopenapi3/v31/info.py @@ -1,4 +1,4 @@ -from pydantic import Field, EmailStr, model_validator +from pydantic import EmailStr, Field, model_validator from aiopenapi3.base import ObjectExtended @@ -31,7 +31,7 @@ def validate_License(self): """ 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. """ - assert not all([getattr(self, i, None) is not None for i in ["identifier", "url"]]) + assert not all(getattr(self, i, None) is not None for i in ["identifier", "url"]) return self diff --git a/src/aiopenapi3/v31/media.py b/src/aiopenapi3/v31/media.py index 22b396fa..9957ab9b 100644 --- a/src/aiopenapi3/v31/media.py +++ b/src/aiopenapi3/v31/media.py @@ -3,11 +3,10 @@ from pydantic import Field from ..base import ObjectExtended - from .example import Example from .general import Reference -from .schemas import Schema from .parameter import Header +from .schemas import Schema class Encoding(ObjectExtended): diff --git a/src/aiopenapi3/v31/parameter.py b/src/aiopenapi3/v31/parameter.py index 484680bd..bcd2be58 100644 --- a/src/aiopenapi3/v31/parameter.py +++ b/src/aiopenapi3/v31/parameter.py @@ -1,17 +1,16 @@ import enum import typing -from typing import Union, Any +from typing import Any, Union from pydantic import Field -from ..base import ObjectExtended, ParameterBase as _ParameterBase - +from ..base import ObjectExtended +from ..base import ParameterBase as _ParameterBase +from ..v30.parameter import _ParameterCodec from .example import Example from .general import Reference from .schemas import Schema -from ..v30.parameter import _ParameterCodec - if typing.TYPE_CHECKING: from .paths import MediaType diff --git a/src/aiopenapi3/v31/paths.py b/src/aiopenapi3/v31/paths.py index c65628aa..1c384f8d 100644 --- a/src/aiopenapi3/v31/paths.py +++ b/src/aiopenapi3/v31/paths.py @@ -1,14 +1,13 @@ -from typing import Union, Any +from typing import Any, Union -from pydantic import Field, model_validator, RootModel +from pydantic import Field, RootModel, model_validator -from ..base import ObjectExtended, PathsBase, OperationBase, PathItemBase -from .general import ExternalDocumentation -from .general import Reference +from ..base import ObjectExtended, OperationBase, PathItemBase, PathsBase +from .general import ExternalDocumentation, Reference from .media import MediaType from .parameter import Header, Parameter -from .servers import Server from .security import SecurityRequirement +from .servers import Server class RequestBody(ObjectExtended): diff --git a/src/aiopenapi3/v31/root.py b/src/aiopenapi3/v31/root.py index e596a4a5..1294f2bb 100644 --- a/src/aiopenapi3/v31/root.py +++ b/src/aiopenapi3/v31/root.py @@ -3,14 +3,12 @@ from pydantic import Field, model_validator from ..base import ObjectExtended, RootBase - +from .components import Components +from .general import Reference from .info import Info -from .paths import Paths, PathItem +from .paths import PathItem, Paths from .security import SecurityRequirement from .servers import Server - -from .components import Components -from .general import Reference from .tag import Tag diff --git a/src/aiopenapi3/v31/schemas.py b/src/aiopenapi3/v31/schemas.py index 91c8ebdb..81061fcb 100644 --- a/src/aiopenapi3/v31/schemas.py +++ b/src/aiopenapi3/v31/schemas.py @@ -1,8 +1,8 @@ -from typing import Union, Any, Optional +from typing import Any, Optional, Union -from pydantic import Field, model_validator, ConfigDict +from pydantic import ConfigDict, Field, model_validator -from ..base import ObjectExtended, SchemaBase, DiscriminatorBase +from ..base import DiscriminatorBase, ObjectExtended, SchemaBase from .xml import XML diff --git a/src/aiopenapi3/v31/security.py b/src/aiopenapi3/v31/security.py index 75c87837..76777a54 100644 --- a/src/aiopenapi3/v31/security.py +++ b/src/aiopenapi3/v31/security.py @@ -1,6 +1,6 @@ from pathlib import Path +from typing import Annotated, Literal -from typing import Union, Annotated, Literal from pydantic import Field, RootModel, constr from ..base import ObjectExtended @@ -74,13 +74,11 @@ class openIdConnect(_SecurityScheme): class SecurityScheme( RootModel[ Annotated[ - Union[ - _SecuritySchemes.apiKey, - _SecuritySchemes.http, - _SecuritySchemes.mutualTLS, - _SecuritySchemes.oauth2, - _SecuritySchemes.openIdConnect, - ], + _SecuritySchemes.apiKey + | _SecuritySchemes.http + | _SecuritySchemes.mutualTLS + | _SecuritySchemes.oauth2 + | _SecuritySchemes.openIdConnect, Field(discriminator="type"), ] ] @@ -91,8 +89,6 @@ class SecurityScheme( .. _Security Scheme: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#security-scheme-object """ - pass - class SecurityRequirement(RootModel[dict[str, list[str]]]): """ @@ -100,5 +96,3 @@ class SecurityRequirement(RootModel[dict[str, list[str]]]): .. _SecurityRequirement: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#securityRequirementObject """ - - pass diff --git a/src/aiopenapi3/v31/servers.py b/src/aiopenapi3/v31/servers.py index de74372f..43df0fcd 100644 --- a/src/aiopenapi3/v31/servers.py +++ b/src/aiopenapi3/v31/servers.py @@ -43,13 +43,12 @@ def validate_server_url_parameters(self) -> "Server": def validate_parameter_enum(self, parameters: dict[str, str]): for name, value in parameters.items(): - if v := self.variables.get(name): - if v.enum and value not in v.enum: - raise ValueError(f"Server Variable {name} value {value} not allowed ({v.enum})") + if (v := self.variables.get(name)) and v.enum and value not in v.enum: + raise ValueError(f"Server Variable {name} value {value} not allowed ({v.enum})") def createUrl(self, variables: dict[str, str]) -> str: self.validate_parameter_enum(variables) - vars: dict[str, str | None] = dict(map(lambda x: (x[0], x[1].default), self.variables.items())) + vars: dict[str, str | None] = {x[0]: x[1].default for x in self.variables.items()} vars.update(variables) url: str = self.url.format(**vars) return url diff --git a/src/aiopenapi3/v32/__init__.py b/src/aiopenapi3/v32/__init__.py index 8a8a4a4a..ce0877f5 100644 --- a/src/aiopenapi3/v32/__init__.py +++ b/src/aiopenapi3/v32/__init__.py @@ -1,20 +1,20 @@ from .components import Components from .example import Example from .general import ExternalDocumentation, Reference -from .info import Contact, License, Info +from .info import Contact, Info, License from .media import Encoding, MediaType -from .parameter import Parameter, Header -from .paths import RequestBody, Link, Response, Operation, PathItem, Paths, Callback, RuntimeExpression +from .parameter import Header, Parameter +from .paths import Callback, Link, Operation, PathItem, Paths, RequestBody, Response, RuntimeExpression from .root import Root from .schemas import Discriminator, Schema -from .security import OAuthFlow, OAuthFlows, SecurityScheme, SecurityRequirement -from .servers import ServerVariable, Server +from .security import OAuthFlow, OAuthFlows, SecurityRequirement, SecurityScheme +from .servers import Server, ServerVariable from .tag import Tag from .xml import XML def __init(): - r = dict() + r = {} CLASSES = [ Components, Example, diff --git a/src/aiopenapi3/v32/components.py b/src/aiopenapi3/v32/components.py index cceedc38..4bbf0642 100644 --- a/src/aiopenapi3/v32/components.py +++ b/src/aiopenapi3/v32/components.py @@ -1,14 +1,13 @@ from pydantic import Field from ..base import ObjectExtended - from .example import Example -from .paths import RequestBody, Link, Response, Callback, PathItem from .general import Reference +from .media import MediaType from .parameter import Header, Parameter +from .paths import Callback, Link, PathItem, RequestBody, Response from .schemas import Schema from .security import SecurityScheme -from .media import MediaType class Components(ObjectExtended): diff --git a/src/aiopenapi3/v32/general.py b/src/aiopenapi3/v32/general.py index 9db600a7..ee46e9d6 100644 --- a/src/aiopenapi3/v32/general.py +++ b/src/aiopenapi3/v32/general.py @@ -1,14 +1,13 @@ import typing -from typing import Union, Any +from typing import Any, Union -from pydantic import Field, AnyUrl, PrivateAttr, ConfigDict +from pydantic import AnyUrl, ConfigDict, Field, PrivateAttr - -from ..base import ObjectExtended, ObjectBase, ReferenceBase +from ..base import ObjectBase, ObjectExtended, ReferenceBase if typing.TYPE_CHECKING: - from .schemas import Schema from .paths import Parameter, PathItem + from .schemas import Schema class ExternalDocumentation(ObjectExtended): diff --git a/src/aiopenapi3/v32/info.py b/src/aiopenapi3/v32/info.py index 33dd3090..4d9b3b7e 100644 --- a/src/aiopenapi3/v32/info.py +++ b/src/aiopenapi3/v32/info.py @@ -1,4 +1,4 @@ -from pydantic import Field, EmailStr, model_validator +from pydantic import EmailStr, Field, model_validator from aiopenapi3.base import ObjectExtended @@ -39,7 +39,7 @@ def validate_License(self): """ 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. """ - assert not all([getattr(self, i, None) is not None for i in ["identifier", "url"]]) + assert not all(getattr(self, i, None) is not None for i in ["identifier", "url"]) return self diff --git a/src/aiopenapi3/v32/media.py b/src/aiopenapi3/v32/media.py index 825a9f1e..875c08ab 100644 --- a/src/aiopenapi3/v32/media.py +++ b/src/aiopenapi3/v32/media.py @@ -2,6 +2,7 @@ if sys.version_info < (3, 12): from typing import Any + from typing_extensions import Self else: from typing import Any, Self @@ -9,11 +10,10 @@ from pydantic import Field from ..base import ObjectExtended - from .example import Example from .general import Reference -from .schemas import Schema from .parameter import Header +from .schemas import Schema class Encoding(ObjectExtended): diff --git a/src/aiopenapi3/v32/parameter.py b/src/aiopenapi3/v32/parameter.py index 3072ff0d..fedc14e3 100644 --- a/src/aiopenapi3/v32/parameter.py +++ b/src/aiopenapi3/v32/parameter.py @@ -1,17 +1,16 @@ import enum import typing -from typing import Union, Any +from typing import Any, Union from pydantic import Field -from ..base import ObjectExtended, ParameterBase as _ParameterBase - +from ..base import ObjectExtended +from ..base import ParameterBase as _ParameterBase +from ..v30.parameter import _ParameterCodec from .example import Example from .general import Reference from .schemas import Schema -from ..v30.parameter import _ParameterCodec - if typing.TYPE_CHECKING: from .paths import MediaType diff --git a/src/aiopenapi3/v32/paths.py b/src/aiopenapi3/v32/paths.py index 578f2ea4..c71f862c 100644 --- a/src/aiopenapi3/v32/paths.py +++ b/src/aiopenapi3/v32/paths.py @@ -1,14 +1,13 @@ -from typing import Union, Any +from typing import Any, Union -from pydantic import Field, model_validator, RootModel +from pydantic import Field, RootModel, model_validator -from ..base import ObjectExtended, PathsBase, OperationBase, PathItemBase -from .general import ExternalDocumentation -from .general import Reference +from ..base import ObjectExtended, OperationBase, PathItemBase, PathsBase +from .general import ExternalDocumentation, Reference from .media import MediaType from .parameter import Header, Parameter -from .servers import Server from .security import SecurityRequirement +from .servers import Server class RequestBody(ObjectExtended): diff --git a/src/aiopenapi3/v32/root.py b/src/aiopenapi3/v32/root.py index bd3cf559..a51fc35d 100644 --- a/src/aiopenapi3/v32/root.py +++ b/src/aiopenapi3/v32/root.py @@ -4,14 +4,12 @@ from pydantic import Field, model_validator from ..base import ObjectExtended, RootBase - +from .components import Components +from .general import Reference from .info import Info -from .paths import Paths, PathItem +from .paths import PathItem, Paths from .security import SecurityRequirement from .servers import Server - -from .components import Components -from .general import Reference from .tag import Tag diff --git a/src/aiopenapi3/v32/schemas.py b/src/aiopenapi3/v32/schemas.py index 6a7b6384..690b8637 100644 --- a/src/aiopenapi3/v32/schemas.py +++ b/src/aiopenapi3/v32/schemas.py @@ -1,9 +1,9 @@ import typing -from typing import Union, Any, Optional +from typing import Any, Optional, Union -from pydantic import Field, model_validator, ConfigDict +from pydantic import ConfigDict, Field, model_validator -from ..base import ObjectExtended, SchemaBase, DiscriminatorBase +from ..base import DiscriminatorBase, ObjectExtended, SchemaBase from .xml import XML if typing.TYPE_CHECKING: diff --git a/src/aiopenapi3/v32/security.py b/src/aiopenapi3/v32/security.py index bb5c9cd4..1a007783 100644 --- a/src/aiopenapi3/v32/security.py +++ b/src/aiopenapi3/v32/security.py @@ -1,6 +1,6 @@ from pathlib import Path +from typing import Annotated, Literal -from typing import Union, Annotated, Literal from pydantic import Field, RootModel, constr from ..base import ObjectExtended @@ -79,13 +79,11 @@ class openIdConnect(_SecurityScheme): class SecurityScheme( RootModel[ Annotated[ - Union[ - _SecuritySchemes.apiKey, - _SecuritySchemes.http, - _SecuritySchemes.mutualTLS, - _SecuritySchemes.oauth2, - _SecuritySchemes.openIdConnect, - ], + _SecuritySchemes.apiKey + | _SecuritySchemes.http + | _SecuritySchemes.mutualTLS + | _SecuritySchemes.oauth2 + | _SecuritySchemes.openIdConnect, Field(discriminator="type"), ] ] @@ -97,8 +95,6 @@ class SecurityScheme( .. _here: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#security-scheme-object """ - pass - class SecurityRequirement(RootModel[dict[str, list[str]]]): """ @@ -108,5 +104,3 @@ class SecurityRequirement(RootModel[dict[str, list[str]]]): .. _here: https://spec.openapis.org/oas/v3.2.0.html#security-requirement-object """ - - pass diff --git a/src/aiopenapi3/v32/servers.py b/src/aiopenapi3/v32/servers.py index 3737b53f..f542571f 100644 --- a/src/aiopenapi3/v32/servers.py +++ b/src/aiopenapi3/v32/servers.py @@ -48,13 +48,12 @@ def validate_server_url_parameters(self) -> "Server": def validate_parameter_enum(self, parameters: dict[str, str]): for name, value in parameters.items(): - if v := self.variables.get(name): - if v.enum and value not in v.enum: - raise ValueError(f"Server Variable {name} value {value} not allowed ({v.enum})") + if (v := self.variables.get(name)) and v.enum and value not in v.enum: + raise ValueError(f"Server Variable {name} value {value} not allowed ({v.enum})") def createUrl(self, variables: dict[str, str]) -> str: self.validate_parameter_enum(variables) - vars: dict[str, str | None] = dict(map(lambda x: (x[0], x[1].default), self.variables.items())) + vars: dict[str, str | None] = {x[0]: x[1].default for x in self.variables.items()} vars.update(variables) url: str = self.url.format(**vars) return url diff --git a/tests/api/v1/main.py b/tests/api/v1/main.py index 1a744a90..7ae03dea 100644 --- a/tests/api/v1/main.py +++ b/tests/api/v1/main.py @@ -1,15 +1,16 @@ import errno +from typing import Annotated import starlette.status -from fastapi import APIRouter, Body, Response, Path +from fastapi import APIRouter, Body, Path, Response from fastapi.responses import JSONResponse -from .schema import Pets, Pet, PetCreate, Error +from .schema import Error, Pet, PetCreate, Pets router = APIRouter(prefix="/v1") -ZOO = dict() +ZOO = {} def _idx(l): @@ -40,7 +41,7 @@ def _idx(l): ) def createPet( response: Response, - pet: PetCreate = Body(..., embed=True), + pet: Annotated[PetCreate, Body(..., embed=True)], ) -> None: if pet.name in ZOO: return JSONResponse( @@ -61,14 +62,13 @@ def listPet(limit: int | None = None) -> Pets: @router.get("/pets/{petId}", operation_id="getPet", response_model=Pet, responses={404: {"model": Error}}) def getPet(pet_id: int = Path(..., alias="petId")) -> Pets: - for k, v in ZOO.items(): + for v in ZOO.values(): if pet_id == v.id: return v - else: - return JSONResponse( - status_code=starlette.status.HTTP_404_NOT_FOUND, - content=Error(code=errno.ENOENT, message=f"{pet_id} not found").model_dump(), - ) + return JSONResponse( + status_code=starlette.status.HTTP_404_NOT_FOUND, + content=Error(code=errno.ENOENT, message=f"{pet_id} not found").model_dump(), + ) @router.delete("/pets/{petId}", operation_id="deletePet", responses={204: {"model": None}, 404: {"model": Error}}) @@ -78,9 +78,8 @@ def deletePet(response: Response, pet_id: int = Path(..., alias="petId")) -> Pet del ZOO[k] response.status_code = starlette.status.HTTP_204_NO_CONTENT return response - else: - return JSONResponse( - status_code=starlette.status.HTTP_404_NOT_FOUND, - content=Error(code=errno.ENOENT, message=f"{pet_id} not found").model_dump(), - media_type="application/json; utf-8", - ) + return JSONResponse( + status_code=starlette.status.HTTP_404_NOT_FOUND, + content=Error(code=errno.ENOENT, message=f"{pet_id} not found").model_dump(), + media_type="application/json; utf-8", + ) diff --git a/tests/api/v1/schema.py b/tests/api/v1/schema.py index 759c8768..e06b15b7 100644 --- a/tests/api/v1/schema.py +++ b/tests/api/v1/schema.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, RootModel, Field +from pydantic import BaseModel, Field, RootModel class PetBase(BaseModel): diff --git a/tests/api/v2/main.py b/tests/api/v2/main.py index 360abb01..d3d6f03f 100644 --- a/tests/api/v2/main.py +++ b/tests/api/v2/main.py @@ -3,14 +3,14 @@ from typing import Annotated import starlette.status -from fastapi import Body, Response, Header, APIRouter, Path +from fastapi import APIRouter, Body, Header, Path, Response from fastapi.responses import JSONResponse from . import schema router = APIRouter(prefix="/v2") -ZOO = dict() +ZOO = {} def _idx(l): @@ -28,7 +28,7 @@ def _idx(l): ) def createPet( response: Response, - pet: schema.Pet = Body(..., embed=True), + pet: Annotated[schema.Pet, Body(..., embed=True)], ) -> schema.Pet: # if isinstance(pet, Cat): # pet = pet.__root__ @@ -53,14 +53,13 @@ def listPet(limit: int | None = None) -> schema.Pets: @router.get("/pets/{petId}", operation_id="getPet", response_model=schema.Pet, responses={404: {"model": schema.Error}}) def getPet(pet_id: str = Path(..., alias="petId")) -> schema.Pets: - for k, pet in ZOO.items(): + for pet in ZOO.values(): if pet_id == pet.identifier: return pet - else: - return JSONResponse( - status_code=starlette.status.HTTP_404_NOT_FOUND, - content=schema.Error(code=errno.ENOENT, message=f"{pet_id} not found").model_dump(), - ) + return JSONResponse( + status_code=starlette.status.HTTP_404_NOT_FOUND, + content=schema.Error(code=errno.ENOENT, message=f"{pet_id} not found").model_dump(), + ) @router.delete( @@ -76,11 +75,10 @@ def deletePet( del ZOO[k] response.status_code = starlette.status.HTTP_204_NO_CONTENT return response - else: - return JSONResponse( - status_code=starlette.status.HTTP_404_NOT_FOUND, - content=schema.Error(code=errno.ENOENT, message=f"{pet_id} not found").model_dump(), - ) + return JSONResponse( + status_code=starlette.status.HTTP_404_NOT_FOUND, + content=schema.Error(code=errno.ENOENT, message=f"{pet_id} not found").model_dump(), + ) @router.patch("/pets", operation_id="patchPets", responses={200: {"model": schema.Pets}}) diff --git a/tests/api/v2/schema.py b/tests/api/v2/schema.py index ee7468f1..2440979b 100644 --- a/tests/api/v2/schema.py +++ b/tests/api/v2/schema.py @@ -1,10 +1,8 @@ -from datetime import timedelta import uuid +from datetime import timedelta +from typing import Annotated, Literal - -from typing import Literal, Union, Annotated - -from pydantic import BaseModel, RootModel, Field, ConfigDict +from pydantic import BaseModel, ConfigDict, Field, RootModel # from pydantic.fields import Undefined @@ -28,7 +26,7 @@ class WhiteCat(PetBase): white_name: str -class Cat(RootModel[Annotated[Union[BlackCat, WhiteCat], Field(discriminator="color")]]): +class Cat(RootModel[Annotated[BlackCat | WhiteCat, Field(discriminator="color")]]): def __getattr__(self, item): return getattr(self.root, item) @@ -42,7 +40,7 @@ class Dog(PetBase): age: timedelta -class Pet(RootModel[Annotated[Union[Cat, Dog], Field(discriminator="pet_type")]]): +class Pet(RootModel[Annotated[Cat | Dog, Field(discriminator="pet_type")]]): def __getattr__(self, item): return getattr(self.root, item) diff --git a/tests/apiv1_test.py b/tests/apiv1_test.py index 40deba3f..5e8d64f9 100644 --- a/tests/apiv1_test.py +++ b/tests/apiv1_test.py @@ -4,17 +4,14 @@ import pytest import pytest_asyncio +# pytest.skip(allow_module_level=True) +from api.v1.main import router +from fastapi import FastAPI from hypercorn.asyncio import serve from hypercorn.config import Config -from fastapi import FastAPI - import aiopenapi3 -# pytest.skip(allow_module_level=True) - -from api.v1.main import router - app = FastAPI( version="1.0.0", title="Dorthu's Petstore", servers=[{"url": "/", "description": "Default, relative server"}] ) @@ -63,14 +60,14 @@ async def test_createPet(server, client): @pytest.mark.asyncio(loop_scope="session") async def test_listPet(server, client): - h, r = await asyncio.to_thread(client._.createPet, **randomPet(uuid.uuid4())) + _h, _r = await asyncio.to_thread(client._.createPet, **randomPet(uuid.uuid4())) l = await asyncio.to_thread(client._.listPet) assert len(l) > 0 @pytest.mark.asyncio(loop_scope="session") async def test_getPet(server, client): - h, pet = await asyncio.to_thread(client._.createPet, **randomPet(uuid.uuid4())) + _h, pet = await asyncio.to_thread(client._.createPet, **randomPet(uuid.uuid4())) r = await asyncio.to_thread(client._.getPet, parameters={"petId": pet.id}) # FastAPI 0.101 Serialization changes # assert type(r).model_json_schema() == type(pet).model_json_schema() diff --git a/tests/apiv2_test.py b/tests/apiv2_test.py index 18dd67fe..6778fe52 100644 --- a/tests/apiv2_test.py +++ b/tests/apiv2_test.py @@ -1,16 +1,19 @@ +import asyncio import datetime import random -import asyncio +import typing import uuid from typing import ForwardRef -import typing - import pydantic - import pytest import pytest_asyncio +from api.v2.main import router +from api.v2.schema import Dog as _Dog +# pytest.skip(allow_module_level=True) +from fastapi import FastAPI +from fastapi.responses import PlainTextResponse from hypercorn.asyncio import serve from hypercorn.config import Config @@ -18,16 +21,6 @@ from aiopenapi3 import OpenAPI from aiopenapi3.v31.schemas import Schema -from api.v2.schema import Dog as _Dog - -# pytest.skip(allow_module_level=True) - -from fastapi import FastAPI -from fastapi.responses import PlainTextResponse - - -from api.v2.main import router - app = FastAPI( version="1.0.0", title="Dorthu's Petstore", servers=[{"url": "/", "description": "Default, relative server"}] ) @@ -149,8 +142,8 @@ def randomPet(client, name=None, cat=False): @pytest.mark.asyncio(loop_scope="session") async def test_Request(server, client): - client._.createPet.data - client._.createPet.parameters + assert client._.createPet.data is not None + assert client._.createPet.parameters == [] client._.createPet.args() client._.createPet.return_value() @@ -276,13 +269,13 @@ def test_allOf_resolution(openapi_version, petstore_expanded): items = typing.get_args(ref.__fields__["__root__"].outer_type_)[0].__fields__ try: - assert sorted(map(lambda x: x.name, filter(lambda y: y.required, items.values()))) == sorted(["id", "name"]), ( + assert sorted(x.name for x in filter(lambda y: y.required, items.values())) == sorted(["id", "name"]), ( ref.model_json_schema() ) - except Exception as e: + except Exception as e: # noqa: BLE001 print(e) - assert sorted(map(lambda x: x.name, items.values())) == ["id", "name", "tag"] + assert sorted(x.name for x in items.values()) == ["id", "name", "tag"] assert items["id"].outer_type_ is int assert items["name"].outer_type_ is str diff --git a/tests/cli_test.py b/tests/cli_test.py index 56e09809..5f9adee0 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -1,11 +1,10 @@ +import json import os import shlex from pathlib import Path -import json - -from aiopenapi3.cli import main import aiopenapi3.log +from aiopenapi3.cli import main def test_validate_cli(): diff --git a/tests/clone_test.py b/tests/clone_test.py index 56dc3eef..194ee175 100644 --- a/tests/clone_test.py +++ b/tests/clone_test.py @@ -2,7 +2,6 @@ from aiopenapi3 import OpenAPI - """ https://github.com/pydantic/pydantic/issues/6010 """ diff --git a/tests/conftest.py b/tests/conftest.py index 26bfea31..868e43f7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,5 @@ -import os import dataclasses - +import os from pathlib import Path import pytest @@ -40,7 +39,7 @@ def __repr__(self): @property def schema(self): - return getattr(getattr(aiopenapi3, f"v{self.major}{self.minor}"), "Schema") + 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")) @@ -81,6 +80,7 @@ def _get_parsed_yaml(filename, version=None): with open("tests/fixtures/" + filename) as f: raw = f.read() import yaml + from aiopenapi3.loader import YAML12Loader parsed = yaml.load(raw, Loader=YAML12Loader) diff --git a/tests/content_length_test.py b/tests/content_length_test.py index 4e4e5d35..69cea32d 100644 --- a/tests/content_length_test.py +++ b/tests/content_length_test.py @@ -1,18 +1,15 @@ import asyncio import random -from hypercorn.asyncio import serve -from hypercorn.config import Config -from fastapi import FastAPI, Request, Response, Query -from fastapi.responses import PlainTextResponse - import pytest import pytest_asyncio - +from fastapi import FastAPI, Query, Request, Response +from fastapi.responses import PlainTextResponse +from hypercorn.asyncio import serve +from hypercorn.config import Config import aiopenapi3 - app = FastAPI(version="1.0.0", title="TLS tests", servers=[{"url": "/", "description": "Default, relative server"}]) @@ -50,16 +47,16 @@ def content_length(request: Request, response: Response, content_length: int = Q @pytest.mark.asyncio(loop_scope="session") async def test_content_length_exceeded(server, client): cl = random.randint(1, client._max_response_content_length) - r = await client._.content_length(parameters=dict(content_length=cl)) + r = await client._.content_length(parameters={"content_length": cl}) assert len(r) == cl cl = client._max_response_content_length - r = await client._.content_length(parameters=dict(content_length=cl)) + r = await client._.content_length(parameters={"content_length": cl}) assert len(r) == cl with pytest.raises(aiopenapi3.errors.ContentLengthExceededError): cl = client._max_response_content_length + 1 - await client._.content_length(parameters=dict(content_length=cl)) + await client._.content_length(parameters={"content_length": cl}) @pytest.mark.asyncio(loop_scope="session") @@ -70,13 +67,13 @@ async def test_sync_content_length_exceeded(server): ) cl = random.randint(1, client._max_response_content_length) - r = await asyncio.to_thread(client._.content_length, parameters=dict(content_length=cl)) + r = await asyncio.to_thread(client._.content_length, parameters={"content_length": cl}) assert len(r) == cl cl = client._max_response_content_length - r = await asyncio.to_thread(client._.content_length, parameters=dict(content_length=cl)) + r = await asyncio.to_thread(client._.content_length, parameters={"content_length": cl}) assert len(r) == cl with pytest.raises(aiopenapi3.errors.ContentLengthExceededError): cl = client._max_response_content_length + 1 - await asyncio.to_thread(client._.content_length, parameters=dict(content_length=cl)) + await asyncio.to_thread(client._.content_length, parameters={"content_length": cl}) diff --git a/tests/debug_test.py b/tests/debug_test.py index bc2c1406..a286c260 100644 --- a/tests/debug_test.py +++ b/tests/debug_test.py @@ -1,5 +1,5 @@ -import pytest import httpx2 +import pytest import aiopenapi3.debug from aiopenapi3 import OpenAPI, ResponseSchemaError diff --git a/tests/error_test.py b/tests/error_test.py index ce11958a..8f0823d0 100644 --- a/tests/error_test.py +++ b/tests/error_test.py @@ -1,11 +1,15 @@ -from aiopenapi3 import OpenAPI -from aiopenapi3 import ResponseSchemaError, ContentTypeError, HTTPStatusError, ResponseDecodingError, RequestError - import httpx2 - - import pytest +from aiopenapi3 import ( + ContentTypeError, + HTTPStatusError, + OpenAPI, + RequestError, + ResponseDecodingError, + ResponseSchemaError, +) + def test_response_error(httpx_mock, with_paths_response_error_vXX): api = OpenAPI("/", with_paths_response_error_vXX, session_factory=httpx2.Client) diff --git a/tests/extra_test.py b/tests/extra_test.py index b8b535af..42060dd5 100644 --- a/tests/extra_test.py +++ b/tests/extra_test.py @@ -1,15 +1,13 @@ import re import typing - from pathlib import Path import httpx2 import pytest from aiopenapi3 import OpenAPI -from aiopenapi3.loader import FileSystemLoader - from aiopenapi3.extra import Cull, Reduce +from aiopenapi3.loader import FileSystemLoader if typing.TYPE_CHECKING: from aiopenapi3.plugin import Document @@ -62,7 +60,7 @@ def parsed(self, ctx: "Document.Context") -> "Document.Context": # Check if description matches the desired format if description.strip() == "Usage: on='{on}'": parameter["name"] = "on" - if "content" in parameter.keys(): + if "content" in parameter: parameter["schema"] = parameter["content"].get("application/json", {}).get("schema", {}) del parameter["content"] # Drop requirement for @odata.type since it's not actually enforced @@ -102,7 +100,6 @@ def test_reduced_small(): loader=FileSystemLoader(Path("tests/").absolute()), plugins=[PetStoreReduced()], ) - return @pytest.mark.parametrize("compressor", [Reduce, Cull]) @@ -140,13 +137,13 @@ def test_reduced(with_extra_reduced, httpx2_mock, compressor): assert "A" in api.components.responses assert "A" in api.components.requestBodies - httpx2_mock.add_response(headers={"Content-Type": "application/json", "X-A": "A"}, json=dict(a=1)) + httpx2_mock.add_response(headers={"Content-Type": "application/json", "X-A": "A"}, json={"a": 1}) from aiopenapi3.request import RequestBase req: RequestBase = api._.A data = req.data.get_type().model_construct(a="a") - headers, payload = req(data=data, parameters=dict(Path="a"), return_headers=True) + headers, payload = req(data=data, parameters={"Path": "a"}, return_headers=True) assert payload.a == 1 assert headers["X-A"] == "A" @@ -204,7 +201,7 @@ def test_reduced(with_extra_reduced, httpx2_mock, compressor): from aiopenapi3.extra import Cookies -@pytest.mark.parametrize("cookie", [dict(policy="jar"), dict(policy="securitySchemes")], ids=["jar", "securityScheme"]) +@pytest.mark.parametrize("cookie", [{"policy": "jar"}, {"policy": "securitySchemes"}], ids=["jar", "securityScheme"]) def test_cookies(httpx2_mock, with_extra_cookie, cookie): api = OpenAPI( diff --git a/tests/formdata_test.py b/tests/formdata_test.py index de7cf6cc..b40bfacb 100644 --- a/tests/formdata_test.py +++ b/tests/formdata_test.py @@ -1,8 +1,9 @@ from pathlib import Path + import httpx2 from aiopenapi3 import OpenAPI -from aiopenapi3.v30.formdata import encode_multipart_parameters, MultipartParameter +from aiopenapi3.v30.formdata import MultipartParameter, encode_multipart_parameters def test_encode_formdata(): @@ -13,11 +14,11 @@ def test_encode_formdata(): schema = Schema() ITEMS = [ - MultipartParameter("text", "text/plain", "bar", dict(), schema), + MultipartParameter("text", "text/plain", "bar", {}, schema), MultipartParameter("text", "text/plain", "bar", {"X-HEAD": "text"}, schema), - MultipartParameter("audio", "audio/wav", b"jd", dict(), schema), - MultipartParameter("image", "image/png", b"jd", dict(), schema), - MultipartParameter("data", "application/octet-stream", data, dict(), schema), + MultipartParameter("audio", "audio/wav", b"jd", {}, schema), + MultipartParameter("image", "image/png", b"jd", {}, schema), + MultipartParameter("data", "application/octet-stream", data, {}, schema), MultipartParameter("mph", "application/octet-stream", data, {"X-HEAD": "mph"}, schema), ] @@ -65,7 +66,7 @@ def test_formdata_encoding(httpx2_mock, with_paths_requestbody_formdata_encoding ) assert msg.defects == [] and msg.is_multipart() - r = dict() + r = {} for p in msg.get_payload(): name = p.get_param("name", header="content-disposition") payload = p.get_payload(decode=True) diff --git a/tests/forms_test.py b/tests/forms_test.py index c1d444b9..b86f6ab3 100644 --- a/tests/forms_test.py +++ b/tests/forms_test.py @@ -2,12 +2,16 @@ import copy import datetime import decimal +from typing import ClassVar -import aiopenapi3 - -from flask import Flask, render_template_string, Blueprint, abort +import pytest +import pytest_asyncio +from asgiref.wsgi import WsgiToAsgi +from flask import Blueprint, Flask, Response, abort, render_template_string, request from flask_bootstrap import Bootstrap4 as Bootstrap from flask_wtf import FlaskForm +from hypercorn.asyncio import serve +from hypercorn.config import Config from wtforms.fields import ( BooleanField, DateField, @@ -17,34 +21,26 @@ DecimalRangeField, EmailField, FileField, - MultipleFileField, FloatField, + HiddenField, IntegerField, IntegerRangeField, + MultipleFileField, + PasswordField, RadioField, - SelectField, SearchField, + SelectField, SelectMultipleField, - SubmitField, StringField, + SubmitField, TelField, + TextAreaField, TimeField, URLField, - HiddenField, - PasswordField, - TextAreaField, ) - from wtforms.validators import DataRequired, NumberRange -from flask import request, Response - -from asgiref.wsgi import WsgiToAsgi -import pytest -import pytest_asyncio - -from hypercorn.asyncio import serve -from hypercorn.config import Config +import aiopenapi3 # csrf.exempt(serve_test) @@ -83,7 +79,7 @@ class Meta: class SelectForm(FlaskForm): - CHOICES = [("cpp", "C++"), ("py", "Python"), ("txt", "Plain Text"), ("rb", "Ruby"), ("c", "C")] + CHOICES: ClassVar = [("cpp", "C++"), ("py", "Python"), ("txt", "Plain Text"), ("rb", "Ruby"), ("c", "C")] class Meta: csrf = False @@ -290,7 +286,7 @@ def form_type(request): async def client(server, form_type, with_paths_requestbody_formdata_wtforms): data = copy.deepcopy(with_paths_requestbody_formdata_wtforms) if form_type != "multipart/form-data": - for op, v in data["paths"].items(): + for v in data["paths"].values(): v["post"]["requestBody"]["content"][form_type] = v["post"]["requestBody"]["content"]["multipart/form-data"] del v["post"]["requestBody"]["content"]["multipart/form-data"] @@ -334,11 +330,11 @@ async def test_String(server, client, form_type): @pytest.mark.asyncio(loop_scope="session") async def test_DateTime(server, client, form_type): cls = client._.datetime.operation.requestBody.content[form_type].schema_.get_type() - now = datetime.datetime.now() + t = (2026, 8, 29, 15, 4, 22, 32582) + dtl = datetime.datetime(*t) # noqa: DTZ001 + dt = dtl.astimezone(datetime.timezone.utc) - data = cls( - time=now.time(), date=now.date(), datetime=datetime.datetime.now(tz=datetime.timezone.utc), datetimelocal=now - ) + data = cls(time=dt.time(), date=dt.date(), datetime=dt, datetimelocal=dtl) r = await client._.datetime(data=data) assert r == "ok" @@ -353,8 +349,8 @@ async def test_Numbers(server, client, form_type): decimal=decimal.Decimal(1 / 3), decimalrange=decimal.Decimal(2 / 3), float=float(1 / 3), - integer=int(9), - integerrange=int(9), + integer=(9), + integerrange=(9), ) r = await client._.numbers(data=data) diff --git a/tests/loader_test.py b/tests/loader_test.py index 5c5817a4..62140534 100644 --- a/tests/loader_test.py +++ b/tests/loader_test.py @@ -1,11 +1,11 @@ import json - from pathlib import Path -import yarl import pytest -from aiopenapi3 import OpenAPI, FileSystemLoader, ReferenceResolutionError -from aiopenapi3.loader import Loader, Plugins, NullLoader +import yarl + +from aiopenapi3 import FileSystemLoader, OpenAPI, ReferenceResolutionError +from aiopenapi3.loader import Loader, NullLoader, Plugins SPECTPL = """ openapi: "3.0.0" @@ -77,8 +77,9 @@ def test_webload(): # FIXME https://github.com/pydantic/pydantic/issues/5730 pytest.skip() name = "https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/network/resource-manager/Microsoft.Network/stable/2018-10-01/serviceEndpointPolicy.json" - from aiopenapi3.loader import WebLoader import yarl + from aiopenapi3.loader import WebLoader + loader = WebLoader(yarl.URL(name)) api = OpenAPI.load_sync(name, loader=loader) diff --git a/tests/parse_data_test.py b/tests/parse_data_test.py index 6eda87a8..a7945012 100644 --- a/tests/parse_data_test.py +++ b/tests/parse_data_test.py @@ -1,7 +1,7 @@ -import pytest - import pathlib +from typing import ClassVar +import pytest import yarl from aiopenapi3 import FileSystemLoader, OpenAPI @@ -21,7 +21,7 @@ def pytest_generate_tests(metafunc): @pytest.mark.skip class TestParseData: # a map specifying multiple argument sets for a test method - params = { + params: ClassVar = { "test_data": [("dir", "file"), "tests/data", lambda x: x.is_file() and x.suffix in (".json", ".yaml")], "test_data_open5gs": [ ("dir", "file"), diff --git a/tests/parsing_test.py b/tests/parsing_test.py index 6126b531..acd8afa1 100644 --- a/tests/parsing_test.py +++ b/tests/parsing_test.py @@ -4,12 +4,11 @@ import uuid - import pytest - from pydantic import ValidationError + from aiopenapi3 import OpenAPI, ReferenceResolutionError -from aiopenapi3.errors import OperationParameterValidationError, OperationIdDuplicationError +from aiopenapi3.errors import OperationIdDuplicationError, OperationParameterValidationError URLBASE = "/" @@ -113,12 +112,10 @@ def test_parsing_paths_links_invalid(with_parsing_paths_links_invalid): spec = OpenAPI(URLBASE, with_parsing_paths_links_invalid) assert all( - [ - i in str(e.value) - for i in [ - "operationId and operationRef are mutually exclusive, only one of them is allowed", - "operationId and operationRef are mutually exclusive, one of them must be specified", - ] + i in str(e.value) + for i in [ + "operationId and operationRef are mutually exclusive, only one of them is allowed", + "operationId and operationRef are mutually exclusive, one of them must be specified", ] ) @@ -182,8 +179,8 @@ def test_parsing_schema_names(with_parsing_schema_names): def test_pydantic_classes(): - from typing import Union, ForwardRef, Annotated, Literal import types + from typing import Annotated, ForwardRef, Literal, Union from pydantic import BaseModel, Field diff --git a/tests/path_test.py b/tests/path_test.py index 03d754b2..312b1e9d 100644 --- a/tests/path_test.py +++ b/tests/path_test.py @@ -4,20 +4,21 @@ import base64 import copy -import uuid import pathlib +import re +import uuid -import pytest import httpx2 +import pytest import yarl from aiopenapi3 import OpenAPI from aiopenapi3.errors import ( - OperationParameterValidationError, - OperationIdDuplicationError, HeadersMissingError, HTTPClientError, HTTPServerError, + OperationIdDuplicationError, + OperationParameterValidationError, ) URLBASE = "/" @@ -164,14 +165,6 @@ def test_paths_security(httpx2_mock, with_paths_security): request = httpx2_mock.get_requests()[-1] assert request.headers["Authorization"].split(" ")[1] == base64.b64encode((auth + ":" + auth).encode()).decode() - try: - pass - except Exception: - api.authenticate(None, digestAuth=(auth, auth)) - api._.api_v1_auth_login_create(data={}, parameters={}) - request = httpx2_mock.get_requests()[-1] - # can't test? - api.authenticate(None, bearerAuth=auth) api._.api_v1_auth_login_create(data={}, parameters={}) request = httpx2_mock.get_requests()[-1] @@ -183,6 +176,26 @@ def test_paths_security(httpx2_mock, with_paths_security): request = httpx2_mock.get_requests()[-1] +def test_paths_security_digest(httpx2_mock, with_paths_security): + api = OpenAPI(URLBASE, with_paths_security, session_factory=httpx2.Client, use_operation_tags=False) + auth = str(uuid.uuid4()) + httpx2_mock.add_response( + status_code=401, + headers={ + "WWW-Authenticate": """Digest realm="testrealm@host.com",qop="auth,auth-int",nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",opaque="5ccc069c403ebaf9f0171e9517f40e41" """ + }, + ) + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json="user") + + api.authenticate(None, digestAuth=(auth, auth)) + api._.api_v1_auth_login_create(data={}, parameters={}) + request = httpx2_mock.get_requests()[-1] + + reg = re.compile(r'([^\s,]+) ?[=] ?"?([^\s,"]+)"?') + v = dict(reg.findall(request.headers["Authorization"])) + assert v["username"] == auth + + def test_paths_security_combined(httpx2_mock, with_paths_security): api = OpenAPI(URLBASE, with_paths_security, session_factory=httpx2.Client, use_operation_tags=False) httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json="user") @@ -194,7 +207,7 @@ def test_paths_security_combined(httpx2_mock, with_paths_security): with pytest.raises(ValueError, match="No security requirement satisfied"): r = api._.api_v1_auth_login_combined(data={}, parameters={}) - api.authenticate(**{"user": "theuser", "token": "thetoken"}) + api.authenticate(user="theuser", token="thetoken") r = api._.api_v1_auth_login_combined(data={}, parameters={}) api.authenticate(None) @@ -348,9 +361,7 @@ def test_paths_parameter_format(httpx2_mock, with_paths_parameter_format): r = api._.deepObjectNestedExplodeQuery(parameters={"object": data}) request = httpx2_mock.get_requests()[-1] u = yarl.URL(str(request.url)) - expected = dict( - list(map(lambda x: (f"object{''.join('[inner]' for _ in range(x))}[size]", depth - x), range(depth))) - ) + expected = {f"object{''.join('[inner]' for _ in range(x))}[size]": depth - x for x in range(depth)} # 'object[size]=3&object[inner][size]=2&object[inner][inner][size]=1' assert all(u.query[k] == str(v) for k, v in expected.items()) @@ -395,8 +406,6 @@ def test_paths_parameter_format(httpx2_mock, with_paths_parameter_format): assert u.parts[9] == "100" assert u.parts[10] == "3.3245460039402305e+23" - return - @pytest.mark.httpx2_mock(can_send_already_matched_responses=True) def test_paths_parameter_format_complex(httpx2_mock, with_paths_parameter_format_complex): @@ -414,7 +423,7 @@ def test_paths_response_header(httpx2_mock, with_paths_response_header): ) api = OpenAPI(URLBASE, with_paths_response_header, session_factory=httpx2.Client) - h, b = api._.get(return_headers=True) + h, _b = api._.get(return_headers=True) request = httpx2_mock.get_requests()[-1] assert isinstance(h["X-required"], str) @@ -423,14 +432,13 @@ def test_paths_response_header(httpx2_mock, with_paths_response_header): with pytest.raises(HeadersMissingError) as e: httpx2_mock.add_response(headers={"Content-Type": "application/json", "X-optional": "1,2,3"}, json="get") - h, b = api._.get(return_headers=True) + _h, _b = api._.get(return_headers=True) assert list(e.value.missing.keys()) == ["x-required"] httpx2_mock.add_response(headers={"Content-Type": "application/json", "X-object": "A,1,B,2,C,3"}, json="types") - h, b = api._.types(return_headers=True) + h, _b = api._.types(return_headers=True) assert h["X-object"].A == 1 assert h["X-object"].B == "2" - return @pytest.mark.httpx2_mock(can_send_already_matched_responses=True) @@ -474,7 +482,7 @@ def test_paths_tags(httpx2_mock, with_paths_tags): OpenAPI(URLBASE, with_paths_tags, session_factory=httpx2.Client, use_operation_tags=False) spec = copy.deepcopy(with_paths_tags) - for k in {"/user/", "/item/"}: + for k in ("/user/", "/item/"): spec["paths"][k]["get"]["operationId"] = f"list{k[1:-1]}" api = OpenAPI(URLBASE, spec, session_factory=httpx2.Client, use_operation_tags=False) @@ -511,7 +519,7 @@ def test_paths_response_status_pattern_default(httpx2_mock, with_paths_response_ def test_paths_response_error(mocker, httpx2_mock, with_paths_response_error_vXX): - from aiopenapi3 import ResponseSchemaError, ContentTypeError, HTTPStatusError, ResponseDecodingError + from aiopenapi3 import ContentTypeError, HTTPStatusError, ResponseDecodingError, ResponseSchemaError api = OpenAPI("/", with_paths_response_error_vXX, session_factory=httpx2.Client) @@ -599,8 +607,6 @@ def test_paths_servers(httpx2_mock, with_paths_servers): request = httpx2_mock.get_requests()[-1] assert request.url.host == "operation" - return - @pytest.mark.httpx2_mock(can_send_already_matched_responses=True) def test_paths_server_variables(httpx2_mock, with_paths_server_variables): @@ -621,7 +627,7 @@ def test_paths_server_variables(httpx2_mock, with_paths_server_variables): with pytest.raises(ValueError, match="Server Variable host value defoned not allowed"): api._.servers() - api._server_variables = dict() + api._server_variables = {} httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=204) r = api._.path() diff --git a/tests/pathv20_test.py b/tests/pathv20_test.py index dd72d23f..78dda3ae 100644 --- a/tests/pathv20_test.py +++ b/tests/pathv20_test.py @@ -1,11 +1,11 @@ import io -import uuid import urllib +import uuid -import yarl import httpx2 import pytest import python_multipart +import yarl from httpx2._multipart import MultipartStream from aiopenapi3 import OpenAPI @@ -66,7 +66,7 @@ def test_paths_security_v20_combined_securityparameters(httpx2_mock, with_paths_ with pytest.raises(ValueError, match="No security requirement satisfied"): api._.combinedSecurity(data={}, parameters={}) - api.authenticate(**{"user": "u", "token": "t"}) + api.authenticate(user="u", token="t") api._.combinedSecurity(data={}, parameters={}) api.authenticate(None) @@ -85,7 +85,7 @@ def test_paths_security_v20_alternate_securityparameters(httpx2_mock, with_paths ): api._.alternateSecurity(data={}, parameters={}) - api.authenticate(**{"user": "u", "token": "t"}) + api.authenticate(user="u", token="t") api._.alternateSecurity(data={}, parameters={}) api.authenticate(None) @@ -133,7 +133,7 @@ def test_paths_response_header_v20(httpx2_mock, with_paths_response_header_v20): headers={"Content-Type": "application/json", "X-required": "1", "X-optional": "1,2,3"}, json="get" ) api = OpenAPI(URLBASE, with_paths_response_header_v20, session_factory=httpx2.Client) - h, b = api._.get(return_headers=True) + h, _b = api._.get(return_headers=True) request = httpx2_mock.get_requests()[-1] assert isinstance(h["X-required"], str) @@ -148,8 +148,6 @@ def test_paths_response_header_v20(httpx2_mock, with_paths_response_header_v20): # h, b = api._.get(return_headers=True) # request = httpx2_mock.get_requests()[-1] - return - @pytest.mark.httpx2_mock(can_send_already_matched_responses=True) def test_paths_parameter_format_v20(httpx2_mock, with_paths_parameter_format_v20): @@ -179,7 +177,7 @@ def test_paths_parameter_format_v20(httpx2_mock, with_paths_parameter_format_v20 result = api._.formdata(parameters=params) request = httpx2_mock.get_requests()[-1] - files = dict() + files = {} def on_file(file): file.file_object.seek(0) @@ -203,14 +201,12 @@ def on_file(file): ) assert result == "ok" - params = dict(A="a", B=5) + params = {"A": "a", "B": 5} result = api._.urlencoded(parameters=params) request = httpx2_mock.get_requests()[-1] assert (v := urllib.parse.parse_qs(request.content.decode())) is not None and v["A"] == ["a"] and v["B"] == ["5"] assert result == "ok" - return - def test_paths_response_file(httpx2_mock, with_paths_parameter_format_v20): httpx2_mock.add_response(headers={"Content-Type": "application/octet-stream"}, content=b"\x00") diff --git a/tests/petstore_test.py b/tests/petstore_test.py index 39d82cea..627db699 100644 --- a/tests/petstore_test.py +++ b/tests/petstore_test.py @@ -1,3 +1,5 @@ +from typing import ClassVar + import httpx2 import pytest @@ -22,12 +24,12 @@ def session_factory(*args, **kwargs) -> httpx2.Client: class OnDocument(Document): - ApiResponse = {"description": "successful operation", "schema": {"$ref": "#/definitions/ApiResponse"}} - PetResponse = {"description": "successful operation", "schema": {"$ref": "#/definitions/Pet"}} + ApiResponse: ClassVar = {"description": "successful operation", "schema": {"$ref": "#/definitions/ApiResponse"}} + PetResponse: ClassVar = {"description": "successful operation", "schema": {"$ref": "#/definitions/Pet"}} def parsed(self, ctx): - for name, path in ctx.document["paths"].items(): - for method, action in path.items(): + for path in ctx.document["paths"].values(): + for action in path.values(): if "default" not in action["responses"]: action["responses"]["default"] = OnDocument.ApiResponse @@ -46,7 +48,7 @@ class OnMessage(Message): def parsed(self, ctx): def goodPet(i): if not isinstance(i.get("photoUrls", None), list): - i["photoUrls"] = list() + i["photoUrls"] = [] for idx, j in enumerate(i["photoUrls"]): if not isinstance(j, str): i["photoUrls"][idx] = "" @@ -55,7 +57,7 @@ def goodPet(i): i["status"] = "pending" if (c := i.get("category", None)) is None or not isinstance(c, dict): - i["category"] = dict(id=0, name="default") + i["category"] = {"id": 0, "name": "default"} if (c := i.get("name", None)) is None or not isinstance(c, str): i["name"] = "" @@ -65,21 +67,21 @@ def goodPet(i): i["id"] = 0 if False: - for t in i.get("tags", list()): + for t in i.get("tags", []): for k, v in {"name": "default", "id": 0}.items(): if k not in t: t[k] = v Pet = self.api.resolve_jr(self.api._root, None, Reference(**{"$ref": "#/definitions/Pet"})) - if ctx.operationId == "getPetById": - if Pet == ctx.expected_type: - goodPet(ctx.parsed) + if ctx.operationId == "getPetById" and Pet == ctx.expected_type: + goodPet(ctx.parsed) - if ctx.operationId in frozenset(["findPetsByStatus", "findPetsByTags"]): - if Pet == getattr(ctx.expected_type.items, "_target", None): - for i in ctx.parsed: - goodPet(i) + if ctx.operationId in frozenset(["findPetsByStatus", "findPetsByTags"]) and Pet == getattr( + ctx.expected_type.items, "_target", None + ): + for i in ctx.parsed: + goodPet(i) return ctx @@ -197,14 +199,14 @@ def test_pets(api, login): assert (isinstance(r, list) and len(r) >= 0) or isinstance(r, ApiResponse) r = api._.findPetsByTags(parameters={"tags": ["unknown"]}) - assert isinstance(r, list) or isinstance(r, ApiResponse) + assert isinstance(r, (list, ApiResponse)) # deletePet r = api._.findPetsByStatus(parameters={"status": ["available", "pending", "sold"]}) for i, pet in enumerate(r): try: api._.deletePet(parameters={"petId": pet.id}) - except Exception: + except Exception: # noqa: BLE001, S110 pass if i > 3: break @@ -234,7 +236,7 @@ def test_pets(api, login): # findPetsByStatus is patched r = api._.findPetsByStatus(parameters={"status": ["invalid"]}) - assert all([i.status == "pending" for i in r]) + assert all(i.status == "pending" for i in r) def test_store(api): diff --git a/tests/petstorev3_test.py b/tests/petstorev3_test.py index d937854b..24ad3797 100644 --- a/tests/petstorev3_test.py +++ b/tests/petstorev3_test.py @@ -1,4 +1,5 @@ import random +from typing import ClassVar import httpx2 import pytest @@ -7,7 +8,6 @@ from aiopenapi3.plugin import Document, Message from aiopenapi3.v20 import Reference - try: import httpx2_auth except ImportError: @@ -30,15 +30,18 @@ def session_factory(*args, **kwargs) -> httpx2.Client: class OnDocument(Document): - ApiResponse = { + ApiResponse: ClassVar = { "description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ApiResponse"}}}, } - PetResponse = {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Pet"}}}} + PetResponse: ClassVar = { + "description": "", + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Pet"}}}, + } def parsed(self, ctx): - for name, path in ctx.document["paths"].items(): - for method, action in path.items(): + for path in ctx.document["paths"].values(): + for action in path.values(): if "default" not in action["responses"]: action["responses"]["default"] = OnDocument.ApiResponse @@ -61,7 +64,7 @@ def received(self, ctx: "Message.Context") -> "Message.Context": if ctx.operationId == "getPetById" and ctx.status_code == "404": import json - ctx.received = json.dumps(dict(code=1, type="error", message=ctx.received.decode())).encode() + ctx.received = json.dumps({"code": 1, "type": "error", "message": ctx.received.decode()}).encode() return ctx def parsed(self, ctx): @@ -69,7 +72,7 @@ def goodPet(i): if i.get("name", None) is None: i["name"] = "default" if not isinstance(i.get("photoUrls", None), list): - i["photoUrls"] = list() + i["photoUrls"] = [] for idx, j in enumerate(i["photoUrls"]): if not isinstance(j, str): i["photoUrls"][idx] = "" @@ -78,26 +81,26 @@ def goodPet(i): i["status"] = "pending" if (c := i.get("category", None)) is None or not isinstance(c, dict): - i["category"] = dict(id=0, name="default") + i["category"] = {"id": 0, "name": "default"} if (c := i.get("tags", None)) is None or not isinstance(c, list): i["tags"] = [] else: for t in c: - if not isinstance(t, dict) or not set(t.keys()) == frozenset(["id", "name"]): + if not isinstance(t, dict) or set(t.keys()) != frozenset(["id", "name"]): i["tags"] = [] break Pet = self.api.resolve_jr(self.api._root, None, Reference(**{"$ref": "#/components/schemas/Pet"})) - if ctx.operationId == "getPetById": - if Pet == ctx.expected_type: - goodPet(ctx.parsed) + if ctx.operationId == "getPetById" and Pet == ctx.expected_type: + goodPet(ctx.parsed) - if ctx.operationId in frozenset(["findPetsByStatus", "findPetsByTags"]): - if Pet == getattr(ctx.expected_type.items, "_target", None): - for i in ctx.parsed: - goodPet(i) + if ctx.operationId in frozenset(["findPetsByStatus", "findPetsByTags"]) and Pet == getattr( + ctx.expected_type.items, "_target", None + ): + for i in ctx.parsed: + goodPet(i) return ctx @@ -221,14 +224,14 @@ def test_pets(api, login): assert (isinstance(r, list) and len(r) >= 0) or isinstance(r, ApiResponse) r = api._.findPetsByTags(parameters={"tags": ["unknown"]}) - assert isinstance(r, list) or isinstance(r, ApiResponse) + assert isinstance(r, (list, ApiResponse)) # deletePet r = api._.findPetsByStatus(parameters={"status": ["available", "pending", "sold"]}) for i, pet in enumerate(r): try: api._.deletePet(parameters={"petId": pet.id}) - except Exception: + except Exception: # noqa: BLE001, S110 pass if i > 3: break @@ -259,7 +262,7 @@ def test_pets(api, login): # findPetsByStatus is patched r = api._.findPetsByStatus(parameters={"status": ["available"]}) - assert all([i.status == "available" for i in r]) + assert all(i.status == "available" for i in r) @pytest.mark.xfail diff --git a/tests/pickle_test.py b/tests/pickle_test.py index 164ee115..deda4def 100644 --- a/tests/pickle_test.py +++ b/tests/pickle_test.py @@ -2,10 +2,9 @@ Tests parsing specs """ -from pathlib import Path -import pickle import copy - +import pickle +from pathlib import Path from aiopenapi3 import OpenAPI diff --git a/tests/plugin_test.py b/tests/plugin_test.py index df70f38c..425af487 100644 --- a/tests/plugin_test.py +++ b/tests/plugin_test.py @@ -5,7 +5,7 @@ import yarl from aiopenapi3 import FileSystemLoader, OpenAPI -from aiopenapi3.plugin import Init, Message, Document +from aiopenapi3.plugin import Document, Init, Message class OnInit(Init): @@ -71,9 +71,8 @@ def parsed(self, ctx): return ctx def unmarshalled(self, ctx): - if ctx.operationId == "listPets": - if ctx.unmarshalled[0].id == 2: - ctx.unmarshalled[0].id = 3 + if ctx.operationId == "listPets" and ctx.unmarshalled[0].id == 2: + ctx.unmarshalled[0].id = 3 return ctx diff --git a/tests/ref_test.py b/tests/ref_test.py index 0b51510f..7353b086 100644 --- a/tests/ref_test.py +++ b/tests/ref_test.py @@ -7,15 +7,13 @@ allOfs are populated as expected as well. """ -from typing import ForwardRef - import typing +from typing import ForwardRef -from aiopenapi3 import OpenAPI - +from pydantic import RootModel from pydantic._internal._model_construction import ModelMetaclass -from pydantic import RootModel +from aiopenapi3 import OpenAPI def test_ref_resolution(openapi_version, petstore_expanded): @@ -69,23 +67,23 @@ def is_nullable(x): # Optional[…] or | None return typing.get_origin(x.annotation) == typing.Union and type(None) in typing.get_args(x.annotation) - assert sorted(map(lambda x: x[0], filter(lambda y: is_nullable(y[1]), items.items()))) == sorted( - ["created", "tag"] - ), ref.model_json_schema() + assert sorted(x[0] for x in filter(lambda y: is_nullable(y[1]), items.items())) == sorted(["created", "tag"]), ( + ref.model_json_schema() + ) def is_required(x): # not assign a default '= Field(default=…)' or '= …' return x.default == pydantic_core.PydanticUndefined - assert sorted(map(lambda x: x[0], filter(lambda y: is_required(y[1]), items.items()))) == sorted(["id", "name"]), ( + assert sorted(x[0] for x in filter(lambda y: is_required(y[1]), items.items())) == sorted(["id", "name"]), ( ref.model_json_schema() ) assert items["id"].annotation is int assert items["name"].annotation is str - assert items["tag"].annotation == typing.Optional[str] + assert items["tag"].annotation == str | None - r = ref.model_validate([dict(id=1, name="dog"), dict(id=2, name="cat", tag="x")]) + r = ref.model_validate([{"id": 1, "name": "dog"}, {"id": 2, "name": "cat", "tag": "x"}]) assert len(r.root) == 2 assert r.root[1].tag == "x" diff --git a/tests/schema_test.py b/tests/schema_test.py index 4f1534d8..200970db 100644 --- a/tests/schema_test.py +++ b/tests/schema_test.py @@ -2,15 +2,13 @@ import typing import uuid from datetime import datetime - - from pathlib import Path -import yarl import httpx2 +import pydantic import pytest +import yarl from pydantic import ValidationError -import pydantic import aiopenapi3 from aiopenapi3 import OpenAPI @@ -149,7 +147,7 @@ def test_schema_type_missing(with_schema_type_missing): """ api = OpenAPI("/", with_schema_type_missing) t = api.components.schemas["Any"].get_type() - v = t.model_validate(dict(id=1)) + v = t.model_validate({"id": 1}) assert v.root.id == 1 v = t.model_validate("1") @@ -304,8 +302,7 @@ class OnDocument(Document): def parsed(self, ctx): ctx.document["openapi"] = str(openapi_version) - from aiopenapi3.loader import YAML12Loader - from aiopenapi3.loader import FileSystemLoader + from aiopenapi3.loader import FileSystemLoader, YAML12Loader OpenAPI.load_file( "/test.yaml", @@ -359,7 +356,7 @@ def test_schema_with_patternProperties(with_schema_patternProperties): O = api.components.schemas["O"].get_type() a = A.model_validate({"I_5": 100}) assert list(a.aio3_patternProperty("^I_")) == [("I_5", 100)] - sorted(typing.get_args(a.aio3_patternProperty.__annotations__["item"])) == ["^I_", "^S_"] + assert sorted(typing.get_args(a.aio3_patternProperty.__annotations__["item"])) == ["^I_", "^S_"] assert a.aio3_patternProperties == {"^S_": [], "^I_": [("I_5", 100)]} @@ -392,19 +389,19 @@ def test_schema_discriminated_union_warnings(with_schema_discriminated_union_war s = copy.deepcopy(with_schema_discriminated_union_warning) api = OpenAPI("/", s) + s = copy.deepcopy(with_schema_discriminated_union_warning) + s["components"]["schemas"]["B"]["properties"]["object_type"]["enum"] = ["f"] with pytest.warns( DiscriminatorWarning, match=r"Discriminated Union member key property enum mismatches property mapping \S+ \!= \S+", ): - s = copy.deepcopy(with_schema_discriminated_union_warning) - s["components"]["schemas"]["B"]["properties"]["object_type"]["enum"] = ["f"] api = OpenAPI("/", s) + s = copy.deepcopy(with_schema_discriminated_union_warning) + del s["components"]["schemas"]["B"]["properties"]["object_type"]["enum"] with pytest.warns( DiscriminatorWarning, match=r"Discriminated Union member \S+ without const/enum key property \S+" ): - s = copy.deepcopy(with_schema_discriminated_union_warning) - del s["components"]["schemas"]["B"]["properties"]["object_type"]["enum"] api = OpenAPI("/", s) if (openapi_version.major, openapi_version.minor, openapi_version.patch) >= (3, 1, 0): @@ -434,19 +431,18 @@ def test_schema_discriminated_union_deep(with_schema_discriminated_union_deep): pet = Pet(dog) d = Dog.model_construct() - return None def test_schema_create_update_read(with_schema_create_update_read): api = OpenAPI("/", with_schema_create_update_read) A = api.components.schemas["A"].get_type() AB = api.components.schemas["AB"].get_type() - A.model_validate(dict(a="a")) + A.model_validate({"a": "a"}) with pytest.raises(ValidationError): - AB.model_validate(dict(a="a")) + AB.model_validate({"a": "a"}) with pytest.raises(ValidationError): - AB.model_validate(dict(b="b")) - AB.model_validate(dict(b="b", a="a")) + AB.model_validate({"b": "b"}) + AB.model_validate({"b": "b", "a": "a"}) def test_schema_constraints(with_schema_constraints): @@ -461,7 +457,7 @@ def test_schema_constraints(with_schema_constraints): A("i" * i) B = (_B := api.components.schemas["B"]).get_type() - for i in range(0, 12): + for i in range(12): if _B.exclusiveMaximum > i > _B.exclusiveMinimum: B(i) else: @@ -469,7 +465,7 @@ def test_schema_constraints(with_schema_constraints): B(i) C = (_C := api.components.schemas["C"]).get_type() - for i in range(0, 12): + for i in range(12): if i % _C.multipleOf != 0: with pytest.raises(ValidationError): C(i) @@ -530,8 +526,6 @@ def test_schema_pathitems(httpx2_mock, with_schema_pathitems): r = req() r = api._.b() - return - def test_schema_baseurl_v20(with_schema_baseurl_v20): api = OpenAPI("/", with_schema_baseurl_v20, session_factory=httpx2.Client) diff --git a/tests/sequential_test.py b/tests/sequential_test.py index d396e8fb..6f7cb38f 100644 --- a/tests/sequential_test.py +++ b/tests/sequential_test.py @@ -1,17 +1,13 @@ import asyncio - from collections.abc import AsyncIterable - -from hypercorn.asyncio import serve -from hypercorn.config import Config import pydantic -from fastapi import FastAPI -from fastapi.sse import EventSourceResponse, ServerSentEvent - import pytest import pytest_asyncio - +from fastapi import FastAPI +from fastapi.sse import EventSourceResponse, ServerSentEvent +from hypercorn.asyncio import serve +from hypercorn.config import Config import aiopenapi3 diff --git a/tests/stream_test.py b/tests/stream_test.py index bc0283dd..e6ef1086 100644 --- a/tests/stream_test.py +++ b/tests/stream_test.py @@ -1,24 +1,19 @@ import asyncio import random import string - -from typing import Annotated - from pathlib import Path +from typing import Annotated -from hypercorn.asyncio import serve -from hypercorn.config import Config import pydantic -from fastapi import FastAPI, Request, Response, Query, UploadFile, Body -from fastapi.responses import PlainTextResponse - import pytest import pytest_asyncio - +from fastapi import Body, FastAPI, Query, Request, Response, UploadFile +from fastapi.responses import PlainTextResponse +from hypercorn.asyncio import serve +from hypercorn.config import Config import aiopenapi3 - app = FastAPI(version="1.0.0", title="TLS tests", servers=[{"url": "/", "description": "Default, relative server"}]) @@ -83,7 +78,7 @@ def request_streaming( async def test_stream_data(server, client): cl = client._max_response_content_length req = client.createRequest("file") - headers, schema_, session, result = await req.stream(parameters=dict(content_length=cl)) + _headers, _schema, session, result = await req.stream(parameters={"content_length": cl}) chunk = l = 0 async for i in result.aiter_bytes(): @@ -113,7 +108,7 @@ async def test_stream_array(server, client): req = client.createRequest("files") - headers, schema_, session, result = await req.stream(parameters=dict(number=10, size=512 * 1024)) + _headers, schema_, session, result = await req.stream(parameters={"number": 10, "size": 512 * 1024}) assert schema_ == req.operation.responses["200"].content["application/json"].schema_ @@ -128,7 +123,7 @@ def cb(): data = yield try: file = t.model_validate(data) - except Exception as e: + except Exception as e: # noqa: BLE001 print(e) assert len(file.name) == 16 # process received File model here @@ -153,7 +148,7 @@ async def test_sync_stream(server): cl = client._max_response_content_length req = client.createRequest("file") - headers, schema_, session, result = await asyncio.to_thread(req.stream, parameters=dict(content_length=cl)) + _headers, _schema, session, result = await asyncio.to_thread(req.stream, parameters={"content_length": cl}) def blocking_recv(rs): r = 0 diff --git a/tests/tls_test.py b/tests/tls_test.py index 29b87419..c477a09d 100644 --- a/tests/tls_test.py +++ b/tests/tls_test.py @@ -3,21 +3,18 @@ import ssl from pathlib import Path +import cryptography import httpx2 import pytest import pytest_asyncio - +import trustme +from fastapi import FastAPI, Request, Response from hypercorn.asyncio import serve from hypercorn.config import Config -import trustme -import cryptography - import aiopenapi3 from aiopenapi3.plugin import Document -from fastapi import FastAPI, Request, Response - app = FastAPI(version="1.0.0", title="TLS tests", servers=[{"url": "/", "description": "Default, relative server"}]) @@ -92,8 +89,8 @@ async def wait_for_server(server): for i in range(10): try: host, _, port = server.bind[0].rpartition(":") - r, w = await asyncio.open_connection(host=host, port=port) - except Exception as e: + _r, w = await asyncio.open_connection(host=host, port=port) + except Exception as e: # noqa: BLE001 await asyncio.sleep(0.1) else: await w.drain() @@ -246,5 +243,5 @@ async def test_certificate_invalid(client): with pytest.raises(ValueError, match=r"Invalid parameter for SecurityScheme tls mutualTLS") as e: client.authenticate(tls=(p := ("/does/not/exist", "/tmp"))) assert isinstance(e.value.__context__, FileNotFoundError) and e.value.__context__.args[0] == sorted( - map(lambda x: Path(x), p) + Path(x) for x in p ) diff --git a/tests/v32_test.py b/tests/v32_test.py index ac233e79..b53ce198 100644 --- a/tests/v32_test.py +++ b/tests/v32_test.py @@ -1,10 +1,8 @@ import httpx2 - import pytest from pytest_httpx2 import IteratorStream -from aiopenapi3 import OpenAPI -from aiopenapi3 import v32 +from aiopenapi3 import OpenAPI, v32 def test_Components(): @@ -83,7 +81,6 @@ async def test_MediaType(httpx2_mock, with_schema_itemSchema): # prefixEncoding # itemEncoding - pass @pytest.mark.httpx2_mock(can_send_already_matched_responses=True)