httpware 0.10.0__tar.gz → 0.10.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.
- {httpware-0.10.0 → httpware-0.10.1}/PKG-INFO +3 -3
- {httpware-0.10.0 → httpware-0.10.1}/README.md +2 -2
- {httpware-0.10.0 → httpware-0.10.1}/pyproject.toml +1 -1
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/_internal/observability.py +11 -4
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/resilience/circuit_breaker.py +4 -1
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/resilience/timeout.py +3 -2
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/__init__.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/_internal/__init__.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/_internal/exception_mapping.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/_internal/import_checker.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/_internal/status.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/client.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/decoders/__init__.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/decoders/msgspec.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/decoders/pydantic.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/errors.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/__init__.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/chain.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/resilience/__init__.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/resilience/_backoff.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/resilience/budget.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/resilience/bulkhead.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/resilience/retry.py +0 -0
- {httpware-0.10.0 → httpware-0.10.1}/src/httpware/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: httpware
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.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
|
|
@@ -142,7 +142,7 @@ It does NOT pass through the middleware chain: `AsyncRetry`, `AsyncBulkhead`, an
|
|
|
142
142
|
|
|
143
143
|
## Errors
|
|
144
144
|
|
|
145
|
-
All 4xx/5xx responses raise typed exceptions automatically: `NotFoundError`, `ServiceUnavailableError`, `RateLimitedError`, etc. — all subclasses of `httpware.StatusError`. Transport-layer transient failures raise `NetworkError`; the resilience middleware raise `RetryBudgetExhaustedError` and `
|
|
145
|
+
All 4xx/5xx responses raise typed exceptions automatically: `NotFoundError`, `ServiceUnavailableError`, `RateLimitedError`, etc. — all subclasses of `httpware.StatusError`. Transport-layer transient failures raise `NetworkError`; the resilience middleware raise `RetryBudgetExhaustedError`, `BulkheadFullError`, and `CircuitOpenError`. Everything inherits `httpware.ClientError`.
|
|
146
146
|
|
|
147
147
|
## Observability
|
|
148
148
|
|
|
@@ -156,7 +156,7 @@ import logging
|
|
|
156
156
|
# Enable visibility into resilience operational events
|
|
157
157
|
logging.getLogger("httpware.retry").setLevel(logging.WARNING)
|
|
158
158
|
logging.getLogger("httpware.bulkhead").setLevel(logging.WARNING)
|
|
159
|
-
logging.getLogger("httpware.circuit_breaker").setLevel(logging.
|
|
159
|
+
logging.getLogger("httpware.circuit_breaker").setLevel(logging.INFO) # INFO: includes recovery events (half_open, closed)
|
|
160
160
|
logging.getLogger("httpware.timeout").setLevel(logging.WARNING)
|
|
161
161
|
```
|
|
162
162
|
|
|
@@ -112,7 +112,7 @@ It does NOT pass through the middleware chain: `AsyncRetry`, `AsyncBulkhead`, an
|
|
|
112
112
|
|
|
113
113
|
## Errors
|
|
114
114
|
|
|
115
|
-
All 4xx/5xx responses raise typed exceptions automatically: `NotFoundError`, `ServiceUnavailableError`, `RateLimitedError`, etc. — all subclasses of `httpware.StatusError`. Transport-layer transient failures raise `NetworkError`; the resilience middleware raise `RetryBudgetExhaustedError` and `
|
|
115
|
+
All 4xx/5xx responses raise typed exceptions automatically: `NotFoundError`, `ServiceUnavailableError`, `RateLimitedError`, etc. — all subclasses of `httpware.StatusError`. Transport-layer transient failures raise `NetworkError`; the resilience middleware raise `RetryBudgetExhaustedError`, `BulkheadFullError`, and `CircuitOpenError`. Everything inherits `httpware.ClientError`.
|
|
116
116
|
|
|
117
117
|
## Observability
|
|
118
118
|
|
|
@@ -126,7 +126,7 @@ import logging
|
|
|
126
126
|
# Enable visibility into resilience operational events
|
|
127
127
|
logging.getLogger("httpware.retry").setLevel(logging.WARNING)
|
|
128
128
|
logging.getLogger("httpware.bulkhead").setLevel(logging.WARNING)
|
|
129
|
-
logging.getLogger("httpware.circuit_breaker").setLevel(logging.
|
|
129
|
+
logging.getLogger("httpware.circuit_breaker").setLevel(logging.INFO) # INFO: includes recovery events (half_open, closed)
|
|
130
130
|
logging.getLogger("httpware.timeout").setLevel(logging.WARNING)
|
|
131
131
|
```
|
|
132
132
|
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
See planning/specs/2026-06-05-observability-design.md for the contract.
|
|
4
4
|
|
|
5
|
-
Logger names (``httpware.retry``, ``httpware.bulkhead``
|
|
6
|
-
(``retry.giving_up``, ``bulkhead.rejected``,
|
|
5
|
+
Logger names (``httpware.retry``, ``httpware.bulkhead``, ``httpware.circuit_breaker``,
|
|
6
|
+
``httpware.timeout``) and event names (``retry.giving_up``, ``bulkhead.rejected``,
|
|
7
|
+
``circuit.opened``, ``timeout.exceeded``, etc.) are the public observability
|
|
7
8
|
surface. They are stable: renames are breaking changes.
|
|
8
9
|
"""
|
|
9
10
|
|
|
11
|
+
import contextlib
|
|
10
12
|
import logging
|
|
11
13
|
import typing
|
|
12
14
|
|
|
@@ -37,7 +39,7 @@ def _emit_event(
|
|
|
37
39
|
the optional-extras isolation invariant: ``import httpware`` must not pull
|
|
38
40
|
``opentelemetry`` into ``sys.modules`` when the extra is absent.
|
|
39
41
|
"""
|
|
40
|
-
logger.log(level, message, extra=attributes)
|
|
42
|
+
logger.log(level, message, extra={**attributes, "event": event_name})
|
|
41
43
|
if import_checker.is_otel_installed:
|
|
42
44
|
try:
|
|
43
45
|
from opentelemetry import trace # noqa: PLC0415 — lazy by design (optional-extras isolation)
|
|
@@ -45,4 +47,9 @@ def _emit_event(
|
|
|
45
47
|
# opentelemetry namespace exists but the api package is broken or missing —
|
|
46
48
|
# degrade to log-only emission. The structured log record above has already fired.
|
|
47
49
|
return
|
|
48
|
-
|
|
50
|
+
# Observability must never break the request path — suppress any failure from
|
|
51
|
+
# add_event (e.g. a recording span with a broken exporter or attribute validation).
|
|
52
|
+
# The structured log record above has already fired; CancelledError/KeyboardInterrupt
|
|
53
|
+
# are not Exception subclasses and will still propagate.
|
|
54
|
+
with contextlib.suppress(Exception):
|
|
55
|
+
trace.get_current_span().add_event(event_name, attributes=attributes)
|
|
@@ -5,7 +5,10 @@ See planning/specs/2026-06-13-circuit-breaker-and-timeout-design.md for the cont
|
|
|
5
5
|
A counted failure is a NetworkError, an httpware TimeoutError, or a StatusError whose
|
|
6
6
|
status_code is in the effective failure set (default: all 5xx). 4xx — including 429 —
|
|
7
7
|
count as successes: 429 means healthy-but-throttling, and tripping on it amplifies
|
|
8
|
-
incidents. Any other exception propagates without affecting circuit state.
|
|
8
|
+
incidents. Any other exception propagates without affecting circuit state. In
|
|
9
|
+
particular, non-NetworkError transport problems — e.g. httpx2.InvalidURL from a
|
|
10
|
+
malformed URL — are foreign: they propagate unchanged and do not increment the
|
|
11
|
+
failure counter, so programming errors cannot trip the breaker.
|
|
9
12
|
|
|
10
13
|
State machine (classic / consecutive-failure):
|
|
11
14
|
CLOSED — forward; count consecutive counted-failures; open at failure_threshold.
|
|
@@ -14,6 +14,7 @@ timeouts. Sync callers configure httpx2's timeouts directly; there is no sync Ti
|
|
|
14
14
|
|
|
15
15
|
import asyncio
|
|
16
16
|
import logging
|
|
17
|
+
import math
|
|
17
18
|
|
|
18
19
|
import httpx2
|
|
19
20
|
|
|
@@ -22,7 +23,7 @@ from httpware.errors import TimeoutError as HttpwareTimeoutError
|
|
|
22
23
|
from httpware.middleware import AsyncNext
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
_TIMEOUT_INVALID = "timeout must be > 0"
|
|
26
|
+
_TIMEOUT_INVALID = "timeout must be a finite number > 0"
|
|
26
27
|
|
|
27
28
|
_LOGGER = logging.getLogger("httpware.timeout")
|
|
28
29
|
|
|
@@ -43,7 +44,7 @@ class AsyncTimeout:
|
|
|
43
44
|
"""
|
|
44
45
|
|
|
45
46
|
def __init__(self, *, timeout: float) -> None:
|
|
46
|
-
if timeout <= 0:
|
|
47
|
+
if not math.isfinite(timeout) or timeout <= 0:
|
|
47
48
|
raise ValueError(_TIMEOUT_INVALID)
|
|
48
49
|
self._timeout = timeout
|
|
49
50
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|