httpware 0.8.0__tar.gz → 0.8.1__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.
Files changed (22) hide show
  1. {httpware-0.8.0 → httpware-0.8.1}/PKG-INFO +2 -2
  2. {httpware-0.8.0 → httpware-0.8.1}/README.md +1 -1
  3. {httpware-0.8.0 → httpware-0.8.1}/pyproject.toml +1 -1
  4. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/__init__.py +2 -0
  5. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/client.py +9 -3
  6. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/decoders/__init__.py +6 -1
  7. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/errors.py +42 -0
  8. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/_internal/__init__.py +0 -0
  9. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/_internal/exception_mapping.py +0 -0
  10. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/_internal/import_checker.py +0 -0
  11. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/_internal/observability.py +0 -0
  12. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/_internal/status.py +0 -0
  13. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/decoders/msgspec.py +0 -0
  14. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/decoders/pydantic.py +0 -0
  15. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/middleware/__init__.py +0 -0
  16. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/middleware/chain.py +0 -0
  17. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/middleware/resilience/__init__.py +0 -0
  18. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/middleware/resilience/_backoff.py +0 -0
  19. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/middleware/resilience/budget.py +0 -0
  20. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/middleware/resilience/bulkhead.py +0 -0
  21. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/middleware/resilience/retry.py +0 -0
  22. {httpware-0.8.0 → httpware-0.8.1}/src/httpware/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpware
3
- Version: 0.8.0
3
+ Version: 0.8.1
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
@@ -79,7 +79,7 @@ with Client(base_url="https://example.test") as client:
79
79
  print(response.json())
80
80
  ```
81
81
 
82
- Typed decoding via `response_model=` works in both worlds — requires `pip install httpware[pydantic]`:
82
+ Typed decoding via `response_model=` works in both worlds — requires `pip install httpware[pydantic]`. Decode failures (malformed body, schema mismatch) raise `httpware.DecodeError`, a `ClientError` subclass — so `except httpware.ClientError` covers them alongside transport and status errors.
83
83
 
84
84
  ```python
85
85
  from httpware import AsyncClient
@@ -49,7 +49,7 @@ with Client(base_url="https://example.test") as client:
49
49
  print(response.json())
50
50
  ```
51
51
 
52
- Typed decoding via `response_model=` works in both worlds — requires `pip install httpware[pydantic]`:
52
+ Typed decoding via `response_model=` works in both worlds — requires `pip install httpware[pydantic]`. Decode failures (malformed body, schema mismatch) raise `httpware.DecodeError`, a `ClientError` subclass — so `except httpware.ClientError` covers them alongside transport and status errors.
53
53
 
54
54
  ```python
55
55
  from httpware import AsyncClient
@@ -26,7 +26,7 @@ classifiers = [
26
26
  "Topic :: Internet :: WWW/HTTP",
27
27
  "Framework :: AsyncIO",
28
28
  ]
29
- version = "0.8.0"
29
+ version = "0.8.1"
30
30
  dependencies = [
31
31
  "httpx2>=2.0.0,<3.0",
32
32
  ]
@@ -9,6 +9,7 @@ from httpware.errors import (
9
9
  ClientError,
10
10
  ClientStatusError,
11
11
  ConflictError,
12
+ DecodeError,
12
13
  ForbiddenError,
13
14
  InternalServerError,
14
15
  NetworkError,
@@ -52,6 +53,7 @@ __all__ = [
52
53
  "ClientError",
53
54
  "ClientStatusError",
54
55
  "ConflictError",
56
+ "DecodeError",
55
57
  "ForbiddenError",
56
58
  "InternalServerError",
57
59
  "Middleware",
@@ -16,7 +16,7 @@ from httpware._internal.status import (
16
16
  _raise_on_status_error,
17
17
  )
18
18
  from httpware.decoders import ResponseDecoder
19
- from httpware.errors import TransportError
19
+ from httpware.errors import DecodeError, TransportError
20
20
  from httpware.middleware import AsyncMiddleware, AsyncNext, Middleware, Next
21
21
  from httpware.middleware.chain import compose, compose_async
22
22
 
@@ -154,7 +154,10 @@ class AsyncClient:
154
154
  response = await self._dispatch(request)
155
155
  if response_model is None:
156
156
  return response
157
- return self._decoder.decode(response.content, response_model)
157
+ try:
158
+ return self._decoder.decode(response.content, response_model)
159
+ except Exception as exc:
160
+ raise DecodeError(response=response, model=response_model, original=exc) from exc
158
161
 
159
162
  def build_request(self, method: str, url: str, **kwargs: typing.Any) -> httpx2.Request:
160
163
  """Delegate request construction to the wrapped httpx2.AsyncClient."""
@@ -871,7 +874,10 @@ class Client:
871
874
  response = self._dispatch(request)
872
875
  if response_model is None:
873
876
  return response
874
- return self._decoder.decode(response.content, response_model)
877
+ try:
878
+ return self._decoder.decode(response.content, response_model)
879
+ except Exception as exc:
880
+ raise DecodeError(response=response, model=response_model, original=exc) from exc
875
881
 
876
882
  def build_request(self, method: str, url: str, **kwargs: typing.Any) -> httpx2.Request:
877
883
  """Delegate request construction to the wrapped httpx2.Client."""
@@ -11,7 +11,12 @@ class ResponseDecoder(Protocol):
11
11
  """Structural protocol every response-body decoder satisfies."""
12
12
 
13
13
  def decode(self, content: bytes, model: type[T]) -> T:
14
- """Decode `content` (raw response bytes) into an instance of `model`."""
14
+ """Decode `content` (raw response bytes) into an instance of `model`.
15
+
16
+ Any exception raised by `decode` is wrapped by `Client.send` /
17
+ `AsyncClient.send` into `httpware.DecodeError`; implementers do not
18
+ need to raise `DecodeError` directly.
19
+ """
15
20
  ...
16
21
 
17
22
 
@@ -212,3 +212,45 @@ class BulkheadFullError(ClientError):
212
212
  _reconstruct_bulkhead_full,
213
213
  (type(self), self.max_concurrent, self.acquire_timeout),
214
214
  )
215
+
216
+
217
+ def _reconstruct_decode_error(
218
+ cls: "type[DecodeError]",
219
+ response: httpx2.Response,
220
+ model: type,
221
+ original: BaseException,
222
+ ) -> "DecodeError":
223
+ return cls(response=response, model=model, original=original)
224
+
225
+
226
+ class DecodeError(ClientError):
227
+ """Raised when the active ResponseDecoder failed to decode response.content.
228
+
229
+ The HTTP call itself succeeded — status was 2xx/3xx and the transport
230
+ delivered the body intact — but the body could not be parsed into the
231
+ requested response_model. Always chained from the underlying library
232
+ exception via ``raise ... from exc``; that exception is also exposed as
233
+ ``self.original`` for structured handling.
234
+ """
235
+
236
+ response: httpx2.Response
237
+ model: type
238
+ original: BaseException
239
+
240
+ def __init__(
241
+ self,
242
+ *,
243
+ response: httpx2.Response,
244
+ model: type,
245
+ original: BaseException,
246
+ ) -> None:
247
+ self.response = response
248
+ self.model = model
249
+ self.original = original
250
+ super().__init__(f"failed to decode response into {model.__name__}: {original}")
251
+
252
+ def __reduce__(self) -> tuple[Any, ...]:
253
+ return (
254
+ _reconstruct_decode_error,
255
+ (type(self), self.response, self.model, self.original),
256
+ )
File without changes