diff --git a/README.md b/README.md index e8f5e618..6e60f064 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,12 @@ This project is a fork of [Dorthu/openapi3](https://github.com/Dorthu/openapi3/) * pydantic compatible "format"-type coercion (e.g. datetime.interval) * additionalProperties (string-to-any dictionaries, including inline schemas with `allOf`/`nullable`) * response body & header parsing via pydantic - * blocking and nonblocking (asyncio) interface via [httpx](https://www.python-httpx.org/) + * blocking and nonblocking (asyncio) interface via [httpx2](https://httpx2.pydantic.dev/) * SOCKS5 via socksio * tests with pytest & [fastapi](https://fastapi.tiangolo.com/) * providing access to methods and arguments via the sad smiley ._. interface - * Plugin Interface/api to modify description documents/requests/responses to adapt to non compliant services - * YAML type coercion hints for not well formatted description documents + * Plugin Interface/api to modify description documents/requests/responses to adapt to non-compliant services + * YAML type coercion hints for not well-formatted description documents * Description Document dependency downloads (using the WebLoader) * logging * `export AIOPENAPI3_LOGGING_HANDLERS=debug` to get /tmp/aiopenapi3-debug.log diff --git a/docs/source/advanced.rst b/docs/source/advanced.rst index 9b6497ca..dc301bce 100644 --- a/docs/source/advanced.rst +++ b/docs/source/advanced.rst @@ -116,14 +116,14 @@ MutualTLS authentication requires * key file * (optional) password to keyfile -to authenticate to the remote server, c.f. :ref:`httpx.Client.cert `_. +to authenticate to the remote server, c.f. :ref:`httpx2.Client.cert `_. .. code:: python api.authenticate(tls=("cert.pem","key.pem")) -when using mutualTLS with self-signed certificates, it is required to add the self-signed CA to the SSLContext of the httpx session by providing a :ref:`Session Factory `. +when using mutualTLS with self-signed certificates, it is required to add the self-signed CA to the SSLContext of the httpx2 session by providing a :ref:`Session Factory `. Forms @@ -187,7 +187,7 @@ Currently there is not public API except accessing OpenAPi._server_variables dir Manual Requests =============== -Creating a request manually allows accessing the httpx.Response as part of the :meth:`aiopenapi3.request.RequestBase.request` return value. +Creating a request manually allows accessing the httpx2.Response as part of the :meth:`aiopenapi3.request.RequestBase.request` return value. .. code:: python @@ -209,12 +209,12 @@ This can be used to provide certain header values (ETag), which are not paramete Request Streaming ----------------- -File uploads via "multipart/form-data" as mentioned in the httpx documentation -(Multipart file `uploads `_ & -`encoding `_) +File uploads via "multipart/form-data" as mentioned in the httpx2 documentation +(Multipart file `uploads `_ & +`encoding `_) do not require the content of the request to be in memory but work with file-like-objects instead. -httpx request streaming using file-like objects is limited to "multipart/form-data" and "application/octet-stream". +httpx2 request streaming using file-like objects is limited to "multipart/form-data" and "application/octet-stream". Additionally it does not support choice of encoding (such as base16, base64url or quoted-printable) as possible with OpenAPI v3.1 contentEncoding, which should not be a limitation. It can not be used with "application/json". @@ -382,9 +382,9 @@ See :aioai3:ref:`tests.stream_test.test_stream_array`. Session Factory =============== -The session_factory argument of the |aiopenapi3| initializers allow setting httpx_ options to the transport. +The session_factory argument of the |aiopenapi3| initializers allow setting httpx2_ options to the transport. -E.g. setting `httpx Event Hooks `_: +E.g. setting `httpx2 Event Hooks `_: .. code:: python @@ -395,32 +395,32 @@ E.g. setting `httpx Event Hooks httpx.AsyncClient: + def session_factory(*args, **kwargs) -> httpx2.AsyncClient: kwargs["event_hooks"] = {"request": [log_request], "response": [log_response]} - return httpx.AsyncClient(*args, verify=False, timeout=60.0, **kwargs) + return httpx2.AsyncClient(*args, verify=False, timeout=60.0, **kwargs) -Or adding a SOCKS5 proxy via httpx_socks and a custom timeout value: +Or adding a SOCKS5 proxy via httpx2_socks and a custom timeout value: .. code:: python - import httpx - import httpx_socks + import httpx2 + import httpx2_socks - def session_factory(*args, **kwargs) -> httpx.AsyncClient: - kwargs["transport"] = httpx_socks.AsyncProxyTransport.from_url("socks5://127.0.0.1:8080", verify=False) - return httpx.AsyncClient(*args, verify=False, timeout=60.0, **kwargs) + def session_factory(*args, **kwargs) -> httpx2.AsyncClient: + kwargs["transport"] = httpx2_socks.AsyncProxyTransport.from_url("socks5://127.0.0.1:8080", verify=False) + return httpx2.AsyncClient(*args, verify=False, timeout=60.0, **kwargs) Or using a self-signed CA with certificate validation and possibly mutualTLS authentication: .. code:: python - def self_signed(*args, **kwargs) -> httpx.AsyncClient: + def self_signed(*args, **kwargs) -> httpx2.AsyncClient: ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile="/etc/ssl/my-ca.pem") if (cert:=kwargs.get("cert", None)) is not None: """required for mutualTLS / client certificate authentication""" ctx.load_cert_chain(certfile=cert[0], keyfile=cert[1]) - return httpx.AsyncClient(*args, verify=ctx, **kwargs) + return httpx2.AsyncClient(*args, verify=ctx, **kwargs) Logging @@ -437,21 +437,21 @@ It can be used to inspect Description Document downloads … .. code:: aiopenapi3.OpenAPI DEBUG Downloading Description Document TS29122_CommonData.yaml using WebLoader(baseurl=https://raw.githubusercontent.com/jdegre/5GC_APIs/master/TS24558_Eecs_ServiceProvisioning.yaml) … - httpx._client DEBUG HTTP Request: GET https://raw.githubusercontent.com/jdegre/5GC_APIs/master/TS29122_CommonData.yaml "HTTP/1.1 200 OK" + httpx2._client DEBUG HTTP Request: GET https://raw.githubusercontent.com/jdegre/5GC_APIs/master/TS29122_CommonData.yaml "HTTP/1.1 200 OK" aiopenapi3.OpenAPI DEBUG Resolving TS29571_CommonData.yaml#/components/schemas/Gpsi - Description Document TS29571_CommonData.yaml unknown … aiopenapi3.OpenAPI DEBUG Downloading Description Document TS29571_CommonData.yaml using WebLoader(baseurl=https://raw.githubusercontent.com/jdegre/5GC_APIs/master/TS24558_Eecs_ServiceProvisioning.yaml) … - httpx._client DEBUG HTTP Request: GET https://raw.githubusercontent.com/jdegre/5GC_APIs/master/TS29571_CommonData.yaml "HTTP/1.1 200 OK" + httpx2._client DEBUG HTTP Request: GET https://raw.githubusercontent.com/jdegre/5GC_APIs/master/TS29571_CommonData.yaml "HTTP/1.1 200 OK" aiopenapi3.OpenAPI DEBUG Resolving TS29122_MonitoringEvent.yaml#/components/schemas/LocationInfo - Description Document TS29122_MonitoringEvent.yaml unknown … aiopenapi3.OpenAPI DEBUG Downloading Description Document TS29122_MonitoringEvent.yaml using WebLoader(baseurl=https://raw.githubusercontent.com/jdegre/5GC_APIs/master/TS24558_Eecs_ServiceProvisioning.yaml) … -and general httpx requests +and general httpx2 requests .. code:: - httpx._client DEBUG HTTP Request: DELETE http://localhost:51965/v2/pets/e7e979fb-bf53-4a89-9475-da9369cb4dbc "HTTP/1.1 422 " - httpx._client DEBUG HTTP Request: GET http://localhost:54045/v2/openapi.json "HTTP/1.1 200 " - httpx._client DEBUG HTTP Request: POST http://localhost:54045/v2/pet "HTTP/1.1 201 " + httpx2._client DEBUG HTTP Request: DELETE http://localhost:51965/v2/pets/e7e979fb-bf53-4a89-9475-da9369cb4dbc "HTTP/1.1 422 " + httpx2._client DEBUG HTTP Request: GET http://localhost:54045/v2/openapi.json "HTTP/1.1 200 " + httpx2._client DEBUG HTTP Request: POST http://localhost:54045/v2/pet "HTTP/1.1 201 " Loader diff --git a/docs/source/api.rst b/docs/source/api.rst index bb0c42c3..14b99794 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -313,7 +313,7 @@ HTTPError is the base class for all request/response related errors. :members: :undoc-members: -A RequestError typically wraps an `error `_ of the underlying httpx_ library. +A RequestError typically wraps an `error `_ of the underlying httpx2_ library. .. autoexception:: ResponseError :members: diff --git a/docs/source/index.rst b/docs/source/index.rst index 07fadc9a..79f804e0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,7 +12,7 @@ *If you can't make it perfect, make it adjustable.* |aiopenapi3| is a client library to interface RESTful services using OpenAPI_/Swagger description documents, -built upon pydantic_ for data validation/coercion and httpx_ for transport. +built upon pydantic_ for data validation/coercion and httpx2_ for transport. Located on `github `_. diff --git a/docs/source/install.rst b/docs/source/install.rst index 36a8b71f..c3f0ea55 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -9,4 +9,4 @@ Installation $ pip install aiopenapi3 -* aiopenapi3[auth] will install httpx-auth_ which is required to authenticate using oauth2/azuread/. Currently httpx-auth is `limited to Sync `_ operations. +* aiopenapi3[auth] will install httpx2-auth_ which is required to authenticate using oauth2/azuread/. Currently httpx2-auth is `limited to Sync `_ operations. diff --git a/docs/source/links.rst b/docs/source/links.rst index c303d460..5aeec481 100644 --- a/docs/source/links.rst +++ b/docs/source/links.rst @@ -1,5 +1,5 @@ .. |aiopenapi3| replace:: **aiopenapi3** .. _OpenAPI: https://github.com/OAI/OpenAPI-Specification/ .. _pydantic: https://github.com/pydantic/pydantic -.. _httpx: https://github.com/encode/httpx -.. _httpx-auth: https://github.com/Colin-b/httpx_auth +.. _httpx2: https://github.com/encode/httpx2 +.. _httpx2-auth: https://github.com/Colin-b/httpx2_auth diff --git a/docs/source/use.rst b/docs/source/use.rst index 47d30ce8..1ff209bf 100644 --- a/docs/source/use.rst +++ b/docs/source/use.rst @@ -45,8 +45,8 @@ For :meth:`aiopenapi3.OpenAPI.load_file` the url parameter does not specify the url which can be used to construct the proper operations path is required nevertheless. |aiopenapi3| can interface services in synchronous as well as asynchronous. -To create a traditional/blocking api client, provide a `session_factory` which return value annotation matches httpx_.Client, -httpx.AsyncClient for asynchronous clients. +To create a traditional/blocking api client, provide a `session_factory` which return value annotation matches httpx2_.Client, +httpx2.AsyncClient for asynchronous clients. After ingesting the description document, the api client object returned can be used to interface the service. diff --git a/pyproject.toml b/pyproject.toml index 0df33d2b..8e196da5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "pydantic >= 2.13.0b2", "email-validator", "yarl", - "httpx", + "httpx2", "more-itertools", 'typing_extensions; python_version<"3.12"', "jmespath", @@ -125,7 +125,7 @@ addopts = "--ignore-glob 'tests/my_*.py'" dev = [ "pytest", "pytest-asyncio>=0.24.0", - "pytest-httpx", + "httpx2-pytest", "pytest-cov", "pytest-mock", "fastapi", diff --git a/requirements.txt b/requirements.txt index 67332a4c..bffa8f93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,11 +4,11 @@ annotated-types==0.7.0 # via pydantic anyio==4.13.0 - # via httpx + # via httpx2 certifi==2026.4.22 # via - # httpcore - # httpx + # httpcore2 + # httpx2 dnspython==2.8.0 # via email-validator email-validator==2.3.0 @@ -16,16 +16,16 @@ email-validator==2.3.0 exceptiongroup==1.3.1 ; python_full_version < '3.11' # via anyio h11==0.16.0 - # via httpcore -httpcore==1.0.9 - # via httpx -httpx==0.28.1 + # via httpcore2 +httpcore2==2.2.0 + # via httpx2 +httpx2==2.2.0 # via aiopenapi3 idna==3.15 # via # anyio # email-validator - # httpx + # httpx2 # yarl ijson==3.5.0 # via aiopenapi3 diff --git a/src/aiopenapi3/cli.py b/src/aiopenapi3/cli.py index 0c15ffee..3a01e8e6 100644 --- a/src/aiopenapi3/cli.py +++ b/src/aiopenapi3/cli.py @@ -16,7 +16,7 @@ import jmespath import yaml import yarl -import httpx +import httpx2 import aiopenapi3.plugin @@ -311,9 +311,9 @@ def log_(s): if args.tracemalloc: tracemalloc.start() - def session_factory(*args_, **kwargs) -> httpx.Client: - return httpx.Client( - *args_, verify=args.disable_ssl_validation is False, timeout=httpx.Timeout(args.timeout), **kwargs + def session_factory(*args_, **kwargs) -> httpx2.Client: + return httpx2.Client( + *args_, verify=args.disable_ssl_validation is False, timeout=httpx2.Timeout(args.timeout), **kwargs ) if args.func: diff --git a/src/aiopenapi3/errors.py b/src/aiopenapi3/errors.py index f367a0bb..bd42ae52 100644 --- a/src/aiopenapi3/errors.py +++ b/src/aiopenapi3/errors.py @@ -2,7 +2,7 @@ from typing import Optional import dataclasses -import httpx +import httpx2 import pydantic if typing.TYPE_CHECKING: @@ -120,7 +120,7 @@ class ContentLengthExceededError(ResponseError): operation: "OperationType" content_length: int message: str - response: httpx.Response + response: httpx2.Response @dataclasses.dataclass(repr=False) @@ -130,7 +130,7 @@ class ContentTypeError(ResponseError): operation: "OperationType" content_type: str | None message: str - response: httpx.Response + response: httpx2.Response def __str__(self): return f"""<{self.__class__.__name__} {self.response.request.method} '{self.response.request.url.path}' ({self.operation.operationId})> @@ -144,7 +144,7 @@ class HTTPStatusError(ResponseError): operation: "OperationType" http_status: int message: str - response: httpx.Response + response: httpx2.Response def __str__(self): return f"""<{self.__class__.__name__} {self.response.request.method} '{self.response.request.url.path}' ({self.operation.operationId})> @@ -157,7 +157,7 @@ class ResponseDecodingError(ResponseError): operation: "OperationType" data: str - response: httpx.Response + response: httpx2.Response @dataclasses.dataclass(repr=False) @@ -167,7 +167,7 @@ class ResponseSchemaError(ResponseError): operation: "OperationType" expectation: "ExpectedType" schema: Optional["SchemaType"] - response: httpx.Response + response: httpx2.Response exception: Exception | None def __str__(self): @@ -181,7 +181,7 @@ class HeadersMissingError(ResponseError): operation: "OperationType" missing: dict[str, "HeaderType"] - response: httpx.Response + response: httpx2.Response def __str__(self): return f"""<{self.__class__.__name__} {self.response.request.method} '{self.response.request.url.path}' ({self.operation.operationId}) diff --git a/src/aiopenapi3/loader.py b/src/aiopenapi3/loader.py index 8f0c79c1..1fe8ed7f 100644 --- a/src/aiopenapi3/loader.py +++ b/src/aiopenapi3/loader.py @@ -2,7 +2,7 @@ import logging import typing import yaml -import httpx +import httpx2 import yarl import re @@ -211,7 +211,7 @@ class WebLoader(Loader): Loader downloads data via http/s using the supplied session_factory """ - def __init__(self, baseurl: yarl.URL, session_factory=httpx.Client, yload: "YAMLLoaderType" = YAML12Loader): + def __init__(self, baseurl: yarl.URL, session_factory=httpx2.Client, yload: "YAMLLoaderType" = YAML12Loader): super().__init__(yload) assert isinstance(baseurl, yarl.URL) self.baseurl: yarl.URL = baseurl diff --git a/src/aiopenapi3/openapi.py b/src/aiopenapi3/openapi.py index 00156fb0..e43e67f5 100644 --- a/src/aiopenapi3/openapi.py +++ b/src/aiopenapi3/openapi.py @@ -13,7 +13,7 @@ from typing import TypeGuard -import httpx +import httpx2 import yarl from pydantic import BaseModel @@ -92,7 +92,7 @@ def servers(self): def load_sync( cls, url, - session_factory: Callable[..., httpx.Client] = httpx.Client, + session_factory: Callable[..., httpx2.Client] = httpx2.Client, loader: Loader | None = None, plugins: list[Plugin] | None = None, use_operation_tags: bool = False, @@ -115,7 +115,7 @@ def load_sync( async def load_async( cls, url: str, - session_factory: Callable[..., httpx.AsyncClient] = httpx.AsyncClient, + session_factory: Callable[..., httpx2.AsyncClient] = httpx2.AsyncClient, loader: Loader | None = None, plugins: list[Plugin] | None = None, use_operation_tags: bool = False, @@ -144,7 +144,7 @@ def load_file( cls, url: str, path: str | pathlib.Path | yarl.URL, - session_factory: Callable[..., httpx.AsyncClient | httpx.Client] = httpx.AsyncClient, + session_factory: Callable[..., httpx2.AsyncClient | httpx2.Client] = httpx2.AsyncClient, loader: Loader | None = None, plugins: list[Plugin] | None = None, use_operation_tags: bool = False, @@ -163,7 +163,7 @@ def load_file( url="", path=pathlib.Path(""), loader=loader, - session_factory=httpx.Client + session_factory=httpx2.Client ) @@ -187,7 +187,7 @@ def loads( cls, url: str, data: str, - session_factory: Callable[..., httpx.AsyncClient | httpx.Client] = httpx.AsyncClient, + session_factory: Callable[..., httpx2.AsyncClient | httpx2.Client] = httpx2.AsyncClient, loader: Loader | None = None, plugins: list[Plugin] | None = None, use_operation_tags: bool = False, @@ -236,7 +236,7 @@ def __init__( self, url: str, document: "JSON", - session_factory: Callable[..., httpx.Client | httpx.AsyncClient] = httpx.AsyncClient, + session_factory: Callable[..., httpx2.Client | httpx2.AsyncClient] = httpx2.AsyncClient, loader: Loader | None = None, plugins: list[Plugin] | None = None, use_operation_tags: bool = True, @@ -255,7 +255,7 @@ def __init__( """ self._base_url: yarl.URL = yarl.URL(url) - self._session_factory: Callable[..., httpx.Client | httpx.AsyncClient] = session_factory + self._session_factory: Callable[..., httpx2.Client | httpx2.AsyncClient] = session_factory self.loader: Loader | None = loader """ @@ -330,8 +330,8 @@ def _init_plugins(self, plugins): self.plugins = Plugins(plugins or []) def _init_session_factory(self, session_factory): - if issubclass(getattr(session_factory, "__annotations__", {}).get("return", None.__class__), httpx.Client) or ( - type(session_factory) is type and issubclass(session_factory, httpx.Client) + if issubclass(getattr(session_factory, "__annotations__", {}).get("return", None.__class__), httpx2.Client) or ( + type(session_factory) is type and issubclass(session_factory, httpx2.Client) ): if isinstance(self._root, v20.Root): self._createRequest = v20.Request @@ -340,8 +340,8 @@ def _init_session_factory(self, session_factory): else: raise ValueError(self._root) elif issubclass( - getattr(session_factory, "__annotations__", {}).get("return", None.__class__), httpx.AsyncClient - ) or (type(session_factory) is type and issubclass(session_factory, httpx.AsyncClient)): + getattr(session_factory, "__annotations__", {}).get("return", None.__class__), httpx2.AsyncClient + ) or (type(session_factory) is type and issubclass(session_factory, httpx2.AsyncClient)): if isinstance(self._root, v20.Root): self._createRequest = v20.AsyncRequest elif isinstance(self._root, (v30.Root, v31.Root, v32.Root)): @@ -753,7 +753,7 @@ def createRequest(self, operationId: str | tuple[str, "HTTPMethodType"]) -> "Req :param operationId: the operationId or tuple(path,method) :return: the returned Request is either :class:`aiopenapi3.request.RequestBase` or - - in case of a httpx.AsyncClient session_factory - :class:`aiopenapi3.request.AsyncRequestBase` + in case of a httpx2.AsyncClient session_factory - :class:`aiopenapi3.request.AsyncRequestBase` """ operation: Optional["OperationType"] = None request: Optional["RequestType"] = None diff --git a/src/aiopenapi3/plugin.py b/src/aiopenapi3/plugin.py index 51239521..b5f2acee 100644 --- a/src/aiopenapi3/plugin.py +++ b/src/aiopenapi3/plugin.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from aiopenapi3 import OpenAPI - import httpx + import httpx2 from .base import PathItemBase, SchemaBase from .request import RequestBase @@ -112,7 +112,7 @@ class Context: """available :func:`~aiopenapi3.plugin.Message.sending` """ received: bytes | None = None """available :func:`~aiopenapi3.plugin.Message.received` """ - headers: "httpx.Headers" = None + headers: "httpx2.Headers" = None """available :func:`~aiopenapi3.plugin.Message.sending` :func:`~aiopenapi3.plugin.Message.received` """ cookies: dict[str, str] = None """available :func:`~aiopenapi3.plugin.Message.sending` """ diff --git a/src/aiopenapi3/request.py b/src/aiopenapi3/request.py index de1633f5..7c2ee7dd 100644 --- a/src/aiopenapi3/request.py +++ b/src/aiopenapi3/request.py @@ -10,7 +10,7 @@ from collections.abc import Iterator from contextlib import aclosing -import httpx +import httpx2 import pydantic import yarl @@ -64,8 +64,8 @@ class RequestBase: class StreamResponse(NamedTuple): headers: "ResponseHeadersType" schema: Optional["SchemaType"] - session: httpx.Client - result: httpx.Response + session: httpx2.Client + result: httpx2.Response class Sequencer: def __init__(self, headers: "ResponseHeadersType", stream: Iterator["JSON"], model: pydantic.BaseModel) -> None: @@ -86,7 +86,7 @@ def __next__(self) -> pydantic.BaseModel: class Response(NamedTuple): headers: "ResponseHeadersType" data: Any - result: httpx.Response + result: httpx2.Response class Vars(NamedTuple): parameters: dict[str, str] | None @@ -173,16 +173,16 @@ def __call__( def _session_factory_default_args(self) -> dict[str, Any]: """ this is the session factory default arguments, - the arguments passed to httpx.Async/Client() + the arguments passed to httpx2.Async/Client() - if you need to pass your own parameters to httpx.Async/Client use a session factory + if you need to pass your own parameters to httpx2.Async/Client use a session factory and pass your pararmters to the constructor in addition to these default arguments """ return {"cert": self.req.cert, "auth": self.req.auth, "headers": {"user-agent": f"aiopenapi3/{__version__}"}} def _send( - self, session: httpx.Client, data: Optional["RequestData"], parameters: Optional["RequestParameters"] - ) -> httpx.Response: + self, session: httpx2.Client, data: Optional["RequestData"], parameters: Optional["RequestParameters"] + ) -> httpx2.Response: req = self._build_req(session) try: result = session.send(req, stream=True) @@ -191,7 +191,7 @@ def _send( return result @abc.abstractmethod - def _process_stream(self, result: httpx.Response) -> tuple["ResponseHeadersType", Optional["SchemaType"]]: + def _process_stream(self, result: httpx2.Response) -> tuple["ResponseHeadersType", Optional["SchemaType"]]: """ process response headers lookup the schema for the stream @@ -199,7 +199,7 @@ def _process_stream(self, result: httpx.Response) -> tuple["ResponseHeadersType" ... @abc.abstractmethod - def _process_request(self, result: httpx.Response) -> tuple["ResponseHeadersType", "ResponseDataType"]: + def _process_request(self, result: httpx2.Response) -> tuple["ResponseHeadersType", "ResponseDataType"]: """ process response headers lookup Model @@ -207,7 +207,7 @@ def _process_request(self, result: httpx.Response) -> tuple["ResponseHeadersType ... @abc.abstractmethod - def _process_sequence(self, result: httpx.Response) -> tuple["ResponseHeadersType", "ResponseDataType", Any]: + def _process_sequence(self, result: httpx2.Response) -> tuple["ResponseHeadersType", "ResponseDataType", Any]: """ process response headers lookup Model @@ -217,7 +217,7 @@ def _process_sequence(self, result: httpx.Response) -> tuple["ResponseHeadersTyp @abc.abstractmethod def _prepare(self, data: Optional["RequestData"], parameters: Optional["RequestParameters"]) -> None: ... - def _build_req(self, session: httpx.Client | httpx.AsyncClient) -> httpx.Request: + def _build_req(self, session: httpx2.Client | httpx2.AsyncClient) -> httpx2.Request: url: yarl.URL = self.api.url if self.servers: @@ -281,7 +281,7 @@ def stream( ) -> "RequestBase.StreamResponse": """ Sends an HTTP request as described by this Path - but do not process the result - * returns a tuple of Schema, httpx.Client, httpx.Response + * returns a tuple of Schema, httpx2.Client, httpx2.Response * requires closing the Client when done processing the response * requires manual processing of the data * intended for use with of large results @@ -311,7 +311,7 @@ def sequence( # type: ignore[override] ) -> Generator["RequestBase.Sequencer", None, None]: self.vars = RequestBase.Vars(parameters, data, context) self._prepare(data, parameters) - session: httpx.Client = self.api._session_factory(**self._session_factory_default_args) + session: httpx2.Client = self.api._session_factory(**self._session_factory_default_args) result = self._send(session, data, parameters) headers, schema_, content_type = self._process_sequence(result) @@ -321,7 +321,7 @@ def sequence( # type: ignore[override] https://github.com/ndjson/ndjson-spec """ - def iter_json(response: httpx.Response) -> Iterator["JSON"]: + def iter_json(response: httpx2.Response) -> Iterator["JSON"]: for i in response.iter_lines(): yield json.loads(i) @@ -333,7 +333,7 @@ def iter_json(response: httpx.Response) -> Iterator["JSON"]: import jsonseq.decode - def iter_json(response: httpx.Response) -> Iterator["JSON"]: + def iter_json(response: httpx2.Response) -> Iterator["JSON"]: decoder = jsonseq.decode.JSONSeqDecoder() for text in response.iter_text(): yield from decoder.decode(text) @@ -344,7 +344,7 @@ def iter_json(response: httpx.Response) -> Iterator["JSON"]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events """ - def iter_json(response: httpx.Response) -> Iterator["JSON"]: + def iter_json(response: httpx2.Response) -> Iterator["JSON"]: for chunk in response.iter_text(): data_ = "" for line in chunk.splitlines(keepends=True): @@ -371,7 +371,7 @@ class ReadEventStream: Using a AsyncIterator input to feed a coroutine """ - def __init__(self, response: httpx.Response) -> None: + def __init__(self, response: httpx2.Response) -> None: self._iter_bytes = response.iter_bytes() def read(self, num_bytes: int) -> bytes: @@ -380,7 +380,7 @@ def read(self, num_bytes: int) -> bytes: return next(self._iter_bytes) - def iter_json(response: httpx.Response) -> Iterator["JSON"]: + def iter_json(response: httpx2.Response) -> Iterator["JSON"]: reader = ReadEventStream(response) yield from ijson.items(reader, "item") else: @@ -416,8 +416,8 @@ class AsyncRequestBase(RequestBase): class StreamResponse(NamedTuple): headers: "ResponseHeadersType" schema: Optional["SchemaType"] - session: httpx.AsyncClient - result: httpx.Response + session: httpx2.AsyncClient + result: httpx2.Response class Sequencer: def __init__( @@ -446,8 +446,8 @@ async def __call__( # type: ignore[override] return data async def _send( - self, session: httpx.AsyncClient, data: Optional["RequestData"], parameters: Optional["RequestParameters"] - ) -> httpx.Response: # type: ignore[override] + self, session: httpx2.AsyncClient, data: Optional["RequestData"], parameters: Optional["RequestParameters"] + ) -> httpx2.Response: # type: ignore[override] req = self._build_req(session) try: result = await session.send(req, stream=True) @@ -508,7 +508,7 @@ async def sequence( # type: ignore[override] https://github.com/ndjson/ndjson-spec """ - async def aiter_json(response: httpx.Response) -> AsyncIterator["JSON"]: + async def aiter_json(response: httpx2.Response) -> AsyncIterator["JSON"]: async for i in response.aiter_lines(): yield json.loads(i) @@ -520,7 +520,7 @@ async def aiter_json(response: httpx.Response) -> AsyncIterator["JSON"]: import jsonseq.decode - async def aiter_json(response: httpx.Response) -> AsyncIterator["JSON"]: + async def aiter_json(response: httpx2.Response) -> AsyncIterator["JSON"]: decoder = jsonseq.decode.JSONSeqDecoder() async for text in response.aiter_text(): for obj in decoder.decode(text): @@ -534,7 +534,7 @@ async def aiter_json(response: httpx.Response) -> AsyncIterator["JSON"]: https://github.com/mpetazzoni/sseclient/blob/main/sseclient/__init__.py#L36 """ - async def aiter_json(response: httpx.Response) -> AsyncIterator["JSON"]: + async def aiter_json(response: httpx2.Response) -> AsyncIterator["JSON"]: async for chunk in response.aiter_text(): data_ = "" @@ -562,7 +562,7 @@ class ReadEventStream: Using a AsyncIterator input to feed a coroutine """ - def __init__(self, response: httpx.Response) -> None: + def __init__(self, response: httpx2.Response) -> None: self._aiter_bytes = response.aiter_bytes() async def read(self, num_bytes: int) -> bytes: @@ -571,7 +571,7 @@ async def read(self, num_bytes: int) -> bytes: return await anext(self._aiter_bytes) - async def aiter_json(response: httpx.Response) -> AsyncIterator["JSON"]: + async def aiter_json(response: httpx2.Response) -> AsyncIterator["JSON"]: reader = ReadEventStream(response) async for item in ijson.items(reader, "item"): yield item diff --git a/src/aiopenapi3/v20/glue.py b/src/aiopenapi3/v20/glue.py index 5571842b..217872d6 100644 --- a/src/aiopenapi3/v20/glue.py +++ b/src/aiopenapi3/v20/glue.py @@ -6,7 +6,7 @@ from typing import TypeGuard -import httpx +import httpx2 import pydantic from ..request import RequestBase, AsyncRequestBase @@ -17,9 +17,9 @@ from .root import Root try: - import httpx_auth + import httpx2_auth except ImportError: - httpx_auth = None + httpx2_auth = None if typing.TYPE_CHECKING: from .._types import ( @@ -116,7 +116,7 @@ def _prepare_security(self): ) def _prepare_secschemes(self, scheme: str, value: str | Sequence[str]) -> None: - if httpx_auth is not None: + if httpx2_auth is not None: self._prepare_secschemes_extra(scheme, value) else: self._prepare_secschemes_default(scheme, value) @@ -127,7 +127,7 @@ def _prepare_secschemes_default(self, scheme: str, value: str | Sequence[str]) - if ss.type == "basic": value = cast(list[str], value) - self.req.auth = httpx.BasicAuth(*value) + self.req.auth = httpx2.BasicAuth(*value) value = cast(str, value) if ss.type == "apiKey": @@ -145,17 +145,17 @@ def _prepare_secschemes_extra(self, scheme: str, value: str | Sequence[str]) -> if ss.type == "basic": value = cast(list[str], value) - self.req.auth = httpx_auth.Basic(*value) + self.req.auth = httpx2_auth.Basic(*value) value = cast(str, value) if ss.type == "apiKey": if ss.in_ == "query": # apiKey in query parameter - self.req.auth = httpx_auth.QueryApiKey(value, ss.name) + self.req.auth = httpx2_auth.QueryApiKey(value, ss.name) if ss.in_ == "header": # apiKey in query header data - self.req.auth = httpx_auth.HeaderApiKey(value, ss.name) + self.req.auth = httpx2_auth.HeaderApiKey(value, ss.name) def _prepare_parameters(self, provided: Optional["RequestParameters"]): provided = provided or dict() @@ -244,7 +244,7 @@ def _prepare(self, data: Optional["RequestData"], parameters: Optional["RequestP self._prepare_parameters(parameters) self._prepare_body(data) - def _process__status_code(self, result: httpx.Response, status_code: str) -> "v20ResponseType": + def _process__status_code(self, result: httpx2.Response, status_code: str) -> "v20ResponseType": # find the response model in spec we received expected_response = None if status_code in self.operation.responses: @@ -263,7 +263,7 @@ def _process__status_code(self, result: httpx.Response, status_code: str) -> "v2 return expected_response def _process__headers( - self, result: httpx.Response, headers: dict[str, str], expected_response: "v20ResponseType" + self, result: httpx2.Response, headers: dict[str, str], expected_response: "v20ResponseType" ) -> "ResponseHeadersType": rheaders = dict() if expected_response.headers: @@ -282,13 +282,13 @@ def _process__headers( rheaders[name] = header._schema.model(header._decode(data)) return rheaders - def _process_stream(self, result: httpx.Response) -> tuple["ResponseHeadersType", Optional["Schema"]]: + def _process_stream(self, result: httpx2.Response) -> tuple["ResponseHeadersType", Optional["Schema"]]: status_code = str(result.status_code) expected_response = self._process__status_code(result, status_code) headers = self._process__headers(result, result.headers, expected_response) return headers, expected_response.schema_ - def _process_request(self, result: httpx.Response) -> tuple["ResponseHeadersType", Optional["ResponseDataType"]]: + def _process_request(self, result: httpx2.Response) -> tuple["ResponseHeadersType", Optional["ResponseDataType"]]: rheaders: "ResponseHeadersType" # spec enforces these are strings status_code = str(result.status_code) diff --git a/src/aiopenapi3/v30/glue.py b/src/aiopenapi3/v30/glue.py index 776532ae..461c3d49 100644 --- a/src/aiopenapi3/v30/glue.py +++ b/src/aiopenapi3/v30/glue.py @@ -4,20 +4,20 @@ import json import urllib.parse -import httpx +import httpx2 try: - import httpx_auth - from httpx_auth import SupportMultiAuth + import httpx2_auth + from httpx2_auth import SupportMultiAuth import inspect except ImportError: - httpx_auth = None + httpx2_auth = None else: HTTPX_AUTH_METHODS = { - name.lower(): getattr(httpx_auth, name) - for name in httpx_auth.__all__ - if inspect.isclass(class_ := getattr(httpx_auth, name)) - if issubclass(class_, httpx.Auth) + name.lower(): getattr(httpx2_auth, name) + for name in httpx2_auth.__all__ + if inspect.isclass(class_ := getattr(httpx2_auth, name)) + if issubclass(class_, httpx2.Auth) } import pydantic @@ -133,7 +133,7 @@ def _prepare_secschemes(self, scheme: str, value: str | Sequence[str]) -> None: and scheme in self.root.components.securitySchemes and self.root.components.securitySchemes[scheme].root ) - if httpx_auth is not None: + if httpx2_auth is not None: self._prepare_secschemes_extra(scheme, value) else: self._prepare_secschemes_default(scheme, value) @@ -151,9 +151,9 @@ def _prepare_secschemes_default(self, scheme: str, value: str | Sequence[str]) - if ss.type == "http": assert isinstance(ss, (v30.security._SecuritySchemes.http, v31.security._SecuritySchemes.http)) if ss.scheme_ == "basic": - self.req.auth = httpx.BasicAuth(*value) + self.req.auth = httpx2.BasicAuth(*value) elif ss.scheme_ == "digest": - self.req.auth = httpx.DigestAuth(*value) + self.req.auth = httpx2.DigestAuth(*value) elif ss.scheme_ == "bearer": self.req.headers["Authorization"] = f"Bearer {value:s}" else: @@ -195,7 +195,7 @@ def _prepare_secschemes_extra(self, scheme: str, value: str | Sequence[str]) -> # REF: https://github.com/Colin-b/httpx_auth/issues/17 if flow := ss.flows.implicit: auths.append( - httpx_auth.OAuth2Implicit( + httpx2_auth.OAuth2Implicit( **value, authorization_url=flow.authorizationUrl, scopes=flow.scopes, @@ -204,7 +204,7 @@ def _prepare_secschemes_extra(self, scheme: str, value: str | Sequence[str]) -> ) if flow := ss.flows.password: auths.append( - httpx_auth.OAuth2ResourceOwnerPasswordCredentials( + httpx2_auth.OAuth2ResourceOwnerPasswordCredentials( **value, token_url=flow.tokenUrl, scopes=flow.scopes, @@ -213,7 +213,7 @@ def _prepare_secschemes_extra(self, scheme: str, value: str | Sequence[str]) -> ) if flow := ss.flows.clientCredentials: auths.append( - httpx_auth.OAuth2ClientCredentials( + httpx2_auth.OAuth2ClientCredentials( **value, token_url=flow.tokenUrl, scopes=flow.scopes, @@ -222,7 +222,7 @@ def _prepare_secschemes_extra(self, scheme: str, value: str | Sequence[str]) -> ) if flow := ss.flows.authorizationCode: auths.append( - httpx_auth.OAuth2AuthorizationCode( + httpx2_auth.OAuth2AuthorizationCode( **value, authorization_url=flow.authorizationUrl, token_url=flow.tokenUrl, @@ -239,7 +239,7 @@ def _prepare_secschemes_extra(self, scheme: str, value: str | Sequence[str]) -> elif isinstance(value, dict): auths.append(auth(**value)) elif ss.scheme_ == "bearer": - auths.append(httpx_auth.HeaderApiKey(f"Bearer {value}", "Authorization")) + auths.append(httpx2_auth.HeaderApiKey(f"Bearer {value}", "Authorization")) else: raise ValueError(f"Authentication method {ss.type}/{ss.scheme_} is not supported by httpx-auth") @@ -490,7 +490,7 @@ def _prepare(self, data: Optional["RequestData"], parameters: Optional["RequestP mph = self._prepare_parameters(parameters) self._prepare_body(data, mph) - def _process__status_code(self, result: httpx.Response, status_code: str) -> "v3xResponseType": + def _process__status_code(self, result: httpx2.Response, status_code: str) -> "v3xResponseType": expected_response = ( self.operation.responses.get(status_code) or self.operation.responses.get(status_code[0] + "XX") @@ -508,7 +508,7 @@ def _process__status_code(self, result: httpx.Response, status_code: str) -> "v3 return expected_response def _process__headers( - self, result: httpx.Response, headers: dict[str, str], expected_response: "v3xResponseType" + self, result: httpx2.Response, headers: dict[str, str], expected_response: "v3xResponseType" ) -> "ResponseHeadersType": rheaders = dict() if expected_response.headers: @@ -530,7 +530,7 @@ def _process__headers( return rheaders def _process__content_type( - self, result: httpx.Response, expected_response: "v3xResponseType", content_type: str | None + self, result: httpx2.Response, expected_response: "v3xResponseType", content_type: str | None ) -> tuple[str, "v3xMediaTypeType"]: if content_type: """ @@ -560,7 +560,7 @@ def _process__content_type( assert content_type is not None return content_type, expected_media - def _process_stream(self, result: httpx.Response) -> tuple["ResponseHeadersType", Optional["SchemaType"]]: + def _process_stream(self, result: httpx2.Response) -> tuple["ResponseHeadersType", Optional["SchemaType"]]: status_code = str(result.status_code) content_type = result.headers.get("Content-Type", None) @@ -571,7 +571,7 @@ def _process_stream(self, result: httpx.Response) -> tuple["ResponseHeadersType" return headers, expected_media.schema_ - def _process_sequence(self, result: httpx.Response) -> tuple["ResponseHeadersType", Optional["SchemaType"], str]: + def _process_sequence(self, result: httpx2.Response) -> tuple["ResponseHeadersType", Optional["SchemaType"], str]: status_code = str(result.status_code) content_type = result.headers.get("Content-Type", None) @@ -582,7 +582,7 @@ def _process_sequence(self, result: httpx.Response) -> tuple["ResponseHeadersTyp return headers, expected_media.itemSchema, content_type - def _process_request(self, result: httpx.Response) -> tuple["ResponseHeadersType", "ResponseDataType"]: + def _process_request(self, result: httpx2.Response) -> tuple["ResponseHeadersType", "ResponseDataType"]: rheaders = dict() # spec enforces these are strings status_code = str(result.status_code) diff --git a/tests/debug_test.py b/tests/debug_test.py index a1d465c3..bc2c1406 100644 --- a/tests/debug_test.py +++ b/tests/debug_test.py @@ -1,15 +1,15 @@ import pytest -import httpx +import httpx2 import aiopenapi3.debug from aiopenapi3 import OpenAPI, ResponseSchemaError -def test_debug_log(httpx_mock, petstore_expanded): - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json={"foo": 1}) +def test_debug_log(httpx2_mock, petstore_expanded): + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json={"foo": 1}) - def debug_session_factory(*args, **kwargs) -> httpx.Client: - s = httpx.Client(*args, event_hooks=aiopenapi3.debug.httpx_debug_event_hooks(), **kwargs) + def debug_session_factory(*args, **kwargs) -> httpx2.Client: + s = httpx2.Client(*args, event_hooks=aiopenapi3.debug.httpx_debug_event_hooks(), **kwargs) return s api = OpenAPI("test.yaml", petstore_expanded, session_factory=debug_session_factory) @@ -19,11 +19,11 @@ def debug_session_factory(*args, **kwargs) -> httpx.Client: @pytest.mark.asyncio(loop_scope="session") -async def test_debug_log_async(httpx_mock, petstore_expanded): - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json={"foo": 1}) +async def test_debug_log_async(httpx2_mock, petstore_expanded): + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json={"foo": 1}) - def debug_session_factory(*args, **kwargs) -> httpx.AsyncClient: - s = httpx.AsyncClient(*args, event_hooks=aiopenapi3.debug.httpx_debug_event_hooks_async(), **kwargs) + def debug_session_factory(*args, **kwargs) -> httpx2.AsyncClient: + s = httpx2.AsyncClient(*args, event_hooks=aiopenapi3.debug.httpx_debug_event_hooks_async(), **kwargs) return s api = OpenAPI("test.yaml", petstore_expanded, session_factory=debug_session_factory) diff --git a/tests/error_test.py b/tests/error_test.py index 7a1b7981..ce11958a 100644 --- a/tests/error_test.py +++ b/tests/error_test.py @@ -1,14 +1,14 @@ from aiopenapi3 import OpenAPI from aiopenapi3 import ResponseSchemaError, ContentTypeError, HTTPStatusError, ResponseDecodingError, RequestError -import httpx +import httpx2 import pytest def test_response_error(httpx_mock, with_paths_response_error_vXX): - api = OpenAPI("/", with_paths_response_error_vXX, session_factory=httpx.Client) + api = OpenAPI("/", with_paths_response_error_vXX, session_factory=httpx2.Client) httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="ok") r = api._.test() @@ -36,13 +36,13 @@ def test_response_error(httpx_mock, with_paths_response_error_vXX): def test_request_error(with_paths_response_error_vXX): - class Client(httpx.Client): + class Client(httpx2.Client): def __init__(self, *args, **kwargs): super().__init__(*args, transport=RaisingTransport(), **kwargs) - class RaisingTransport(httpx.BaseTransport): + class RaisingTransport(httpx2.BaseTransport): def handle_request(self, request): - raise httpx.TimeoutException(message="timeout") + raise httpx2.TimeoutException(message="timeout") api = OpenAPI("/", with_paths_response_error_vXX, session_factory=Client) diff --git a/tests/extra_test.py b/tests/extra_test.py index fb720b71..b8b535af 100644 --- a/tests/extra_test.py +++ b/tests/extra_test.py @@ -3,7 +3,7 @@ from pathlib import Path -import httpx +import httpx2 import pytest from aiopenapi3 import OpenAPI @@ -87,7 +87,7 @@ def test_reduced_msgraph(): api = OpenAPI.load_file( "/api.json", "data/ms-graph-openapi.json", - session_factory=httpx.Client, + session_factory=httpx2.Client, loader=FileSystemLoader(Path("tests/").absolute()), plugins=[MSGraphReduced()], ) @@ -98,7 +98,7 @@ def test_reduced_small(): api = OpenAPI.load_file( "/", "data/petstorev3-openapi.yaml", - session_factory=httpx.Client, + session_factory=httpx2.Client, loader=FileSystemLoader(Path("tests/").absolute()), plugins=[PetStoreReduced()], ) @@ -106,11 +106,11 @@ def test_reduced_small(): @pytest.mark.parametrize("compressor", [Reduce, Cull]) -def test_reduced(with_extra_reduced, httpx_mock, compressor): +def test_reduced(with_extra_reduced, httpx2_mock, compressor): api = OpenAPI.load_file( "http://127.0.0.1/api.yaml", with_extra_reduced, - session_factory=httpx.Client, + session_factory=httpx2.Client, plugins=[], loader=FileSystemLoader(Path("tests/fixtures")), ) @@ -125,7 +125,7 @@ def test_reduced(with_extra_reduced, httpx_mock, compressor): api = OpenAPI.load_file( "http://127.0.0.1/api.yaml", with_extra_reduced, - session_factory=httpx.Client, + session_factory=httpx2.Client, plugins=[compressor(("/A/{Path}", None))], loader=FileSystemLoader(Path("tests/fixtures")), ) @@ -140,7 +140,7 @@ def test_reduced(with_extra_reduced, httpx_mock, compressor): assert "A" in api.components.responses assert "A" in api.components.requestBodies - httpx_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=dict(a=1)) from aiopenapi3.request import RequestBase @@ -154,7 +154,7 @@ def test_reduced(with_extra_reduced, httpx_mock, compressor): api = OpenAPI.load_file( "http://127.0.0.1/api.yaml", with_extra_reduced, - session_factory=httpx.Client, + session_factory=httpx2.Client, plugins=[compressor((re.compile("/B"), None))], loader=FileSystemLoader(Path("tests/fixtures")), ) @@ -168,7 +168,7 @@ def test_reduced(with_extra_reduced, httpx_mock, compressor): api = OpenAPI.load_file( "http://127.0.0.1/api.yaml", with_extra_reduced, - session_factory=httpx.Client, + session_factory=httpx2.Client, plugins=[compressor("A")], loader=FileSystemLoader(Path("tests/fixtures")), ) @@ -186,7 +186,7 @@ def test_reduced(with_extra_reduced, httpx_mock, compressor): api = OpenAPI.load_file( "http://127.0.0.1/api.yaml", with_extra_reduced, - session_factory=httpx.Client, + session_factory=httpx2.Client, plugins=[compressor(re.compile(r"[A]{1}$"))], loader=FileSystemLoader(Path("tests/fixtures")), ) @@ -205,16 +205,16 @@ def test_reduced(with_extra_reduced, httpx_mock, compressor): @pytest.mark.parametrize("cookie", [dict(policy="jar"), dict(policy="securitySchemes")], ids=["jar", "securityScheme"]) -def test_cookies(httpx_mock, with_extra_cookie, cookie): +def test_cookies(httpx2_mock, with_extra_cookie, cookie): api = OpenAPI( "http://127.0.0.1/api.yaml", with_extra_cookie, - session_factory=httpx.Client, + session_factory=httpx2.Client, plugins=[Cookies(**cookie)], ) - httpx_mock.add_response( + httpx2_mock.add_response( url="http://127.0.0.1/api/set-cookie", headers=[("Set-Cookie", "Session=value"), ("Set-Cookie", "a=b")], json='"ok"', @@ -222,16 +222,16 @@ def test_cookies(httpx_mock, with_extra_cookie, cookie): api._.set_cookie() if cookie["policy"] == "jar": - httpx_mock.add_response( + httpx2_mock.add_response( url="http://127.0.0.1/api/require-cookie", match_headers={"Cookie": "Session=value; a=b"}, json='"ok"' ) else: - httpx_mock.add_response( + httpx2_mock.add_response( url="http://127.0.0.1/api/require-cookie", match_headers={"Cookie": "Session=value"}, json='"ok"' ) api._.require_cookie() - req = httpx_mock.get_requests()[-1] + req = httpx2_mock.get_requests()[-1] if cookie["policy"] == "securitySchemes": assert req.headers.get_list("cookie") == ["Session=value"] diff --git a/tests/formdata_test.py b/tests/formdata_test.py index bbe4cebb..de7cf6cc 100644 --- a/tests/formdata_test.py +++ b/tests/formdata_test.py @@ -1,5 +1,5 @@ from pathlib import Path -import httpx +import httpx2 from aiopenapi3 import OpenAPI from aiopenapi3.v30.formdata import encode_multipart_parameters, MultipartParameter @@ -32,10 +32,10 @@ def test_encode_formdata(): assert data -def test_formdata_encoding(httpx_mock, with_paths_requestbody_formdata_encoding): - api = OpenAPI("http://localhost/api", with_paths_requestbody_formdata_encoding, session_factory=httpx.Client) +def test_formdata_encoding(httpx2_mock, with_paths_requestbody_formdata_encoding): + api = OpenAPI("http://localhost/api", with_paths_requestbody_formdata_encoding, session_factory=httpx2.Client) - httpx_mock.add_response( + httpx2_mock.add_response( headers={"Content-Type": "application/json"}, json="ok", ) @@ -48,7 +48,7 @@ def test_formdata_encoding(httpx_mock, with_paths_requestbody_formdata_encoding) profileImage=b"\x00\01\0x2", ) result = api._.encoding(data=data) - request = httpx_mock.get_request() + request = httpx2_mock.get_request() import email diff --git a/tests/path_test.py b/tests/path_test.py index 9a9ec8f9..03d754b2 100644 --- a/tests/path_test.py +++ b/tests/path_test.py @@ -8,7 +8,7 @@ import pathlib import pytest -import httpx +import httpx2 import yarl from aiopenapi3 import OpenAPI @@ -117,10 +117,10 @@ def test_operation_populated(openapi_version, petstore_expanded): assert type(con2.schema_._target) is openapi_version.schema -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_paths_security(httpx_mock, with_paths_security): - api = OpenAPI(URLBASE, with_paths_security, session_factory=httpx.Client, use_operation_tags=False) - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json="user") +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_paths_security(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") auth = str(uuid.uuid4()) @@ -141,27 +141,27 @@ def test_paths_security(httpx_mock, with_paths_security): # global security api.authenticate(None, cookieAuth=auth) api._.api_v1_auth_login_info(data={}, parameters={}) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] # path api.authenticate(None, tokenAuth=auth) api._.api_v1_auth_login_create(data={}, parameters={}) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.headers["Authorization"] == auth api.authenticate(None, paramAuth=auth) api._.api_v1_auth_login_create(data={}, parameters={}) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert yarl.URL(str(request.url)).query["auth"] == auth api.authenticate(None, cookieAuth=auth) api._.api_v1_auth_login_create(data={}, parameters={}) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.headers["Cookie"] == f"Session={auth}" api.authenticate(None, basicAuth=(auth, auth)) api._.api_v1_auth_login_create(data={}, parameters={}) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.headers["Authorization"].split(" ")[1] == base64.b64encode((auth + ":" + auth).encode()).decode() try: @@ -169,23 +169,23 @@ def test_paths_security(httpx_mock, with_paths_security): except Exception: api.authenticate(None, digestAuth=(auth, auth)) api._.api_v1_auth_login_create(data={}, parameters={}) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] # can't test? api.authenticate(None, bearerAuth=auth) api._.api_v1_auth_login_create(data={}, parameters={}) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.headers["Authorization"] == f"Bearer {auth}" # null session - via empty Operation Security api.authenticate(None) r = api._.api_v1_auth_login_null() - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] -def test_paths_security_combined(httpx_mock, with_paths_security): - api = OpenAPI(URLBASE, with_paths_security, session_factory=httpx.Client, use_operation_tags=False) - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json="user") +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") auth = str(uuid.uuid4()) @@ -202,9 +202,9 @@ def test_paths_security_combined(httpx_mock, with_paths_security): r = api._.api_v1_auth_login_combined(data={}, parameters={}) -def test_paths_parameters(httpx_mock, with_paths_parameters): - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json="test") - api = OpenAPI(URLBASE, with_paths_parameters, session_factory=httpx.Client) +def test_paths_parameters(httpx2_mock, with_paths_parameters): + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json="test") + api = OpenAPI(URLBASE, with_paths_parameters, session_factory=httpx2.Client) with pytest.raises( ValueError, match=r"Required Parameter \['Cookie', 'Header', 'Path', 'Query'\] missing \(provided \[\]\)" @@ -213,7 +213,7 @@ def test_paths_parameters(httpx_mock, with_paths_parameters): Header = [i**i for i in range(3)] api._.getTest(data={}, parameters={"Cookie": "Cookie", "Path": "Path", "Header": Header, "Query": "Query"}) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.headers["Header"] == ",".join(map(str, Header)) assert request.headers["Cookie"] == "Cookie=Cookie" @@ -231,13 +231,13 @@ def test_paths_parameters(httpx_mock, with_paths_parameters): def test_paths_parameters_invalid(with_paths_parameters_invalid): with pytest.raises(OperationParameterValidationError, match=r"Parameter names are invalid: \[\'\', \'Path:\'\]"): - OpenAPI(URLBASE, with_paths_parameters_invalid, session_factory=httpx.Client) + OpenAPI(URLBASE, with_paths_parameters_invalid, session_factory=httpx2.Client) -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_paths_parameters_oneOf(httpx_mock, with_paths_parameters_oneOf): - api = OpenAPI(URLBASE, with_paths_parameters_oneOf, session_factory=httpx.Client) - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json="test") +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_paths_parameters_oneOf(httpx2_mock, with_paths_parameters_oneOf): + api = OpenAPI(URLBASE, with_paths_parameters_oneOf, session_factory=httpx2.Client) + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json="test") api._.getTest(parameters={"nullable_array": ["x", "1"]}) api._.getTest(parameters={"nullable_array": None}) import pydantic @@ -261,23 +261,23 @@ def test_paths_parameters_oneOf(httpx_mock, with_paths_parameters_oneOf): def test_paths_parameter_missing(with_paths_parameter_missing): with pytest.raises(OperationParameterValidationError, match="Parameter name not found in parameters: missing"): - OpenAPI(URLBASE, with_paths_parameter_missing, session_factory=httpx.Client) + OpenAPI(URLBASE, with_paths_parameter_missing, session_factory=httpx2.Client) -def test_paths_parameter_default(httpx_mock, with_paths_parameter_default): - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json="default") - api = OpenAPI(URLBASE, with_paths_parameter_default, session_factory=httpx.Client) +def test_paths_parameter_default(httpx2_mock, with_paths_parameter_default): + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json="default") + api = OpenAPI(URLBASE, with_paths_parameter_default, session_factory=httpx2.Client) api._.default() - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] u = yarl.URL(str(request.url)) assert u.parts[2] == "op" assert u.parts[3] == "path" -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json="test") - api = OpenAPI(URLBASE, with_paths_parameter_format, session_factory=httpx.Client) +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_paths_parameter_format(httpx2_mock, with_paths_parameter_format): + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json="test") + api = OpenAPI(URLBASE, with_paths_parameter_format, session_factory=httpx2.Client) # using values from # https://spec.openapis.org/oas/v3.1.0#style-examples @@ -295,7 +295,7 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): ne = parameters.copy() del ne["empty"] r = api._.FormQuery(parameters=parameters) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] u = yarl.URL(str(request.url)) assert u.query["string"] == "blue" assert u.query["array"] == ",".join(parameters["array"]) @@ -303,7 +303,7 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): assert u.query["empty"] == "" r = api._.FormExplodeQuery(parameters=parameters) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] u = yarl.URL(str(request.url)) assert u.query["string"] == "blue" assert u.query.getall("array") == parameters["array"] @@ -314,7 +314,7 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): r._prepare(None, parameters) assert r.req.url == "/label/query/.blue/.blue.black.brown/.R.100.G.200.B.150/./.false/.100/.3.3245460039402305e+23" v = r.request(parameters=parameters) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] u = yarl.URL(str(request.url)) assert u.parts[4] == ".blue" assert u.parts[5] == ".blue.black.brown" @@ -322,7 +322,7 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): # assert u.parts[7] == "" # . is scrubbed as path self r = api._.LabelExplodePath(parameters=parameters) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] u = yarl.URL(str(request.url)) assert u.parts[4] == ".blue" assert u.parts[5] == ".blue.black.brown" @@ -332,7 +332,7 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): assert u.parts[9] == ".3.3245460039402305e+23" r = api._.deepObjectExplodeQuery(parameters={"object": parameters["object"]}) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] u = yarl.URL(str(request.url)) assert u.query["object[R]"] == "100" and u.query["object[G]"] == "200" and u.query["object[B]"] == "150" @@ -346,7 +346,7 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): for data in [o, o.model_dump()]: # {"size": 3, "inner": {"size": 2, "inner": {"size": 1, "inner": {}}}} r = api._.deepObjectNestedExplodeQuery(parameters={"object": data}) - request = httpx_mock.get_requests()[-1] + 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))) @@ -355,14 +355,14 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): assert all(u.query[k] == str(v) for k, v in expected.items()) r = api._.DelimitedQuery(parameters={"pipe": ["a", "b"], "space": ["1", "2"], "object": parameters["object"]}) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] u = yarl.URL(str(request.url)) assert u.query["pipe"] == "a|b" assert u.query["space"] == "1 2" assert u.query["object"] == "R 100 G 200 B 150" r = api._.matrixPath(parameters=parameters) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] u = yarl.URL(str(request.url)) assert u.parts[4] == ";string=blue" @@ -374,7 +374,7 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): assert u.parts[10] == ";number=3.3245460039402305e+23" r = api._.simpleHeader(parameters=ne) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] u = yarl.URL(str(request.url)) assert request.headers.get("string") == "blue" @@ -385,7 +385,7 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): assert request.headers.get("number") == "3.3245460039402305e+23" r = api._.simpleExplodePath(parameters=ne) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] u = yarl.URL(str(request.url)) assert u.parts[5] == "blue" @@ -398,161 +398,167 @@ def test_paths_parameter_format(httpx_mock, with_paths_parameter_format): return -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_paths_parameter_format_complex(httpx_mock, with_paths_parameter_format_complex): - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json="test") +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_paths_parameter_format_complex(httpx2_mock, with_paths_parameter_format_complex): + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json="test") - api = OpenAPI(URLBASE, with_paths_parameter_format_complex, session_factory=httpx.Client) + api = OpenAPI(URLBASE, with_paths_parameter_format_complex, session_factory=httpx2.Client) r = api._.get() r = api._.get(parameters={"value": 5}) assert r -def test_paths_response_header(httpx_mock, with_paths_response_header): - httpx_mock.add_response( +def test_paths_response_header(httpx2_mock, with_paths_response_header): + httpx2_mock.add_response( headers={"Content-Type": "application/json", "X-required": "1", "X-optional": "1,2,3"}, json="get" ) - api = OpenAPI(URLBASE, with_paths_response_header, session_factory=httpx.Client) + api = OpenAPI(URLBASE, with_paths_response_header, session_factory=httpx2.Client) h, b = api._.get(return_headers=True) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert isinstance(h["X-required"], str) o = h["X-optional"] assert isinstance(o, list) and len(o) == 3 and isinstance(o[0], str) and o[-1] == "3" with pytest.raises(HeadersMissingError) as e: - httpx_mock.add_response(headers={"Content-Type": "application/json", "X-optional": "1,2,3"}, json="get") + httpx2_mock.add_response(headers={"Content-Type": "application/json", "X-optional": "1,2,3"}, json="get") h, b = api._.get(return_headers=True) assert list(e.value.missing.keys()) == ["x-required"] - httpx_mock.add_response(headers={"Content-Type": "application/json", "X-object": "A,1,B,2,C,3"}, json="types") + 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) assert h["X-object"].A == 1 assert h["X-object"].B == "2" return -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_paths_response_content_empty(httpx_mock, with_paths_response_content_empty_vXX): - httpx_mock.add_response(status_code=200) - api = OpenAPI(URLBASE, with_paths_response_content_empty_vXX, session_factory=httpx.Client) +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_paths_response_content_empty(httpx2_mock, with_paths_response_content_empty_vXX): + httpx2_mock.add_response(status_code=200) + api = OpenAPI(URLBASE, with_paths_response_content_empty_vXX, session_factory=httpx2.Client) h, b = api._.empty(return_headers=True) assert b is None and h == {} - httpx_mock.add_response(status_code=200, headers={"X-required": "1"}) + httpx2_mock.add_response(status_code=200, headers={"X-required": "1"}) h, b = api._.headers(return_headers=True) assert b is None and h["X-required"] == "1" -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_paths_response_content_type_octet(httpx_mock, with_paths_response_content_type_octet): +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_paths_response_content_type_octet(httpx2_mock, with_paths_response_content_type_octet): CONTENT = b"\x00\x11" - httpx_mock.add_response(headers={"Content-Type": "application/octet-stream", "X-required": "1"}, content=CONTENT) - api = OpenAPI(URLBASE, with_paths_response_content_type_octet, session_factory=httpx.Client) + httpx2_mock.add_response(headers={"Content-Type": "application/octet-stream", "X-required": "1"}, content=CONTENT) + api = OpenAPI(URLBASE, with_paths_response_content_type_octet, session_factory=httpx2.Client) headers, data = api._.header(return_headers=True) assert isinstance(headers["X-required"], str) assert data == CONTENT - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] data = api._.octet() assert data == CONTENT - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_paths_tags(httpx_mock, with_paths_tags): +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_paths_tags(httpx2_mock, with_paths_tags): import copy - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json="list") - api = OpenAPI(URLBASE, with_paths_tags, session_factory=httpx.Client, use_operation_tags=True) + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json="list") + api = OpenAPI(URLBASE, with_paths_tags, session_factory=httpx2.Client, use_operation_tags=True) b = api._.users.list() r = frozenset(api._) assert frozenset(["items.list", "objects.list", "users.list"]) == r with pytest.raises(OperationIdDuplicationError, match="list"): - OpenAPI(URLBASE, with_paths_tags, session_factory=httpx.Client, use_operation_tags=False) + OpenAPI(URLBASE, with_paths_tags, session_factory=httpx2.Client, use_operation_tags=False) spec = copy.deepcopy(with_paths_tags) for k in {"/user/", "/item/"}: spec["paths"][k]["get"]["operationId"] = f"list{k[1:-1]}" - api = OpenAPI(URLBASE, spec, session_factory=httpx.Client, use_operation_tags=False) + api = OpenAPI(URLBASE, spec, session_factory=httpx2.Client, use_operation_tags=False) api._.listuser() r = frozenset(api._) assert frozenset(["listuser", "listitem"]) == r -def test_paths_response_status_pattern_default(httpx_mock, with_paths_response_status_pattern_default): - api = OpenAPI("/", with_paths_response_status_pattern_default, session_factory=httpx.Client) +def test_paths_response_status_pattern_default(httpx2_mock, with_paths_response_status_pattern_default): + api = OpenAPI("/", with_paths_response_status_pattern_default, session_factory=httpx2.Client) api.raise_on_http_status = [] - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=201, json="created") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=201, json="created") r = api._.test() assert r == "created" - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="good") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="good") r = api._.test() assert r == "good" - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=500, json="bad") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=500, json="bad") r = api._.test() assert r == "bad" - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=100, json="unknown") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=100, json="unknown") r = api._.test() assert r == "unknown" - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=500, json="notbad") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=500, json="notbad") from aiopenapi3.errors import ResponseSchemaError with pytest.raises(ResponseSchemaError): api._.test() -def test_paths_response_error(mocker, httpx_mock, with_paths_response_error_vXX): +def test_paths_response_error(mocker, httpx2_mock, with_paths_response_error_vXX): from aiopenapi3 import ResponseSchemaError, ContentTypeError, HTTPStatusError, ResponseDecodingError - api = OpenAPI("/", with_paths_response_error_vXX, session_factory=httpx.Client) + api = OpenAPI("/", with_paths_response_error_vXX, session_factory=httpx2.Client) - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="ok") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="ok") r = api._.test() assert r == "ok" - httpx_mock.add_response(headers={"Content-Type": "text/html"}, status_code=200, json="ok") + httpx2_mock.add_response(headers={"Content-Type": "text/html"}, status_code=200, json="ok") with pytest.raises(ContentTypeError): api._.test() - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=201, json="ok") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=201, json="ok") with pytest.raises(HTTPStatusError): api._.test() - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, content="'") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, content="'") with pytest.raises(ResponseDecodingError): api._.test() - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="fail") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="fail") with pytest.raises(ResponseSchemaError): api._.test() - httpx_mock.add_response(headers={"Content-Type": "application/json", "X-required": "1"}, status_code=437, json="ok") + httpx2_mock.add_response( + headers={"Content-Type": "application/json", "X-required": "1"}, status_code=437, json="ok" + ) with pytest.raises(HTTPClientError): api._.test() - httpx_mock.add_response(headers={"Content-Type": "application/json", "X-required": "1"}, status_code=537, json="ok") + httpx2_mock.add_response( + headers={"Content-Type": "application/json", "X-required": "1"}, status_code=537, json="ok" + ) with pytest.raises(HTTPServerError): api._.test() - httpx_mock.add_response(headers={"Content-Type": "application/json", "X-required": "1"}, status_code=437, json="ok") + httpx2_mock.add_response( + headers={"Content-Type": "application/json", "X-required": "1"}, status_code=437, json="ok" + ) mocker.patch.object(api, "raise_on_http_status", return_value=[], autospec=True) api._.test() -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_paths_request_calling(httpx_mock, with_paths_response_status_pattern_default): - api = OpenAPI("/", with_paths_response_status_pattern_default, session_factory=httpx.Client) +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_paths_request_calling(httpx2_mock, with_paths_response_status_pattern_default): + api = OpenAPI("/", with_paths_response_status_pattern_default, session_factory=httpx2.Client) - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=201, json="created") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=201, json="created") r = api._.test() assert r == "created" @@ -574,41 +580,41 @@ def test_paths_request_calling(httpx_mock, with_paths_response_status_pattern_de assert r == "created" -def test_paths_servers(httpx_mock, with_paths_servers): - api = OpenAPI("/", with_paths_servers, session_factory=httpx.Client) +def test_paths_servers(httpx2_mock, with_paths_servers): + api = OpenAPI("/", with_paths_servers, session_factory=httpx2.Client) assert api.url.host == "servers" - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="'ok'") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="'ok'") r = api._.servers() - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.url.host == "servers" - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=204) + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=204) r = api._.path() - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.url.host == "path" - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="'ok'") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="'ok'") r = api._.operation() - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.url.host == "operation" return -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_paths_server_variables(httpx_mock, with_paths_server_variables): - api = OpenAPI("http://example/openapi.yaml", with_paths_server_variables, session_factory=httpx.Client) +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_paths_server_variables(httpx2_mock, with_paths_server_variables): + api = OpenAPI("http://example/openapi.yaml", with_paths_server_variables, session_factory=httpx2.Client) assert api.url.host == "default" - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="'ok'") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="'ok'") r = api._.servers() - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.url.host == "default" api._server_variables = {"host": "defined"} r = api._.servers() - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.url.host == "defined" api._server_variables = {"host": "defoned"} @@ -617,14 +623,14 @@ def test_paths_server_variables(httpx_mock, with_paths_server_variables): api._server_variables = dict() - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=204) + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=204) r = api._.path() - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.url.host == "example" and request.url.path == "/v1/defined" - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="'ok'") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=200, json="'ok'") r = api._.operation() - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.url.host == "operation" and request.url.path == "/v3/defined" @@ -632,13 +638,13 @@ def test_paths_server_variables_missing(with_paths_server_variables): dd = copy.deepcopy(with_paths_server_variables) dd["servers"][0]["url"] = "https://{missing}/test" with pytest.raises(ValueError, match=r"Missing Server Variables \[\'missing\'\]"): - OpenAPI("http://example/openapi.yaml", dd, session_factory=httpx.Client) + OpenAPI("http://example/openapi.yaml", dd, session_factory=httpx2.Client) -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_paths_parameter_querystring(httpx_mock, with_paths_parameter_querystring): - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=204) - api = OpenAPI("/", with_paths_parameter_querystring, session_factory=httpx.Client) +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_paths_parameter_querystring(httpx2_mock, with_paths_parameter_querystring): + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=204) + api = OpenAPI("/", with_paths_parameter_querystring, session_factory=httpx2.Client) def querymatch(a, b): assert dict(yarl.URL(f"http://example.org/?{a}").query) == dict(yarl.URL(f"http://example.org/?{b}").query) @@ -650,7 +656,7 @@ def querymatch(a, b): ex = param.examples["spacesAndPluses"] obj = t.model_validate(ex.dataValue) api._.qs0(parameters={"qs0": obj}) - req = httpx_mock.get_requests()[-1] + req = httpx2_mock.get_requests()[-1] assert querymatch(req.url.query.decode(), ex.serializedValue) # json @@ -659,7 +665,7 @@ def querymatch(a, b): ex = param.examples["TwoNoFlag"] obj = t.model_validate(ex.dataValue) api._.json(parameters={"json": obj}) - req = httpx_mock.get_requests()[-1] + req = httpx2_mock.get_requests()[-1] assert querymatch(req.url.query.decode(), ex.serializedValue) assert req.url.query.decode() == ex.serializedValue + "=" @@ -669,6 +675,6 @@ def querymatch(a, b): ex = param.examples["Selector"] obj = t.model_validate_strings(c.example) api._.selector(parameters={"selector": obj}) - req = httpx_mock.get_requests()[-1] + req = httpx2_mock.get_requests()[-1] assert querymatch(req.url.query.decode(), ex.serializedValue) assert req.url.query.decode() == ex.serializedValue + "=" diff --git a/tests/pathv20_test.py b/tests/pathv20_test.py index 162577d7..dd72d23f 100644 --- a/tests/pathv20_test.py +++ b/tests/pathv20_test.py @@ -3,10 +3,10 @@ import urllib import yarl -import httpx +import httpx2 import pytest import python_multipart -from httpx._multipart import MultipartStream +from httpx2._multipart import MultipartStream from aiopenapi3 import OpenAPI @@ -22,11 +22,11 @@ def test_paths_security_v20_url(with_paths_security_v20): assert str(api.url) == "https://api.example.com/v1" -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_paths_security_v20_securityparameters(httpx_mock, with_paths_security_v20): - api = OpenAPI(URLBASE, with_paths_security_v20, session_factory=httpx.Client) +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_paths_security_v20_securityparameters(httpx2_mock, with_paths_security_v20): + api = OpenAPI(URLBASE, with_paths_security_v20, session_factory=httpx2.Client) user = api._.createUser.return_value().get_type().model_construct(name="test", id=1) - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json=user.model_dump()) + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json=user.model_dump()) auth = str(uuid.uuid4()) @@ -37,30 +37,30 @@ def test_paths_security_v20_securityparameters(httpx_mock, with_paths_security_v # global security api.authenticate(None, BasicAuth=(auth, auth)) api._.getUser(data={}, parameters={"userId": 1}) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] # path api.authenticate(None, QueryAuth=auth) api._.createUser(data={}, parameters={}) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.url.params["auth"] == auth # header api.authenticate(None, HeaderAuth=f"Bearer {auth}") api._.createUser(data={}, parameters={}) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.headers["Authorization"] == f"Bearer {auth}" # null session - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json=[user.model_dump()]) + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json=[user.model_dump()]) api.authenticate(None) api._.listUsers(data={}, parameters={}) -def test_paths_security_v20_combined_securityparameters(httpx_mock, with_paths_security_v20): - api = OpenAPI(URLBASE, with_paths_security_v20, session_factory=httpx.Client) +def test_paths_security_v20_combined_securityparameters(httpx2_mock, with_paths_security_v20): + api = OpenAPI(URLBASE, with_paths_security_v20, session_factory=httpx2.Client) user = api._.createUser.return_value().get_type().model_construct(name="test", id=1) - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json="combined") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json="combined") api.authenticate(user="u") with pytest.raises(ValueError, match="No security requirement satisfied"): @@ -74,10 +74,10 @@ def test_paths_security_v20_combined_securityparameters(httpx_mock, with_paths_s api._.combinedSecurity(data={}, parameters={}) -def test_paths_security_v20_alternate_securityparameters(httpx_mock, with_paths_security_v20): - api = OpenAPI(URLBASE, with_paths_security_v20, session_factory=httpx.Client) +def test_paths_security_v20_alternate_securityparameters(httpx2_mock, with_paths_security_v20): + api = OpenAPI(URLBASE, with_paths_security_v20, session_factory=httpx2.Client) user = api._.createUser.return_value().get_type().model_construct(name="test", id=1) - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json="alternate") + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json="alternate") api.authenticate(user="u") with pytest.raises( @@ -95,12 +95,12 @@ def test_paths_security_v20_alternate_securityparameters(httpx_mock, with_paths_ api._.alternateSecurity(data={}, parameters={}) -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_paths_security_v20_post_body(httpx_mock, with_paths_security_v20): +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_paths_security_v20_post_body(httpx2_mock, with_paths_security_v20): auth = str(uuid.uuid4()) - api = OpenAPI(URLBASE, with_paths_security_v20, session_factory=httpx.Client) + api = OpenAPI(URLBASE, with_paths_security_v20, session_factory=httpx2.Client) user = api._.createUser.return_value().get_type().model_construct(name="test", id=1) - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json=user.model_dump()) + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json=user.model_dump()) api.authenticate(HeaderAuth=f"Bearer {auth}") with pytest.raises(ValueError, match="Request Body is required but none was provided."): @@ -109,8 +109,8 @@ def test_paths_security_v20_post_body(httpx_mock, with_paths_security_v20): api._.createUser(data=user, parameters={}) -def test_paths_security_v20_parameters(httpx_mock, with_paths_security_v20): - api = OpenAPI(URLBASE, with_paths_security_v20, session_factory=httpx.Client) +def test_paths_security_v20_parameters(httpx2_mock, with_paths_security_v20): + api = OpenAPI(URLBASE, with_paths_security_v20, session_factory=httpx2.Client) user = api._.createUser.return_value().get_type().model_construct(name="test", id=1) auth = str(uuid.uuid4()) @@ -119,22 +119,22 @@ def test_paths_security_v20_parameters(httpx_mock, with_paths_security_v20): with pytest.raises(ValueError, match=r"Required Parameter \['userId'\] missing \(provided \[\]\)"): api._.getUser(data={}, parameters={}) - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json=[user.model_dump()]) + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json=[user.model_dump()]) api.authenticate(None) api._.listUsers(data={}, parameters={"inQuery": "Q", "inHeader": "H"}) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.headers["inHeader"] == "H" assert yarl.URL(str(request.url)).query["inQuery"] == "Q" -def test_paths_response_header_v20(httpx_mock, with_paths_response_header_v20): - httpx_mock.add_response( +def test_paths_response_header_v20(httpx2_mock, with_paths_response_header_v20): + httpx2_mock.add_response( 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=httpx.Client) + api = OpenAPI(URLBASE, with_paths_response_header_v20, session_factory=httpx2.Client) h, b = api._.get(return_headers=True) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert isinstance(h["X-required"], str) o = h["X-optional"] @@ -142,32 +142,32 @@ def test_paths_response_header_v20(httpx_mock, with_paths_response_header_v20): # seems like there is no notion of required headers in swagger # with pytest.raises(ValueError, match=r"missing Header \['x-required'\]"): - # httpx_mock.add_response( + # httpx2_mock.add_response( # headers={"Content-Type": "application/json", "X-optional": "2"}, content=b"[]" # ) # h, b = api._.get(return_headers=True) - # request = httpx_mock.get_requests()[-1] + # request = httpx2_mock.get_requests()[-1] return -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_paths_parameter_format_v20(httpx_mock, with_paths_parameter_format_v20): - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json="ok") - api = OpenAPI(URLBASE, with_paths_parameter_format_v20, session_factory=httpx.Client) +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_paths_parameter_format_v20(httpx2_mock, with_paths_parameter_format_v20): + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json="ok") + api = OpenAPI(URLBASE, with_paths_parameter_format_v20, session_factory=httpx2.Client) parameters = { "array": ["blue", "black", "brown"], "string": "blue", } r = api._.path(parameters=parameters) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] u = yarl.URL(str(request.url)) assert u.parts[4] == "blue|black|brown" assert u.parts[5] == "default" r = api._.query(parameters=parameters) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] u = yarl.URL(str(request.url)) assert u.query["default"] == "default" assert u.query["string"] == "blue" @@ -177,7 +177,7 @@ def test_paths_parameter_format_v20(httpx_mock, with_paths_parameter_format_v20) params["file0"] = ("file0name", io.BytesIO(b"x"), "ct") params["file1"] = ("file1name", io.BytesIO(b"y"), "ct") result = api._.formdata(parameters=params) - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] files = dict() @@ -205,23 +205,23 @@ def on_file(file): params = dict(A="a", B=5) result = api._.urlencoded(parameters=params) - request = httpx_mock.get_requests()[-1] + 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(httpx_mock, with_paths_parameter_format_v20): - httpx_mock.add_response(headers={"Content-Type": "application/octet-stream"}, content=b"\x00") - api = OpenAPI(URLBASE, with_paths_parameter_format_v20, session_factory=httpx.Client) +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") + api = OpenAPI(URLBASE, with_paths_parameter_format_v20, session_factory=httpx2.Client) f = api._.getfile() assert f == b"\x00" -def test_paths_stream(httpx_mock, with_paths_parameter_format_v20): - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json="ok") - api = OpenAPI(URLBASE, with_paths_parameter_format_v20, session_factory=httpx.Client) +def test_paths_stream(httpx2_mock, with_paths_parameter_format_v20): + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json="ok") + api = OpenAPI(URLBASE, with_paths_parameter_format_v20, session_factory=httpx2.Client) parameters = { "array": ["blue", "black", "brown"], diff --git a/tests/petstore_test.py b/tests/petstore_test.py index 846bd1a7..39d82cea 100644 --- a/tests/petstore_test.py +++ b/tests/petstore_test.py @@ -1,4 +1,4 @@ -import httpx +import httpx2 import pytest from aiopenapi3 import OpenAPI, ResponseSchemaError @@ -15,10 +15,10 @@ def log_response(response): print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}") -def session_factory(*args, **kwargs) -> httpx.Client: +def session_factory(*args, **kwargs) -> httpx2.Client: if False: kwargs["event_hooks"] = {"request": [log_request], "response": [log_response]} - return httpx.Client(*args, verify=False, **kwargs) + return httpx2.Client(*args, verify=False, **kwargs) class OnDocument(Document): diff --git a/tests/petstorev3_test.py b/tests/petstorev3_test.py index 4791f5bf..d937854b 100644 --- a/tests/petstorev3_test.py +++ b/tests/petstorev3_test.py @@ -1,6 +1,6 @@ import random -import httpx +import httpx2 import pytest from aiopenapi3 import OpenAPI, ResponseSchemaError @@ -9,9 +9,9 @@ try: - import httpx_auth + import httpx2_auth except ImportError: - httpx_auth = None + httpx2_auth = None def log_request(request): @@ -23,10 +23,10 @@ def log_response(response): print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}") -def session_factory(*args, **kwargs) -> httpx.Client: +def session_factory(*args, **kwargs) -> httpx2.Client: if False: kwargs["event_hooks"] = {"request": [log_request], "response": [log_response]} - return httpx.Client(*args, verify=False, **kwargs) + return httpx2.Client(*args, verify=False, **kwargs) class OnDocument(Document): @@ -136,7 +136,7 @@ def login(api, user): @pytest.mark.xfail -@pytest.mark.skipif(httpx_auth, reason="oauth does not work") +@pytest.mark.skipif(httpx2_auth, reason="oauth does not work") def test_oauth(api): """requires *working* oauth""" api.authenticate(petstore_auth={}) @@ -159,7 +159,7 @@ def test_user(api, user): @pytest.mark.xfail -@pytest.mark.skipif(httpx_auth, reason="oauth does not work") +@pytest.mark.skipif(httpx2_auth, reason="oauth does not work") def test_pets(api, login): """requires *working* oauth or no oauth""" d = api.components.schemas diff --git a/tests/plugin_test.py b/tests/plugin_test.py index 1113bdb7..df70f38c 100644 --- a/tests/plugin_test.py +++ b/tests/plugin_test.py @@ -1,7 +1,7 @@ import datetime from pathlib import Path -import httpx +import httpx2 import yarl from aiopenapi3 import FileSystemLoader, OpenAPI @@ -77,15 +77,15 @@ def unmarshalled(self, ctx): return ctx -def test_Plugins(httpx_mock, with_plugin_base): - httpx_mock.add_response(headers={"Content-Type": "application/json"}, content=b"[]") +def test_Plugins(httpx2_mock, with_plugin_base): + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, content=b"[]") plugins = [OnInit(), OnDocument("plugin-base.yaml"), OnMessage()] api = OpenAPI.loads( "plugin-base.yaml", with_plugin_base, plugins=plugins, loader=FileSystemLoader(Path().cwd() / "tests/fixtures"), - session_factory=httpx.Client, + session_factory=httpx2.Client, ) api._base_url = yarl.URL("http://127.0.0.1:80") r = api._.listPets() diff --git a/tests/schema_test.py b/tests/schema_test.py index f93136ba..4f1534d8 100644 --- a/tests/schema_test.py +++ b/tests/schema_test.py @@ -7,7 +7,7 @@ from pathlib import Path import yarl -import httpx +import httpx2 import pytest from pydantic import ValidationError import pydantic @@ -17,15 +17,15 @@ from aiopenapi3.errors import ResponseSchemaError -def test_invalid_response(httpx_mock, petstore_expanded): - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json={"foo": 1}) - api = OpenAPI("test.yaml", petstore_expanded, session_factory=httpx.Client) +def test_invalid_response(httpx2_mock, petstore_expanded): + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json={"foo": 1}) + api = OpenAPI("test.yaml", petstore_expanded, session_factory=httpx2.Client) with pytest.raises(ResponseSchemaError) as r: p = api._.find_pet_by_id(data={}, parameters={"id": 1}) -def test_schema_without_properties(httpx_mock): +def test_schema_without_properties(httpx2_mock): """ Tests that a response model is generated, and responses parsed correctly, for response schemas without properties @@ -34,9 +34,9 @@ def test_schema_without_properties(httpx_mock): "/test.yaml", Path("paths-content-schema-property-without-properties.yaml"), loader=aiopenapi3.FileSystemLoader(Path("tests/fixtures")), - session_factory=httpx.Client, + session_factory=httpx2.Client, ) - httpx_mock.add_response( + httpx2_mock.add_response( headers={"Content-Type": "application/json"}, json={ "example": "it worked", @@ -519,10 +519,10 @@ def test_schema_enum_array(with_schema_enum_array): api = OpenAPI("/", with_schema_enum_array) -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_schema_pathitems(httpx_mock, with_schema_pathitems): - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json={"foo": "bar"}) - api = OpenAPI("/", with_schema_pathitems, session_factory=httpx.Client) +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_schema_pathitems(httpx2_mock, with_schema_pathitems): + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json={"foo": "bar"}) + api = OpenAPI("/", with_schema_pathitems, session_factory=httpx2.Client) req = api.createRequest(("/a", "get")) r = req() @@ -534,7 +534,7 @@ def test_schema_pathitems(httpx_mock, with_schema_pathitems): def test_schema_baseurl_v20(with_schema_baseurl_v20): - api = OpenAPI("/", with_schema_baseurl_v20, session_factory=httpx.Client) + api = OpenAPI("/", with_schema_baseurl_v20, session_factory=httpx2.Client) assert api.url == yarl.URL("https://api.example.com:81/v1") diff --git a/tests/tls_test.py b/tests/tls_test.py index 927dfbb5..29b87419 100644 --- a/tests/tls_test.py +++ b/tests/tls_test.py @@ -3,7 +3,7 @@ import ssl from pathlib import Path -import httpx +import httpx2 import pytest import pytest_asyncio @@ -131,12 +131,12 @@ def parsed(self, ctx: "Document.Context") -> "Document.Context": @pytest_asyncio.fixture(loop_scope="session") async def client(server, certs, wait_for_server): - def self_signed(*args, **kwargs) -> httpx.AsyncClient: + def self_signed(*args, **kwargs) -> httpx2.AsyncClient: ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certs["org"]["issuer"]) if (cert := kwargs.get("cert", None)) is not None: ctx.load_cert_chain(certfile=cert[0], keyfile=cert[1]) kwargs.pop("cert") - return httpx.AsyncClient(*args, verify=ctx, **kwargs) + return httpx2.AsyncClient(*args, verify=ctx, **kwargs) api = await aiopenapi3.OpenAPI.load_async( f"https://{server.bind[0]}/openapi.json", session_factory=self_signed, plugins=[MutualTLSSecurity()] @@ -208,12 +208,12 @@ async def test_tls_optional(server, client, certs): @pytest.mark.asyncio(loop_scope="session") async def test_sync(server, certs, wait_for_server): - def self_signed_(*args, **kwargs) -> httpx.Client: + def self_signed_(*args, **kwargs) -> httpx2.Client: ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certs["org"]["issuer"]) if (cert := kwargs.get("cert", None)) is not None: ctx.load_cert_chain(certfile=cert[0], keyfile=cert[1]) kwargs.pop("cert") - return httpx.Client(*args, verify=ctx, **kwargs) + return httpx2.Client(*args, verify=ctx, **kwargs) client = await asyncio.to_thread( aiopenapi3.OpenAPI.load_sync, diff --git a/tests/v32_test.py b/tests/v32_test.py index 438d4047..ac233e79 100644 --- a/tests/v32_test.py +++ b/tests/v32_test.py @@ -1,7 +1,7 @@ -import httpx +import httpx2 import pytest -from pytest_httpx import IteratorStream +from pytest_httpx2 import IteratorStream from aiopenapi3 import OpenAPI from aiopenapi3 import v32 @@ -25,13 +25,13 @@ def test_Encoding(): pass -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) @pytest.mark.asyncio(loop_scope="session") -async def test_MediaType(httpx_mock, with_schema_itemSchema): +async def test_MediaType(httpx2_mock, with_schema_itemSchema): # itemSchema import pydantic - api = OpenAPI("https://example.org/api/", with_schema_itemSchema, session_factory=httpx.AsyncClient) + api = OpenAPI("https://example.org/api/", with_schema_itemSchema, session_factory=httpx2.AsyncClient) records = with_schema_itemSchema["components"]["examples"]["LogJSONPerLine"]["value"].strip("\n").split("\n") @@ -39,22 +39,22 @@ async def test_MediaType(httpx_mock, with_schema_itemSchema): t = pydantic.TypeAdapter(list[ServerSentEvent]) ct = "\n\n".join(f"data: {i}\n: {idx}" for idx, i in enumerate(records * 16)) - httpx_mock.add_response( + httpx2_mock.add_response( url="https://example.org/api/json_seq", headers={"Content-Type": "application/json-seq"}, stream=IteratorStream([b"\x1e" + i.encode() + b"\n" for i in (records * 16)]), ) - httpx_mock.add_response( + httpx2_mock.add_response( url="https://example.org/api/jsonl", headers={"Content-Type": "application/jsonl"}, stream=IteratorStream([i.encode() + b"\n" for i in (records * 16)]), ) - httpx_mock.add_response( + httpx2_mock.add_response( url="https://example.org/api/ndjson", headers={"Content-Type": "application/x-ndjson"}, stream=IteratorStream([i.encode() + b"\n" for i in (records * 16)]), ) - httpx_mock.add_response( + httpx2_mock.add_response( url="https://example.org/api/text_events", headers={"Content-Type": "text/event-stream"}, content=ct ) @@ -86,12 +86,12 @@ async def test_MediaType(httpx_mock, with_schema_itemSchema): pass -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_MediaType_itemSchema_sync(httpx_mock, with_schema_itemSchema): +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_MediaType_itemSchema_sync(httpx2_mock, with_schema_itemSchema): # itemSchema import pydantic - api = OpenAPI("https://example.org/api/", with_schema_itemSchema, session_factory=httpx.Client) + api = OpenAPI("https://example.org/api/", with_schema_itemSchema, session_factory=httpx2.Client) LogEntry: pydantic.BaseModel = api.components.schemas["LogEntry"].get_type() records = with_schema_itemSchema["components"]["examples"]["LogJSONPerLine"]["value"].strip("\n").split("\n") @@ -100,22 +100,22 @@ def test_MediaType_itemSchema_sync(httpx_mock, with_schema_itemSchema): t = pydantic.TypeAdapter(list[ServerSentEvent]) ct = "\n\n".join(f"data: {i}\n: {idx}" for idx, i in enumerate(records * 16)) - httpx_mock.add_response( + httpx2_mock.add_response( url="https://example.org/api/json_seq", headers={"Content-Type": "application/json-seq"}, stream=IteratorStream([b"\x1e" + i.encode() + b"\n" for i in (records * 16)]), ) - httpx_mock.add_response( + httpx2_mock.add_response( url="https://example.org/api/jsonl", headers={"Content-Type": "application/jsonl"}, stream=IteratorStream([i.encode() + b"\n" for i in (records * 16)]), ) - httpx_mock.add_response( + httpx2_mock.add_response( url="https://example.org/api/ndjson", headers={"Content-Type": "application/x-ndjson"}, stream=IteratorStream([i.encode() + b"\n" for i in (records * 16)]), ) - httpx_mock.add_response( + httpx2_mock.add_response( url="https://example.org/api/text_events", headers={"Content-Type": "text/event-stream"}, content=ct ) @@ -150,18 +150,18 @@ def test_PathItem_query(with_path_query): api = OpenAPI("https://example.org/api/", with_path_query) -@pytest.mark.httpx_mock(can_send_already_matched_responses=True) -def test_PathItem_additionalOperations(httpx_mock, with_path_additionalOperations): - httpx_mock.add_response(headers={"Content-Type": "application/json"}, json="ok") +@pytest.mark.httpx2_mock(can_send_already_matched_responses=True) +def test_PathItem_additionalOperations(httpx2_mock, with_path_additionalOperations): + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, json="ok") - api = OpenAPI("https://example.org/api/", with_path_additionalOperations, session_factory=httpx.Client) + api = OpenAPI("https://example.org/api/", with_path_additionalOperations, session_factory=httpx2.Client) assert api._.test() == "ok" - request: httpx.Request = httpx_mock.get_requests()[-1] + request: httpx2.Request = httpx2_mock.get_requests()[-1] assert request.method == "TEST" and request.url.path == "/api/data" assert api._[("/api/data", "test")]() == "ok" - request = httpx_mock.get_requests()[-1] + request = httpx2_mock.get_requests()[-1] assert request.method == "TEST" and request.url.path == "/api/data" @@ -191,9 +191,9 @@ def test_Server_name(): pass -def test_Tag(httpx_mock, with_schema_tags_v32): - httpx_mock.add_response(headers={"Content-Type": "application/json"}, status_code=204) - api = OpenAPI("https://example.org/api/", with_schema_tags_v32, session_factory=httpx.Client) +def test_Tag(httpx2_mock, with_schema_tags_v32): + httpx2_mock.add_response(headers={"Content-Type": "application/json"}, status_code=204) + api = OpenAPI("https://example.org/api/", with_schema_tags_v32, session_factory=httpx2.Client) api._.external.partner.x() assert sorted(filter(lambda x: x.partition(".")[0] == "external", api._.Iter(api, True))) == ["external.partner.x"] diff --git a/uv.lock b/uv.lock index 4d2a07f2..086fe0a3 100644 --- a/uv.lock +++ b/uv.lock @@ -7,7 +7,7 @@ name = "aiopenapi3" source = { editable = "." } dependencies = [ { name = "email-validator" }, - { name = "httpx" }, + { name = "httpx2" }, { name = "ijson" }, { name = "jmespath" }, { name = "jsonseq" }, @@ -34,13 +34,13 @@ dev = [ { name = "fastapi-versioning" }, { name = "flask" }, { name = "flask-wtf" }, + { name = "httpx2-pytest" }, { name = "ijson" }, { name = "nonecorn" }, { name = "pydantic-extra-types" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, - { name = "pytest-httpx" }, { name = "pytest-mock" }, { name = "python-multipart" }, { name = "trustme" }, @@ -51,8 +51,8 @@ dev = [ [package.metadata] requires-dist = [ { name = "email-validator" }, - { name = "httpx" }, { name = "httpx-auth", marker = "extra == 'auth'", specifier = ">=0.21.0" }, + { name = "httpx2" }, { name = "ijson" }, { name = "jmespath" }, { name = "jsonseq" }, @@ -73,13 +73,13 @@ dev = [ { name = "fastapi-versioning" }, { name = "flask" }, { name = "flask-wtf" }, + { name = "httpx2-pytest" }, { name = "ijson" }, { name = "nonecorn", specifier = "!=0.18.0" }, { name = "pydantic-extra-types", specifier = ">=2.10.1" }, { name = "pytest" }, { name = "pytest-asyncio", specifier = ">=0.24.0" }, { name = "pytest-cov" }, - { name = "pytest-httpx" }, { name = "pytest-mock" }, { name = "python-multipart", specifier = ">=0.0.6" }, { name = "trustme" }, @@ -590,6 +590,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httpcore2" +version = "2.2.0" +source = { registry = "https://pypi.org/simple/" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/7e/8ab39aab1d392845b6512009a9be57d24a5bd4ec7a22d02e513d0645e7a8/httpcore2-2.2.0.tar.gz", hash = "sha256:10e0e142f1ecc1c1cb2a9ebbce82e57f16169f61d163ea336abf36799e89294b", size = 63533, upload-time = "2026-05-17T05:29:55.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/22/64de17e7956e8c002f7558ed667d924c2a288344aeff4bd8ff5dc5fdb70b/httpcore2-2.2.0-py3-none-any.whl", hash = "sha256:ce859f268bf8d34fa2d7753e09e4dd5194f557e1b3038439b68a89b2999572fa", size = 79288, upload-time = "2026-05-17T05:29:52.56Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -617,6 +630,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/23/a72f91bea596b522ac297b948ffee6decdedb535c034fca8062bd72981ce/httpx_auth-0.23.1-py3-none-any.whl", hash = "sha256:04f8bd0824efe3d9fb79690cc670b0da98ea809babb7aea04a72f334d4fd5ec5", size = 45328, upload-time = "2025-01-07T18:47:18.694Z" }, ] +[[package]] +name = "httpx2" +version = "2.2.0" +source = { registry = "https://pypi.org/simple/" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore2" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/aa/c3119de1aa7ad870a01aaddbf3bc3445ed9a681c31d45e3838fd8b7bc155/httpx2-2.2.0.tar.gz", hash = "sha256:f3428d59b1752b8f5629826277262fb4d65e3a683f48af8a5b16c4d012e0b801", size = 80477, upload-time = "2026-05-17T05:29:57.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/e0/e0a52596c14194e428c20de4903f4abec38c0dfb5364d20f1d4a2b6266ef/httpx2-2.2.0-py3-none-any.whl", hash = "sha256:12347ebd2daeaefd50b529359778fff767082a09c5826752c963e71269722ff0", size = 74083, upload-time = "2026-05-17T05:29:54.543Z" }, +] + +[[package]] +name = "httpx2-pytest" +version = "1.0.1" +source = { registry = "https://pypi.org/simple/" } +dependencies = [ + { name = "httpx2" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/c9/bc6405b80e0d8a2b0e155aa9976864a79a96fef5478c564cc0d89d23cb86/httpx2_pytest-1.0.1.tar.gz", hash = "sha256:e5d36bbac673bae6e11d3520e74bd360c3e392b56270efcb95f1d5369cbd1ec1", size = 59136, upload-time = "2026-05-22T15:02:54.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/ef/e9b6fb576f1f518f68e9f9cce9776870452ae2b8cfbba53b4127701b9a55/httpx2_pytest-1.0.1-py3-none-any.whl", hash = "sha256:09ff3365101508c5b311301c6ee8e1a96dbb676028b19e00da66199d2e759a99", size = 20975, upload-time = "2026-05-22T15:02:54.007Z" }, +] + [[package]] name = "hyperframe" version = "6.1.0" @@ -1387,19 +1428,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] -[[package]] -name = "pytest-httpx" -version = "0.36.2" -source = { registry = "https://pypi.org/simple/" } -dependencies = [ - { name = "httpx" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4e/42/f53c58570e80d503ade9dd42ce57f2915d14bcbe25f6308138143950d1d6/pytest_httpx-0.36.2.tar.gz", hash = "sha256:05a56527484f7f4e8c856419ea379b8dc359c36801c4992fdb330f294c690356", size = 57683, upload-time = "2026-04-09T13:57:19.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/55/1fa65f8e4fceb19dd6daa867c162ad845d547f6058cd92b4b02384a44777/pytest_httpx-0.36.2-py3-none-any.whl", hash = "sha256:d42ebd5679442dc7bfb0c48e0767b6562e9bc4534d805127b0084171886a5e22", size = 20315, upload-time = "2026-04-09T13:57:18.587Z" }, -] - [[package]] name = "pytest-mock" version = "3.15.1"