httpware 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {httpware-0.2.0 → httpware-0.3.0}/PKG-INFO +13 -13
- {httpware-0.2.0 → httpware-0.3.0}/README.md +9 -10
- {httpware-0.2.0 → httpware-0.3.0}/pyproject.toml +3 -3
- {httpware-0.2.0 → httpware-0.3.0}/src/httpware/__init__.py +0 -2
- {httpware-0.2.0 → httpware-0.3.0}/src/httpware/_internal/import_checker.py +1 -0
- {httpware-0.2.0 → httpware-0.3.0}/src/httpware/client.py +16 -2
- httpware-0.3.0/src/httpware/decoders/pydantic.py +44 -0
- httpware-0.2.0/src/httpware/decoders/pydantic.py +0 -29
- {httpware-0.2.0 → httpware-0.3.0}/src/httpware/_internal/__init__.py +0 -0
- {httpware-0.2.0 → httpware-0.3.0}/src/httpware/decoders/__init__.py +0 -0
- {httpware-0.2.0 → httpware-0.3.0}/src/httpware/decoders/msgspec.py +0 -0
- {httpware-0.2.0 → httpware-0.3.0}/src/httpware/errors.py +0 -0
- {httpware-0.2.0 → httpware-0.3.0}/src/httpware/middleware/__init__.py +0 -0
- {httpware-0.2.0 → httpware-0.3.0}/src/httpware/middleware/chain.py +0 -0
- {httpware-0.2.0 → httpware-0.3.0}/src/httpware/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: httpware
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Resilience-first async HTTP client framework for Python
|
|
5
5
|
Keywords: http,async,client,resilience,retry,circuit-breaker,middleware,httpx,pydantic
|
|
6
6
|
Author: Artur Shiriev
|
|
@@ -15,17 +15,18 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
15
15
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
16
16
|
Classifier: Framework :: AsyncIO
|
|
17
17
|
Requires-Dist: httpx2>=2.0.0,<3.0
|
|
18
|
-
Requires-Dist: pydantic
|
|
19
|
-
Requires-Dist: httpware[msgspec,otel] ; extra == 'all'
|
|
18
|
+
Requires-Dist: httpware[pydantic,msgspec,otel] ; extra == 'all'
|
|
20
19
|
Requires-Dist: msgspec>=0.18 ; extra == 'msgspec'
|
|
21
20
|
Requires-Dist: opentelemetry-api>=1.20 ; extra == 'otel'
|
|
22
21
|
Requires-Dist: opentelemetry-sdk>=1.20 ; extra == 'otel'
|
|
22
|
+
Requires-Dist: pydantic>=2.0,<3.0 ; extra == 'pydantic'
|
|
23
23
|
Requires-Python: >=3.11, <4
|
|
24
24
|
Project-URL: repository, https://github.com/modern-python/httpware
|
|
25
25
|
Project-URL: docs, https://httpware.readthedocs.io
|
|
26
26
|
Provides-Extra: all
|
|
27
27
|
Provides-Extra: msgspec
|
|
28
28
|
Provides-Extra: otel
|
|
29
|
+
Provides-Extra: pydantic
|
|
29
30
|
Description-Content-Type: text/markdown
|
|
30
31
|
|
|
31
32
|
# httpware
|
|
@@ -37,26 +38,25 @@ Description-Content-Type: text/markdown
|
|
|
37
38
|
|
|
38
39
|
**Async HTTP client framework for Python.**
|
|
39
40
|
|
|
40
|
-
`httpware` is a
|
|
41
|
+
`httpware` is a thin opinionated wrapper around `httpx2`. It re-exports `httpx2.Request`/`httpx2.Response`, adds a middleware chain composed at client construction, supports opt-in typed response decoding (pydantic and msgspec are both extras), and raises a status-keyed exception tree automatically on 4xx/5xx.
|
|
41
42
|
|
|
42
|
-
> **Status:** Pre-1.0 (0.
|
|
43
|
+
> **Status:** Pre-1.0 (0.3.0). Public API is subject to change between minor releases until v1.0. Resilience middleware (retry / timeout / bulkhead), streaming, and observability are not yet shipped.
|
|
43
44
|
|
|
44
45
|
## Install
|
|
45
46
|
|
|
46
47
|
```bash
|
|
47
|
-
pip install httpware
|
|
48
|
+
pip install httpware # core only — no decoder
|
|
49
|
+
pip install httpware[pydantic] # + PydanticDecoder (the default-decoder path)
|
|
50
|
+
pip install httpware[msgspec] # + MsgspecDecoder
|
|
51
|
+
pip install httpware[all] # everything declared above (pydantic, msgspec, otel)
|
|
48
52
|
```
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
pip install httpware[msgspec] # MsgspecDecoder
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
(`otel`, `niquests`, and `all` extras are declared; integrations have not shipped yet.)
|
|
54
|
+
`AsyncClient()` with no `decoder=` argument defaults to constructing a `PydanticDecoder`; that path requires the `pydantic` extra and raises `ImportError` at `AsyncClient.__init__` if it is missing. The `otel` extra is declared but the OpenTelemetry middleware (Epic 5) has not shipped yet.
|
|
57
55
|
|
|
58
56
|
## Quickstart
|
|
59
57
|
|
|
58
|
+
> Requires: `pip install httpware[pydantic]`
|
|
59
|
+
|
|
60
60
|
```python
|
|
61
61
|
from httpware import AsyncClient
|
|
62
62
|
from pydantic import BaseModel
|
|
@@ -7,26 +7,25 @@
|
|
|
7
7
|
|
|
8
8
|
**Async HTTP client framework for Python.**
|
|
9
9
|
|
|
10
|
-
`httpware` is a
|
|
10
|
+
`httpware` is a thin opinionated wrapper around `httpx2`. It re-exports `httpx2.Request`/`httpx2.Response`, adds a middleware chain composed at client construction, supports opt-in typed response decoding (pydantic and msgspec are both extras), and raises a status-keyed exception tree automatically on 4xx/5xx.
|
|
11
11
|
|
|
12
|
-
> **Status:** Pre-1.0 (0.
|
|
12
|
+
> **Status:** Pre-1.0 (0.3.0). Public API is subject to change between minor releases until v1.0. Resilience middleware (retry / timeout / bulkhead), streaming, and observability are not yet shipped.
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
pip install httpware
|
|
17
|
+
pip install httpware # core only — no decoder
|
|
18
|
+
pip install httpware[pydantic] # + PydanticDecoder (the default-decoder path)
|
|
19
|
+
pip install httpware[msgspec] # + MsgspecDecoder
|
|
20
|
+
pip install httpware[all] # everything declared above (pydantic, msgspec, otel)
|
|
18
21
|
```
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
pip install httpware[msgspec] # MsgspecDecoder
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
(`otel`, `niquests`, and `all` extras are declared; integrations have not shipped yet.)
|
|
23
|
+
`AsyncClient()` with no `decoder=` argument defaults to constructing a `PydanticDecoder`; that path requires the `pydantic` extra and raises `ImportError` at `AsyncClient.__init__` if it is missing. The `otel` extra is declared but the OpenTelemetry middleware (Epic 5) has not shipped yet.
|
|
27
24
|
|
|
28
25
|
## Quickstart
|
|
29
26
|
|
|
27
|
+
> Requires: `pip install httpware[pydantic]`
|
|
28
|
+
|
|
30
29
|
```python
|
|
31
30
|
from httpware import AsyncClient
|
|
32
31
|
from pydantic import BaseModel
|
|
@@ -26,19 +26,19 @@ classifiers = [
|
|
|
26
26
|
"Topic :: Internet :: WWW/HTTP",
|
|
27
27
|
"Framework :: AsyncIO",
|
|
28
28
|
]
|
|
29
|
-
version = "0.
|
|
29
|
+
version = "0.3.0"
|
|
30
30
|
dependencies = [
|
|
31
31
|
"httpx2>=2.0.0,<3.0",
|
|
32
|
-
"pydantic>=2.0,<3.0",
|
|
33
32
|
]
|
|
34
33
|
|
|
35
34
|
[project.optional-dependencies]
|
|
35
|
+
pydantic = ["pydantic>=2.0,<3.0"]
|
|
36
36
|
msgspec = ["msgspec>=0.18"]
|
|
37
37
|
otel = [
|
|
38
38
|
"opentelemetry-api>=1.20",
|
|
39
39
|
"opentelemetry-sdk>=1.20",
|
|
40
40
|
]
|
|
41
|
-
all = ["httpware[msgspec,otel]"]
|
|
41
|
+
all = ["httpware[pydantic,msgspec,otel]"]
|
|
42
42
|
|
|
43
43
|
[project.urls]
|
|
44
44
|
repository = "https://github.com/modern-python/httpware"
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from httpware.client import AsyncClient
|
|
4
4
|
from httpware.decoders import ResponseDecoder
|
|
5
|
-
from httpware.decoders.pydantic import PydanticDecoder
|
|
6
5
|
from httpware.errors import (
|
|
7
6
|
STATUS_TO_EXCEPTION,
|
|
8
7
|
BadRequestError,
|
|
@@ -36,7 +35,6 @@ __all__ = [
|
|
|
36
35
|
"Middleware",
|
|
37
36
|
"Next",
|
|
38
37
|
"NotFoundError",
|
|
39
|
-
"PydanticDecoder",
|
|
40
38
|
"RateLimitedError",
|
|
41
39
|
"ResponseDecoder",
|
|
42
40
|
"ServerStatusError",
|
|
@@ -6,8 +6,8 @@ from http import HTTPStatus
|
|
|
6
6
|
|
|
7
7
|
import httpx2
|
|
8
8
|
|
|
9
|
+
from httpware._internal import import_checker
|
|
9
10
|
from httpware.decoders import ResponseDecoder
|
|
10
|
-
from httpware.decoders.pydantic import PydanticDecoder
|
|
11
11
|
from httpware.errors import (
|
|
12
12
|
STATUS_TO_EXCEPTION,
|
|
13
13
|
ClientStatusError,
|
|
@@ -28,6 +28,20 @@ _HTTPX2_CLIENT_CONFLICT_MESSAGE = (
|
|
|
28
28
|
f"{_FORWARDED_KWARG_NAMES}; configure the httpx2.AsyncClient you pass instead."
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
_DEFAULT_DECODER_MISSING_MESSAGE = (
|
|
32
|
+
"AsyncClient(decoder=None) defaults to PydanticDecoder, which requires the "
|
|
33
|
+
"'pydantic' extra. Either install it (`pip install httpware[pydantic]`) or "
|
|
34
|
+
"pass an explicit decoder=..."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _default_pydantic_decoder() -> ResponseDecoder:
|
|
39
|
+
if not import_checker.is_pydantic_installed:
|
|
40
|
+
raise ImportError(_DEFAULT_DECODER_MISSING_MESSAGE)
|
|
41
|
+
from httpware.decoders.pydantic import PydanticDecoder # noqa: PLC0415 — lazy by design
|
|
42
|
+
|
|
43
|
+
return PydanticDecoder()
|
|
44
|
+
|
|
31
45
|
|
|
32
46
|
class AsyncClient:
|
|
33
47
|
"""Async HTTP client: thin wrapper around httpx2 with typed decoding and middleware."""
|
|
@@ -85,7 +99,7 @@ class AsyncClient:
|
|
|
85
99
|
self._httpx2_client = httpx2.AsyncClient(**kwargs)
|
|
86
100
|
self._owns_client = True
|
|
87
101
|
|
|
88
|
-
self._decoder = decoder if decoder is not None else
|
|
102
|
+
self._decoder = decoder if decoder is not None else _default_pydantic_decoder()
|
|
89
103
|
self._user_middleware = tuple(middleware)
|
|
90
104
|
self._dispatch = compose(self._user_middleware, self._terminal)
|
|
91
105
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""PydanticDecoder — module-level cached TypeAdapter adapter for ResponseDecoder.
|
|
2
|
+
|
|
3
|
+
Requires the `pydantic` extra: `pip install httpware[pydantic]`. Importing this
|
|
4
|
+
module without the extra works (the `pydantic` import is guarded by a
|
|
5
|
+
`find_spec` check), but instantiating the decoder raises `ImportError` with the
|
|
6
|
+
install hint.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import functools
|
|
10
|
+
from typing import TypeVar
|
|
11
|
+
|
|
12
|
+
from httpware._internal import import_checker
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if import_checker.is_pydantic_installed:
|
|
16
|
+
from pydantic import TypeAdapter
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
MISSING_DEPENDENCY_MESSAGE = (
|
|
20
|
+
"PydanticDecoder requires the 'pydantic' extra. Install with: pip install httpware[pydantic]"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@functools.lru_cache(maxsize=1024)
|
|
27
|
+
def _get_adapter(model: type[T]) -> "TypeAdapter[T]":
|
|
28
|
+
return TypeAdapter(model)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PydanticDecoder:
|
|
32
|
+
"""Decode raw response bytes into `model` via a cached `pydantic.TypeAdapter`."""
|
|
33
|
+
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
if not import_checker.is_pydantic_installed:
|
|
36
|
+
raise ImportError(MISSING_DEPENDENCY_MESSAGE)
|
|
37
|
+
|
|
38
|
+
def decode(self, content: bytes, model: type[T]) -> T:
|
|
39
|
+
"""Validate `content` as JSON against `model` in a single parse pass."""
|
|
40
|
+
try:
|
|
41
|
+
adapter = _get_adapter(model)
|
|
42
|
+
except TypeError:
|
|
43
|
+
adapter = TypeAdapter(model)
|
|
44
|
+
return adapter.validate_json(content)
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
"""PydanticDecoder — module-level cached TypeAdapter adapter for ResponseDecoder."""
|
|
2
|
-
|
|
3
|
-
import functools
|
|
4
|
-
from typing import TypeVar
|
|
5
|
-
|
|
6
|
-
from pydantic import TypeAdapter
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
T = TypeVar("T")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@functools.lru_cache(maxsize=1024)
|
|
13
|
-
def _get_adapter(model: type[T]) -> TypeAdapter[T]:
|
|
14
|
-
return TypeAdapter(model)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class PydanticDecoder:
|
|
18
|
-
"""Decode raw response bytes into `model` via a cached `pydantic.TypeAdapter`."""
|
|
19
|
-
|
|
20
|
-
def decode(self, content: bytes, model: type[T]) -> T:
|
|
21
|
-
"""Validate `content` as JSON against `model` in a single parse pass."""
|
|
22
|
-
try:
|
|
23
|
-
adapter = _get_adapter(model)
|
|
24
|
-
except TypeError:
|
|
25
|
-
adapter = TypeAdapter(model)
|
|
26
|
-
return adapter.validate_json(content)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
__all__ = ["PydanticDecoder"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|