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.
Files changed (24) hide show
  1. {httpware-0.10.0 → httpware-0.10.1}/PKG-INFO +3 -3
  2. {httpware-0.10.0 → httpware-0.10.1}/README.md +2 -2
  3. {httpware-0.10.0 → httpware-0.10.1}/pyproject.toml +1 -1
  4. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/_internal/observability.py +11 -4
  5. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/resilience/circuit_breaker.py +4 -1
  6. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/resilience/timeout.py +3 -2
  7. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/__init__.py +0 -0
  8. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/_internal/__init__.py +0 -0
  9. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/_internal/exception_mapping.py +0 -0
  10. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/_internal/import_checker.py +0 -0
  11. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/_internal/status.py +0 -0
  12. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/client.py +0 -0
  13. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/decoders/__init__.py +0 -0
  14. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/decoders/msgspec.py +0 -0
  15. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/decoders/pydantic.py +0 -0
  16. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/errors.py +0 -0
  17. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/__init__.py +0 -0
  18. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/chain.py +0 -0
  19. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/resilience/__init__.py +0 -0
  20. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/resilience/_backoff.py +0 -0
  21. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/resilience/budget.py +0 -0
  22. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/resilience/bulkhead.py +0 -0
  23. {httpware-0.10.0 → httpware-0.10.1}/src/httpware/middleware/resilience/retry.py +0 -0
  24. {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.0
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 `BulkheadFullError`. Everything inherits `httpware.ClientError`.
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.WARNING)
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 `BulkheadFullError`. Everything inherits `httpware.ClientError`.
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.WARNING)
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
 
@@ -26,7 +26,7 @@ classifiers = [
26
26
  "Topic :: Internet :: WWW/HTTP",
27
27
  "Framework :: AsyncIO",
28
28
  ]
29
- version = "0.10.0"
29
+ version = "0.10.1"
30
30
  dependencies = [
31
31
  "httpx2>=2.0.0,<3.0",
32
32
  ]
@@ -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``) and event names
6
- (``retry.giving_up``, ``bulkhead.rejected``, etc.) are the public observability
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
- trace.get_current_span().add_event(event_name, attributes=attributes)
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