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.
- {httpware-0.15.2 → httpware-0.16.1}/PKG-INFO +3 -2
- {httpware-0.15.2 → httpware-0.16.1}/README.md +1 -1
- httpware-0.16.1/pyproject.toml +126 -0
- httpware-0.15.2/pyproject.toml → httpware-0.16.1/pyproject.toml.orig +17 -11
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/_internal/observability.py +0 -2
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/client.py +2 -1
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/decoders/_resolver.py +1 -1
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/_event_loop_guard.py +3 -3
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/budget.py +0 -2
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/bulkhead.py +0 -2
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/circuit_breaker.py +0 -2
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/retry.py +0 -2
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/__init__.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/_internal/__init__.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/_internal/body_cap.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/_internal/exception_mapping.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/_internal/import_checker.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/_internal/redaction.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/_internal/status.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/decoders/__init__.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/decoders/_caching.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/decoders/msgspec.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/decoders/pydantic.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/errors.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/__init__.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/chain.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/__init__.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/_backoff.py +0 -0
- {httpware-0.15.2 → httpware-0.16.1}/src/httpware/middleware/resilience/timeout.py +0 -0
- {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.
|
|
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.
|
|
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", #
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
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 = "
|
|
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)
|
|
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.
|
|
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
|
|
34
|
-
#
|
|
35
|
-
#
|
|
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
|
|
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
|