httpware 0.15.2__tar.gz → 0.16.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 (30) hide show
  1. {httpware-0.15.2 → httpware-0.16.1}/PKG-INFO +3 -2
  2. {httpware-0.15.2 → httpware-0.16.1}/README.md +1 -1
  3. httpware-0.16.1/pyproject.toml +126 -0
  4. httpware-0.15.2/pyproject.toml → httpware-0.16.1/pyproject.toml.orig +17 -11
  5. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/_internal/observability.py +0 -2
  6. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/client.py +2 -1
  7. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/decoders/_resolver.py +1 -1
  8. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/_event_loop_guard.py +3 -3
  9. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/budget.py +0 -2
  10. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/bulkhead.py +0 -2
  11. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/circuit_breaker.py +0 -2
  12. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/retry.py +0 -2
  13. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/__init__.py +0 -0
  14. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/_internal/__init__.py +0 -0
  15. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/_internal/body_cap.py +0 -0
  16. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/_internal/exception_mapping.py +0 -0
  17. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/_internal/import_checker.py +0 -0
  18. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/_internal/redaction.py +0 -0
  19. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/_internal/status.py +0 -0
  20. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/decoders/__init__.py +0 -0
  21. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/decoders/_caching.py +0 -0
  22. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/decoders/msgspec.py +0 -0
  23. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/decoders/pydantic.py +0 -0
  24. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/errors.py +0 -0
  25. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/__init__.py +0 -0
  26. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/chain.py +0 -0
  27. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/__init__.py +0 -0
  28. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/_backoff.py +0 -0
  29. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/timeout.py +0 -0
  30. {httpware-0.15.2 → httpware-0.16.1}/src/httpware/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpware
3
- Version: 0.15.2
3
+ Version: 0.16.1
4
4
  Summary: Python HTTP client framework with sync & async clients and built-in resilience
5
5
  Keywords: http-client,httpx,async,asyncio,resilience,retry,circuit-breaker,bulkhead,middleware,pydantic,msgspec,python
6
6
  Author: Artur Shiriev
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
15
16
  Classifier: Typing :: Typed
16
17
  Classifier: Topic :: Software Development :: Libraries
17
18
  Classifier: Topic :: Internet :: WWW/HTTP
