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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpware
3
- Version: 0.2.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>=2.0,<3.0
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 typed, async HTTP client library with a protocol-based seam so the transport is swappable (`httpx2` ships as the default). Middleware composes via an onion model. Pydantic and msgspec response decoding ship out of the box. `RecordedTransport` replaces `respx` for transport-level tests.
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.1.0 alpha). 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
+ > **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
- Optional extras:
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 typed, async HTTP client library with a protocol-based seam so the transport is swappable (`httpx2` ships as the default). Middleware composes via an onion model. Pydantic and msgspec response decoding ship out of the box. `RecordedTransport` replaces `respx` for transport-level tests.
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.1.0 alpha). Public API is subject to change between minor releases until v1.0. Resilience middleware (retry / timeout / bulkhead), streaming, and observability are not yet shipped.
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
- Optional extras:
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.2.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",
@@ -4,3 +4,4 @@ from importlib.util import find_spec
4
4
 
5
5
 
6
6
  is_msgspec_installed = find_spec("msgspec") is not None
7
+ is_pydantic_installed = find_spec("pydantic") is not None
@@ -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 PydanticDecoder()
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