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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 25 additions & 25 deletions docs/source/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://www.python-httpx.org/api/#client>`_.
to authenticate to the remote server, c.f. :ref:`httpx2.Client.cert <https://httpx2.pydantic.dev/api/#client>`_.

.. 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 <advanced: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 <advanced:Session Factory>`.


Forms
Expand Down Expand Up @@ -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

Expand All @@ -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 <https://www.python-httpx.org/quickstart/#sending-multipart-file-uploads>`_ &
`encoding <https://www.python-httpx.org/advanced/#multipart-file-encoding>`_)
File uploads via "multipart/form-data" as mentioned in the httpx2 documentation
(Multipart file `uploads <https://httpx2.pydantic.dev/quickstart/#sending-multipart-file-uploads>`_ &
`encoding <https://httpx2.pydantic.dev/advanced/#multipart-file-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".

Expand Down Expand Up @@ -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 <https://www.python-httpx.org/advanced/#event-hooks>`_:
E.g. setting `httpx2 Event Hooks <https://httpx2.pydantic.dev/advanced/#event-hooks>`_:

.. code:: python

Expand All @@ -395,32 +395,32 @@ E.g. setting `httpx Event Hooks <https://www.python-httpx.org/advanced/#event-ho
request = response.request
print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}")

def session_factory(*args, **kwargs) -> 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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ HTTPError is the base class for all request/response related errors.
:members:
:undoc-members:

A RequestError typically wraps an `error <https://www.python-httpx.org/exceptions/>`_ of the underlying httpx_ library.
A RequestError typically wraps an `error <https://httpx2.pydantic.dev/exceptions/>`_ of the underlying httpx2_ library.

.. autoexception:: ResponseError
:members:
Expand Down
2 changes: 1 addition & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/commonism/aiopenapi3>`_.


Expand Down
2 changes: 1 addition & 1 deletion docs/source/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/Colin-b/httpx_auth/pull/48>`_ operations.
* aiopenapi3[auth] will install httpx2-auth_ which is required to authenticate using oauth2/azuread/. Currently httpx2-auth is `limited to Sync <https://github.com/Colin-b/httpx2_auth/pull/48>`_ operations.
4 changes: 2 additions & 2 deletions docs/source/links.rst
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions docs/source/use.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ dependencies = [
"pydantic >= 2.13.0b2",
"email-validator",
"yarl",
"httpx",
"httpx2",
"more-itertools",
'typing_extensions; python_version<"3.12"',
"jmespath",
Expand Down Expand Up @@ -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",
Expand Down
16 changes: 8 additions & 8 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,28 @@
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
# via aiopenapi3
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
Expand Down
8 changes: 4 additions & 4 deletions src/aiopenapi3/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import jmespath
import yaml
import yarl
import httpx
import httpx2

import aiopenapi3.plugin

Expand Down Expand Up @@ -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:
Expand Down
14 changes: 7 additions & 7 deletions src/aiopenapi3/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Optional
import dataclasses

import httpx
import httpx2
import pydantic

if typing.TYPE_CHECKING:
Expand Down Expand Up @@ -120,7 +120,7 @@ class ContentLengthExceededError(ResponseError):
operation: "OperationType"
content_length: int
message: str
response: httpx.Response
response: httpx2.Response


@dataclasses.dataclass(repr=False)
Expand All @@ -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})>
Expand All @@ -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})>
Expand All @@ -157,7 +157,7 @@ class ResponseDecodingError(ResponseError):

operation: "OperationType"
data: str
response: httpx.Response
response: httpx2.Response


@dataclasses.dataclass(repr=False)
Expand All @@ -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):
Expand All @@ -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})
Expand Down
4 changes: 2 additions & 2 deletions src/aiopenapi3/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
import typing
import yaml
import httpx
import httpx2
import yarl
import re

Expand Down Expand Up @@ -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
Expand Down
Loading