@@ -119,7 +120,7 @@ Full guides live at **[httpware.modern-python.org](https://httpware.modern-pytho
119
120
  - **[Testing](https://httpware.modern-python.org/testing/)** — `httpx2.MockTransport` injection.
120
121
  - **[Recipes](https://httpware.modern-python.org/recipes/modern-di/)** — DI wiring, phase-decorator patterns, link-header pagination.
121
122
 
122
- ## 🗒️ [Release notes](https://github.com/modern-python/httpware/releases) · 📦 [PyPI](https://pypi.org/project/httpware) · 📝 [License](LICENSE)
123
+ ## 🗒️ [Release notes](https://github.com/modern-python/httpware/releases) · 📦 [PyPI](https://pypi.org/project/httpware) · 📝 [License](https://github.com/modern-python/httpware/blob/main/LICENSE)
123
124
 
124
125
  ## Part of `modern-python`
125
126
 
@@ -84,7 +84,7 @@ Full guides live at **[httpware.modern-python.org](https://httpware.modern-pytho
84
84
  - **[Testing](https://httpware.modern-python.org/testing/)** — `httpx2.MockTransport` injection.
85
85
  - **[Recipes](https://httpware.modern-python.org/recipes/modern-di/)** — DI wiring, phase-decorator patterns, link-header pagination.
86
86
 
87
- ## 🗒️ [Release notes](https://github.com/modern-python/httpware/releases) · 📦 [PyPI](https://pypi.org/project/httpware) · 📝 [License](LICENSE)
87
+ ## 🗒️ [Release notes](https://github.com/modern-python/httpware/releases) · 📦 [PyPI](https://pypi.org/project/httpware) · 📝 [License](https://github.com/modern-python/httpware/blob/main/LICENSE)
88
88
 
89
89
  ## Part of `modern-python`
90
90
 
@@ -0,0 +1,126 @@
1
+ [project]
2
+ name = "httpware"
3
+ description = "Python HTTP client framework with sync & async clients and built-in resilience"
4
+ requires-python = ">=3.11,<4"
5
+ license = "MIT"
6
+ readme = "README.md"
7
+ keywords = [
8
+ "http-client",
9
+ "httpx",
10
+ "async",
11
+ "asyncio",
12
+ "resilience",
13
+ "retry",
14
+ "circuit-breaker",
15
+ "bulkhead",
16
+ "middleware",
17
+ "pydantic",
18
+ "msgspec",
19
+ "python",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 4 - Beta",
23
+ "Intended Audience :: Developers",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Programming Language :: Python :: Free Threading :: 2 - Beta",
29
+ "Typing :: Typed",
30
+ "Topic :: Software Development :: Libraries",
31
+ "Topic :: Internet :: WWW/HTTP",
32
+ "Framework :: AsyncIO",
33
+ ]
34
+ version = "0.16.1"
35
+ dependencies = ["httpx2>=2.0.0,<3.0"]
36
+
37
+ [[project.authors]]
38
+ name = "Artur Shiriev"
39
+ email = "me@shiriev.ru"
40
+
41
+ [project.optional-dependencies]
42
+ pydantic = ["pydantic>=2.0,<3.0"]
43
+ msgspec = ["msgspec>=0.18"]
44
+ otel = ["opentelemetry-api>=1.20"]
45
+ all = ["httpware[pydantic,msgspec,otel]"]
46
+
47
+ [project.urls]
48
+ Homepage = "https://httpware.modern-python.org"
49
+ Documentation = "https://httpware.modern-python.org"
50
+ Repository = "https://github.com/modern-python/httpware"
51
+ Issues = "https://github.com/modern-python/httpware/issues"
52
+ Changelog = "https://github.com/modern-python/httpware/releases"
53
+
54
+ [build-system]
55
+ requires = ["uv_build>=0.11,<1.0"]
56
+ build-backend = "uv_build"
57
+
58
+ [tool.uv.build-backend]
59
+ module-name = "httpware"
60
+ module-root = "src"
61
+
62
+ [tool.ruff]
63
+ fix = true
64
+ unsafe-fixes = true
65
+ line-length = 120
66
+
67
+ [tool.ruff.lint]
68
+ select = ["ALL"]
69
+ ignore = [
70
+ "D1",
71
+ "D203",
72
+ "D213",
73
+ "COM812",
74
+ "ISC001",
75
+ "CPY001",
76
+ "FBT",
77
+ "TCH",
78
+ ]
79
+
80
+ [tool.ruff.lint.isort]
81
+ lines-after-imports = 2
82
+ no-lines-before = [
83
+ "standard-library",
84
+ "local-folder",
85
+ ]
86
+
87
+ [tool.ruff.lint.per-file-ignores]
88
+ "src/httpware/client.py" = [
89
+ "ASYNC109",
90
+ "ANN401",
91
+ ]
92
+ "tests/**" = ["S101"]
93
+
94
+ [tool.pytest.ini_options]
95
+ addopts = ""
96
+ asyncio_mode = "auto"
97
+ pythonpath = ["src"]
98
+ asyncio_default_fixture_loop_scope = "function"
99
+ markers = ["stress: concurrency stress test; proves thread-safety under free-threaded CPython (3.14t). Runs under the GIL too, so it counts toward coverage."]
100
+
101
+ [tool.coverage.run]
102
+ concurrency = ["thread"]
103
+ omit = ["benchmarks/*"]
104
+
105
+ [tool.coverage.report]
106
+ exclude_also = [
107
+ "if typing.TYPE_CHECKING:",
108
+ 'pytest\.fail\(',
109
+ ]
110
+ fail_under = 100
111
+
112
+ [dependency-groups]
113
+ dev = [
114
+ "pytest",
115
+ "pytest-cov",
116
+ "pytest-asyncio",
117
+ "pytest-repeat",
118
+ "pytest-benchmark",
119
+ "hypothesis",
120
+ ]
121
+ lint = [
122
+ "ruff",
123
+ "ty",
124
+ "eof-fixer",
125
+ "typing-extensions",
126
+ ]
@@ -26,12 +26,13 @@ classifiers = [
26
26
  "Programming Language :: Python :: 3.12",
27
27
  "Programming Language :: Python :: 3.13",
28
28
  "Programming Language :: Python :: 3.14",
29
+ "Programming Language :: Python :: Free Threading :: 2 - Beta",
29
30
  "Typing :: Typed",
30
31
  "Topic :: Software Development :: Libraries",
31
32
  "Topic :: Internet :: WWW/HTTP",
32
33
  "Framework :: AsyncIO",
33
34
  ]
34
- version = "0.15.2"
35
+ version = "0.16.1"
35
36
  dependencies = [
36
37
  "httpx2>=2.0.0,<3.0",
37
38
  ]
@@ -77,32 +78,37 @@ lint = [
77
78
  fix = true
78
79
  unsafe-fixes = true
79
80
  line-length = 120
80
- target-version = "py311"
81
81
 
82
82
  [tool.ruff.lint]
83
83
  select = ["ALL"]
84
84
  ignore = [
85
- "D1", # allow missing docstrings
86
- "S101", # allow asserts
87
- "TCH", # ignore flake8-type-checking
88
- "FBT", # allow boolean args
89
- "D203", # "one-blank-line-before-class" conflicting with D211
90
- "D213", # "multi-line-summary-second-line" conflicting with D212
91
- "COM812", # flake8-commas "Trailing comma missing"
92
- "ISC001", # flake8-implicit-str-concat
85
+ "D1", # docstrings are not forced; a docstring exists when it says something
86
+ "D203", # conflicts with D211
87
+ "D213", # conflicts with D212
88
+ "COM812", # conflicts with the formatter
89
+ "ISC001", # conflicts with the formatter
90
+ "CPY001", # no per-file copyright header
91
+ "FBT", # boolean positional arguments are fine
92
+ "TCH", # imports stay real; TYPE_CHECKING-only imports break runtime introspection
93
93
  ]
94
94
  isort.lines-after-imports = 2
95
95
  isort.no-lines-before = ["standard-library", "local-folder"]
96
96
 
97
97
  [tool.ruff.lint.per-file-ignores]
98
98
  "src/httpware/client.py" = ["ASYNC109", "ANN401"]
99
+ "tests/**" = ["S101"]
99
100
 
100
101
  [tool.pytest.ini_options]
101
- addopts = "--cov=. --cov-report term-missing --cov-fail-under=100"
102
+ addopts = ""
102
103
  asyncio_mode = "auto"
103
104
  pythonpath = ["src"]
104
105
  asyncio_default_fixture_loop_scope = "function"
106
+ markers = [
107
+ "stress: concurrency stress test; proves thread-safety under free-threaded CPython (3.14t). Runs under the GIL too, so it counts toward coverage.",
108
+ ]
105
109
 
106
110
  [tool.coverage]
107
111
  run.concurrency = ["thread"]
112
+ run.omit = ["benchmarks/*"]
108
113
  report.exclude_also = ["if typing.TYPE_CHECKING:", 'pytest\.fail\(']
114
+ report.fail_under = 100
@@ -1,7 +1,5 @@
1
1
  """Observability emission helper — structured logging + opt-in OpenTelemetry span events.
2
2
 
3
- See architecture/resilience.md (Observability section) for the contract.
4
-
5
3
  Logger names (``httpware.retry``, ``httpware.bulkhead``, ``httpware.circuit_breaker``,
6
4
  ``httpware.timeout``) and event names (``retry.giving_up``, ``bulkhead.rejected``,
7
5
  ``circuit.opened``, ``timeout.exceeded``, etc.) are the public observability
@@ -1035,7 +1035,8 @@ class AsyncClient:
1035
1035
  is closed when the context exits.
1036
1036
 
1037
1037
  Bypasses the middleware chain (no AsyncRetry, no AsyncBulkhead, no user-installed
1038
- middleware) see architecture/client.md for the contract.
1038
+ middleware): the protocol is typed on a fully-buffered response, and reading it
1039
+ would consume the stream.
1039
1040
 
1040
1041
  Auto-raises StatusError subclasses on 4xx/5xx (NotFoundError,
1041
1042
  ServiceUnavailableError, etc.) — consistent with client.get()/post()/etc.
@@ -8,7 +8,7 @@ client calls `resolve` before `_dispatch` — so a missing decoder fails before
8
8
  the request goes out — and `_BoundDecoder.decode` after the response arrives.
9
9
 
10
10
  Both clients hold one `_DecoderResolver`; it is fully synchronous, so there is
11
- no sync/async split. See architecture/decoders.md for the full Seam B contract.
11
+ no sync/async split.
12
12
  """
13
13
 
14
14
  from typing import Generic, TypeVar
@@ -30,8 +30,8 @@ def check_event_loop(
30
30
  cached = get_loop()
31
31
  if cached is None:
32
32
  set_loop(current)
33
- # pragma below: inner double-check-with-lock race arm; only reachable when
34
- # two threads simultaneously pass the outer check, which single-threaded
35
- # tests can't trigger.
33
+ # pragma below: inner double-check-with-lock race arm. Reachable only when two
34
+ # threads pass the outer check and race for loop_lock — free-threaded CPython can
35
+ # reach it, but only nondeterministically, so it stays excluded from coverage.
36
36
  elif cached is not current: # pragma: no cover
37
37
  raise RuntimeError(message_template.format(first=cached, current=current))
@@ -1,7 +1,5 @@
1
1
  """Finagle-style token-bucket retry budget.
2
2
 
3
- See architecture/resilience.md (Retry + RetryBudget section) for the contract.
4
-
5
3
  Thread-safe and asyncio-safe: all mutations go through a threading.Lock,
6
4
  which ensures no torn state across concurrent accesses. When a RetryBudget
7
5
  is shared between a sync Client (pool thread) and an AsyncClient (event-loop
@@ -1,7 +1,5 @@
1
1
  """AsyncBulkhead middleware — concurrency limiter via asyncio.Semaphore.
2
2
 
3
- See architecture/resilience.md (Bulkhead section) for the contract.
4
-
5
3
  The middleware owns an asyncio.Semaphore(max_concurrent). On each request,
6
4
  it acquires a slot (bounded by acquire_timeout via asyncio.timeout) and
7
5
  releases the slot in a try/finally so success, exceptions, and cancellation
@@ -1,7 +1,5 @@
1
1
  """CircuitBreaker + AsyncCircuitBreaker — consecutive-failure and failure-rate circuit breakers.
2
2
 
3
- See architecture/resilience.md (CircuitBreaker + AsyncTimeout section) for the contract.
4
-
5
3
  A counted failure is a NetworkError, an httpware TimeoutError, or a StatusError whose
6
4
  status_code is in the effective failure set (default: all 5xx). 4xx — including 429 —
7
5
  count as successes: 429 means healthy-but-throttling, and tripping on it amplifies
@@ -1,7 +1,5 @@
1
1
  """AsyncRetry + Retry middleware — automatic retry of transient failures with budget control.
2
2
 
3
- See architecture/resilience.md (Retry + RetryBudget section) for the full contract.
4
-
5
3
  Status-code retry: the client terminal raises StatusError subclasses on 4xx/5xx,
6
4
  so the retry middleware catches StatusError and inspects exc.response.status_code. The
7
5
  original StatusError subclass is re-raised unwrapped on exhaustion, with a PEP 678 note