clientwright 0.1.0__py3-none-any.whl

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 (102) hide show
  1. clientwright/__init__.py +179 -0
  2. clientwright/__version__.py +1 -0
  3. clientwright/adapters/__init__.py +3 -0
  4. clientwright/adapters/_httpx_shared.py +831 -0
  5. clientwright/adapters/_lazy.py +30 -0
  6. clientwright/adapters/aiohttp/__init__.py +45 -0
  7. clientwright/adapters/aiohttp/_imports.py +30 -0
  8. clientwright/adapters/aiohttp/adapter.py +236 -0
  9. clientwright/adapters/aiohttp/capabilities.py +81 -0
  10. clientwright/adapters/aiohttp/classify.py +59 -0
  11. clientwright/adapters/aiohttp/errors.py +57 -0
  12. clientwright/adapters/aiohttp/middleware.py +103 -0
  13. clientwright/adapters/aiohttp/normalize.py +64 -0
  14. clientwright/adapters/aiohttp/options.py +16 -0
  15. clientwright/adapters/aiohttp/trace.py +109 -0
  16. clientwright/adapters/aiohttp/views.py +108 -0
  17. clientwright/adapters/httpx/__init__.py +45 -0
  18. clientwright/adapters/httpx/_imports.py +17 -0
  19. clientwright/adapters/httpx/adapter.py +39 -0
  20. clientwright/adapters/httpx/capabilities.py +9 -0
  21. clientwright/adapters/httpx/classify.py +14 -0
  22. clientwright/adapters/httpx/errors.py +35 -0
  23. clientwright/adapters/httpx/normalize.py +27 -0
  24. clientwright/adapters/httpx/normalize_sync.py +27 -0
  25. clientwright/adapters/httpx/transport.py +40 -0
  26. clientwright/adapters/httpx/views.py +46 -0
  27. clientwright/adapters/httpx2/__init__.py +46 -0
  28. clientwright/adapters/httpx2/_imports.py +18 -0
  29. clientwright/adapters/httpx2/adapter.py +37 -0
  30. clientwright/adapters/httpx2/capabilities.py +9 -0
  31. clientwright/adapters/httpx2/classify.py +14 -0
  32. clientwright/adapters/httpx2/errors.py +36 -0
  33. clientwright/adapters/httpx2/normalize.py +27 -0
  34. clientwright/adapters/httpx2/normalize_sync.py +27 -0
  35. clientwright/adapters/httpx2/transport.py +35 -0
  36. clientwright/adapters/httpx2/views.py +45 -0
  37. clientwright/adapters/observability/__init__.py +26 -0
  38. clientwright/adapters/observability/_metrics/__init__.py +1 -0
  39. clientwright/adapters/observability/_metrics/prometheus.py +200 -0
  40. clientwright/adapters/observability/_tracing/__init__.py +3 -0
  41. clientwright/adapters/observability/_tracing/otel.py +61 -0
  42. clientwright/adapters/requests/__init__.py +45 -0
  43. clientwright/adapters/requests/_imports.py +21 -0
  44. clientwright/adapters/requests/adapter.py +206 -0
  45. clientwright/adapters/requests/capabilities.py +80 -0
  46. clientwright/adapters/requests/classify.py +62 -0
  47. clientwright/adapters/requests/errors.py +40 -0
  48. clientwright/adapters/requests/normalize.py +63 -0
  49. clientwright/adapters/requests/views.py +121 -0
  50. clientwright/adapters/urllib3/__init__.py +48 -0
  51. clientwright/adapters/urllib3/_imports.py +18 -0
  52. clientwright/adapters/urllib3/adapter.py +260 -0
  53. clientwright/adapters/urllib3/capabilities.py +86 -0
  54. clientwright/adapters/urllib3/classify.py +46 -0
  55. clientwright/adapters/urllib3/errors.py +55 -0
  56. clientwright/adapters/urllib3/normalize.py +51 -0
  57. clientwright/adapters/urllib3/views.py +119 -0
  58. clientwright/contrib/__init__.py +3 -0
  59. clientwright/contrib/deadline.py +107 -0
  60. clientwright/contrib/dishka.py +80 -0
  61. clientwright/core/__init__.py +6 -0
  62. clientwright/core/balancer/__init__.py +1 -0
  63. clientwright/core/balancer/policy.py +23 -0
  64. clientwright/core/capabilities.py +156 -0
  65. clientwright/core/config.py +367 -0
  66. clientwright/core/contracts/__init__.py +33 -0
  67. clientwright/core/contracts/adapter.py +62 -0
  68. clientwright/core/contracts/context.py +31 -0
  69. clientwright/core/contracts/message.py +118 -0
  70. clientwright/core/contracts/observability.py +92 -0
  71. clientwright/core/contracts/settings.py +140 -0
  72. clientwright/core/engine/__init__.py +1 -0
  73. clientwright/core/engine/aio.py +246 -0
  74. clientwright/core/engine/base.py +65 -0
  75. clientwright/core/engine/redirects.py +64 -0
  76. clientwright/core/engine/suppress.py +30 -0
  77. clientwright/core/engine/sync.py +241 -0
  78. clientwright/core/errors.py +113 -0
  79. clientwright/core/model.py +138 -0
  80. clientwright/core/native.py +84 -0
  81. clientwright/core/options.py +46 -0
  82. clientwright/core/plan.py +190 -0
  83. clientwright/core/policy/__init__.py +1 -0
  84. clientwright/core/policy/budget.py +76 -0
  85. clientwright/core/policy/circuit.py +169 -0
  86. clientwright/core/policy/concurrency.py +98 -0
  87. clientwright/core/policy/retry.py +80 -0
  88. clientwright/core/policy/timeout.py +64 -0
  89. clientwright/core/registry.py +75 -0
  90. clientwright/core/telemetry/__init__.py +6 -0
  91. clientwright/core/telemetry/emitter.py +163 -0
  92. clientwright/core/telemetry/names.py +61 -0
  93. clientwright/core/telemetry/null.py +89 -0
  94. clientwright/core/telemetry/redaction.py +24 -0
  95. clientwright/core/testing/__init__.py +7 -0
  96. clientwright/core/testing/doubles.py +107 -0
  97. clientwright/core/testing/origin.py +201 -0
  98. clientwright/py.typed +0 -0
  99. clientwright-0.1.0.dist-info/METADATA +210 -0
  100. clientwright-0.1.0.dist-info/RECORD +102 -0
  101. clientwright-0.1.0.dist-info/WHEEL +4 -0
  102. clientwright-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,201 @@
1
+ """Fault-injecting HTTP origin on the standard library, for conformance tests.
2
+
3
+ Routes:
4
+ - ``/echo`` 200 with a small JSON body (method, path, headers echo)
5
+ - ``/status/{code}`` responds with that status
6
+ - ``/slow/{seconds}`` sleeps, then 200
7
+ - ``/redirect/{n}`` 302 chain of n hops ending at /echo
8
+ - ``/redirect-loop`` 302 to itself forever
9
+ - ``/flaky/{key}/{fails}`` first {fails} requests per key answer 503, then 200
10
+ - ``/retry-after/{seconds}`` 503 with a Retry-After header
11
+ - ``/disconnect`` closes the connection without a response
12
+
13
+ Chaos routes (mid-stream and protocol-level faults):
14
+ - ``/hang-body/{seconds}`` 200 announcing 10 bytes: 3 arrive, the rest after the stall
15
+ - ``/drop-body`` 200 announcing 10 bytes but the connection dies after 3
16
+ - ``/garbage`` raw non-HTTP bytes instead of a status line
17
+ - ``/reset`` hard TCP reset (SO_LINGER 0) instead of a response
18
+ - ``/flaky-disconnect/{key}/{fails}`` first {fails} requests per key drop the connection, then 200
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import socket
25
+ import struct
26
+ import threading
27
+ import time
28
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
29
+ from types import TracebackType
30
+
31
+
32
+ class _Handler(BaseHTTPRequestHandler):
33
+ protocol_version = "HTTP/1.1"
34
+ server: OriginServer # type: ignore[assignment]
35
+
36
+ def log_message(self, format: str, *args: object) -> None:
37
+ return None
38
+
39
+ def _reply(self, status: int, body: bytes = b"", headers: dict[str, str] | None = None) -> None:
40
+ self.send_response(status)
41
+ for name, value in (headers or {}).items():
42
+ self.send_header(name, value)
43
+ self.send_header("Content-Length", str(len(body)))
44
+ self.end_headers()
45
+ if body and self.command != "HEAD":
46
+ self.wfile.write(body)
47
+
48
+ def _handle(self) -> None:
49
+ server = self.server
50
+ with server.lock:
51
+ server.requests.append((self.command, self.path))
52
+ # ALWAYS drain the request body: on keep-alive reuse, unread body bytes
53
+ # would be parsed as the start of the next request line.
54
+ length = int(self.headers.get("Content-Length") or 0)
55
+ request_body = self.rfile.read(length) if length else b""
56
+ parts = [part for part in self.path.split("?")[0].split("/") if part]
57
+ if not parts or parts[0] == "echo":
58
+ payload = {
59
+ "method": self.command,
60
+ "path": self.path,
61
+ "headers": {name.lower(): value for name, value in self.headers.items()},
62
+ "body": request_body.decode("utf-8", errors="replace"),
63
+ }
64
+ self._reply(200, json.dumps(payload).encode(), {"Content-Type": "application/json"})
65
+ return
66
+ if parts[0] == "status":
67
+ self._reply(int(parts[1]))
68
+ return
69
+ if parts[0] == "slow":
70
+ time.sleep(float(parts[1]))
71
+ self._reply(200, b"slow")
72
+ return
73
+ if parts[0] == "redirect":
74
+ hops = int(parts[1])
75
+ target = "/echo" if hops <= 1 else f"/redirect/{hops - 1}"
76
+ self._reply(302, headers={"Location": target})
77
+ return
78
+ if parts[0] == "redirect-loop":
79
+ self._reply(302, headers={"Location": "/redirect-loop"})
80
+ return
81
+ if parts[0] == "flaky":
82
+ key, fails = parts[1], int(parts[2])
83
+ with server.lock:
84
+ seen = server.flaky_counters.get(key, 0)
85
+ server.flaky_counters[key] = seen + 1
86
+ if seen < fails:
87
+ self._reply(503, b"flaky")
88
+ else:
89
+ self._reply(200, b"recovered")
90
+ return
91
+ if parts[0] == "retry-after":
92
+ self._reply(503, b"", {"Retry-After": parts[1]})
93
+ return
94
+ if parts[0] == "disconnect":
95
+ self.close_connection = True
96
+ self.connection.close()
97
+ return
98
+ if parts[0] == "hang-body":
99
+ body = b"0123456789"
100
+ self.send_response(200)
101
+ self.send_header("Content-Length", str(len(body)))
102
+ self.end_headers()
103
+ self.wfile.write(body[:3])
104
+ self.wfile.flush()
105
+ time.sleep(float(parts[1]))
106
+ try:
107
+ self.wfile.write(body[3:])
108
+ except OSError: # the client gave up mid-stall; routine for this route
109
+ self.close_connection = True
110
+ return
111
+ if parts[0] == "drop-body":
112
+ self.send_response(200)
113
+ self.send_header("Content-Length", "10")
114
+ self.end_headers()
115
+ self.wfile.write(b"abc")
116
+ self.wfile.flush()
117
+ self.close_connection = True
118
+ self.connection.close()
119
+ return
120
+ if parts[0] == "garbage":
121
+ self.close_connection = True
122
+ self.wfile.write(b"\x16\x03\x01 banana instead of a status line\r\n\r\n")
123
+ self.wfile.flush()
124
+ self.connection.close()
125
+ return
126
+ if parts[0] == "reset":
127
+ self.close_connection = True
128
+ # SO_LINGER with a zero timeout turns close() into a TCP RST.
129
+ self.connection.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
130
+ self.connection.close()
131
+ return
132
+ if parts[0] == "flaky-disconnect":
133
+ key, fails = parts[1], int(parts[2])
134
+ with server.lock:
135
+ seen = server.flaky_counters.get(key, 0)
136
+ server.flaky_counters[key] = seen + 1
137
+ if seen < fails:
138
+ self.close_connection = True
139
+ self.connection.close()
140
+ return
141
+ self._reply(200, b"recovered")
142
+ return
143
+ self._reply(404)
144
+
145
+ def do_GET(self) -> None:
146
+ self._handle()
147
+
148
+ def do_POST(self) -> None:
149
+ self._handle()
150
+
151
+ def do_PUT(self) -> None:
152
+ self._handle()
153
+
154
+ def do_HEAD(self) -> None:
155
+ self._handle()
156
+
157
+ def do_DELETE(self) -> None:
158
+ self._handle()
159
+
160
+
161
+ class OriginServer(ThreadingHTTPServer):
162
+ """Context-managed origin bound to an ephemeral localhost port."""
163
+
164
+ daemon_threads = True
165
+
166
+ def handle_error(self, request: object, client_address: object) -> None:
167
+ """Silence per-connection tracebacks: aborted clients are the point here."""
168
+ return None
169
+
170
+ def __init__(self) -> None:
171
+ super().__init__(("127.0.0.1", 0), _Handler)
172
+ self.lock = threading.Lock()
173
+ self.requests: list[tuple[str, str]] = []
174
+ self.flaky_counters: dict[str, int] = {}
175
+ self._thread = threading.Thread(target=self.serve_forever, daemon=True)
176
+
177
+ @property
178
+ def url(self) -> str:
179
+ raw_host, port = self.server_address[0], self.server_address[1]
180
+ host = raw_host.decode() if isinstance(raw_host, bytes) else raw_host
181
+ return f"http://{host}:{port}"
182
+
183
+ def request_count(self, path_prefix: str) -> int:
184
+ with self.lock:
185
+ return sum(1 for _, path in self.requests if path.startswith(path_prefix))
186
+
187
+ def __enter__(self) -> OriginServer:
188
+ self._thread.start()
189
+ return self
190
+
191
+ def __exit__(
192
+ self,
193
+ exc_type: type[BaseException] | None,
194
+ exc: BaseException | None,
195
+ tb: TracebackType | None,
196
+ ) -> None:
197
+ self.shutdown()
198
+ self.server_close()
199
+
200
+
201
+ __all__ = ["OriginServer"]
clientwright/py.typed ADDED
File without changes
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.5
2
+ Name: clientwright
3
+ Version: 0.1.0
4
+ Summary: HTTP client platform: one resilience and observability core with adapters for httpx, aiohttp, requests, urllib3 and friends
5
+ Project-URL: Repository, https://github.com/bedrock-python/clientwright
6
+ Project-URL: Documentation, https://bedrock-python.github.io/clientwright/
7
+ Project-URL: Changelog, https://github.com/bedrock-python/clientwright/blob/master/CHANGELOG.md
8
+ Project-URL: Bug Tracker, https://github.com/bedrock-python/clientwright/issues
9
+ Author-email: Alex Shalaev <shalaevad.alexey@gmail.com>
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: aiohttp,circuit-breaker,deadline,http,http-client,httpx,observability,opentelemetry,prometheus,requests,resilience,retry,timeout,urllib3
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Framework :: AsyncIO
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Internet :: WWW/HTTP
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: System :: Monitoring
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.12
25
+ Provides-Extra: aiohttp
26
+ Requires-Dist: aiohttp<4,>=3.12; extra == 'aiohttp'
27
+ Provides-Extra: all
28
+ Requires-Dist: aiohttp<4,>=3.12; extra == 'all'
29
+ Requires-Dist: deadline-budget<1,>=0.1; extra == 'all'
30
+ Requires-Dist: dishka>=1.4; extra == 'all'
31
+ Requires-Dist: httpx2<3,>=2; extra == 'all'
32
+ Requires-Dist: httpx<1,>=0.28; extra == 'all'
33
+ Requires-Dist: opentelemetry-api>=1.26; extra == 'all'
34
+ Requires-Dist: prometheus-client>=0.20; extra == 'all'
35
+ Requires-Dist: requests<3,>=2.32; extra == 'all'
36
+ Requires-Dist: urllib3<3,>=2.2; extra == 'all'
37
+ Provides-Extra: deadline
38
+ Requires-Dist: deadline-budget<1,>=0.1; extra == 'deadline'
39
+ Provides-Extra: dishka
40
+ Requires-Dist: dishka>=1.4; extra == 'dishka'
41
+ Provides-Extra: httpx
42
+ Requires-Dist: httpx<1,>=0.28; extra == 'httpx'
43
+ Provides-Extra: httpx2
44
+ Requires-Dist: httpx2<3,>=2; extra == 'httpx2'
45
+ Provides-Extra: metrics
46
+ Requires-Dist: prometheus-client>=0.20; extra == 'metrics'
47
+ Provides-Extra: observability
48
+ Requires-Dist: opentelemetry-api>=1.26; extra == 'observability'
49
+ Requires-Dist: prometheus-client>=0.20; extra == 'observability'
50
+ Provides-Extra: requests
51
+ Requires-Dist: requests<3,>=2.32; extra == 'requests'
52
+ Provides-Extra: tracing
53
+ Requires-Dist: opentelemetry-api>=1.26; extra == 'tracing'
54
+ Provides-Extra: urllib3
55
+ Requires-Dist: urllib3<3,>=2.2; extra == 'urllib3'
56
+ Description-Content-Type: text/markdown
57
+
58
+ # clientwright
59
+
60
+ [![PyPI](https://img.shields.io/pypi/v/clientwright?color=blue)](https://pypi.org/project/clientwright/)
61
+ [![Python](https://img.shields.io/pypi/pyversions/clientwright)](https://pypi.org/project/clientwright/)
62
+ [![License](https://img.shields.io/github/license/bedrock-python/clientwright)](LICENSE)
63
+ [![CI](https://github.com/bedrock-python/clientwright/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/bedrock-python/clientwright/actions/workflows/ci.yml)
64
+ [![codecov](https://codecov.io/gh/bedrock-python/clientwright/graph/badge.svg)](https://codecov.io/gh/bedrock-python/clientwright)
65
+ [![Docs](https://img.shields.io/badge/docs-online-blue)](https://bedrock-python.github.io/clientwright/)
66
+
67
+ One resilience and observability core, many HTTP clients.
68
+
69
+ **Documentation: <https://bedrock-python.github.io/clientwright/>** — a guided
70
+ tour from first client to writing your own adapter; this README is the elevator
71
+ version.
72
+
73
+ `clientwright` is to HTTP clients what [servicewright](https://github.com/bedrock-python/servicewright)
74
+ is to services: a zero-dependency kernel of policies (retries, circuit breaking, deadlines,
75
+ owned redirects, telemetry) plus adapters that wire it UNDER the public API of popular HTTP
76
+ libraries. You get back the **real native client** - a genuine `httpx.AsyncClient`, not a
77
+ wrapper - with the whole machinery already working inside it.
78
+
79
+ ```python
80
+ import httpx
81
+ from clientwright import ClientConfig, TimeoutConfig, build, inspect
82
+
83
+ config = ClientConfig(
84
+ service_name="google-oauth",
85
+ base_url="https://oauth2.googleapis.com",
86
+ timeout=TimeoutConfig(total=10.0, connect=2.0),
87
+ )
88
+ client: httpx.AsyncClient = build("httpx", config)
89
+ assert type(client) is httpx.AsyncClient # not a subclass, not a wrapper
90
+
91
+ response = await client.post("/token", data={...}) # retries, breaker, deadline,
92
+ # metrics and spans already apply
93
+
94
+ handle = inspect(client) # what actually got applied
95
+ print(handle.report.dropped) # config the adapter could not express
96
+ ```
97
+
98
+ ## Why
99
+
100
+ - **Instrumentation under the public API.** The seam sits in the transport
101
+ (httpx), not above the client - taking the raw native client never loses
102
+ logging, metrics, retries or the circuit breaker.
103
+ - **One engine, not N reimplementations.** A single attempt loop owns retries,
104
+ redirects, deadlines and telemetry for every adapter; adapters only translate
105
+ messages and send.
106
+ - **Capability-honest.** Adapters declare what they can and cannot express
107
+ (`AdapterCapabilities`); anything you configured that cannot be honored lands
108
+ in a `ConfigApplicationReport` - and fails the build under
109
+ `on_unsupported="strict"` instead of silently lying.
110
+ - **Owned redirects.** The engine follows redirects itself, so one logical call
111
+ has ONE deadline, ONE retry budget and ONE breaker signal regardless of hops.
112
+ - **Total deadline everywhere.** httpx has no wall-clock timeout; clientwright
113
+ enforces one across attempts, backoff sleeps and redirect hops (hard
114
+ cancellation on async, phase clamping on sync).
115
+ - **Frozen telemetry schema.** `http_client_*` metric families with mandatory
116
+ `adapter`/`seam` labels - backend divergence is observable on a dashboard,
117
+ not buried in a README.
118
+ - **Batteries optional.** `pip install clientwright` has zero dependencies;
119
+ adapters and observability backends are extras with lazy import guards.
120
+
121
+ ## Install
122
+
123
+ ```bash
124
+ pip install clientwright[httpx] # httpx adapter (sync + async)
125
+ pip install clientwright[aiohttp] # aiohttp adapter (async)
126
+ pip install clientwright[httpx,metrics] # + Prometheus backend
127
+ pip install clientwright[httpx,tracing] # + OpenTelemetry backend
128
+ ```
129
+
130
+ ## Sync twin
131
+
132
+ ```python
133
+ from clientwright import build_sync
134
+
135
+ client = build_sync("httpx", config) # a genuine httpx.Client
136
+ ```
137
+
138
+ The kernel's policies are pure synchronous functions; the same retry decision
139
+ code drives both the async and the sync engine.
140
+
141
+ ## Retry and idempotency
142
+
143
+ Per-call channel via httpx `extensions`:
144
+
145
+ ```python
146
+ from clientwright.adapters.httpx import IDEMPOTENT_EXTENSION, ROUTE_EXTENSION
147
+
148
+ await client.post(
149
+ "/orders",
150
+ json=payload,
151
+ extensions={
152
+ ROUTE_EXTENSION: "/orders", # low-cardinality metric/breaker label
153
+ IDEMPOTENT_EXTENSION: True, # this POST is safe to retry
154
+ },
155
+ )
156
+ ```
157
+
158
+ aiohttp has no request extensions; the same channel is a context manager:
159
+
160
+ ```python
161
+ from clientwright.adapters.aiohttp import call_options
162
+
163
+ with call_options(route="/orders", idempotent=True):
164
+ await session.post("/orders", json=payload)
165
+ ```
166
+
167
+ Retries respect `Retry-After`, an origin-wide token-bucket budget (max ~10%
168
+ retry traffic by default), body replayability and the remaining deadline.
169
+
170
+ ## DI scope
171
+
172
+ `ClientRuntime` (circuits, retry budgets, per-origin limiters) must live in
173
+ **APP scope** and be shared across REQUEST-scoped clients via
174
+ `AdapterDeps(runtime=...)` - otherwise breaker state dies with every request.
175
+ With `clientwright[dishka]`, `contrib.dishka.ClientwrightProvider` does both:
176
+ APP-scope runtime and a generator provide that closes the client in `finally`.
177
+
178
+ ## Deadline budgets
179
+
180
+ With `clientwright[deadline]`, the remaining budget of the request being
181
+ served caps every outgoing call:
182
+
183
+ ```python
184
+ from deadline_budget import BudgetContext
185
+
186
+ from clientwright import AdapterDeps, build
187
+ from clientwright.contrib.deadline import AmbientDeadlineSource, use_budget
188
+
189
+ deps = AdapterDeps(deadline_source=AmbientDeadlineSource())
190
+ client = build("httpx", config, deps)
191
+
192
+ with use_budget(BudgetContext.create(total_seconds=5.0)):
193
+ await client.get("/users") # runs with what is left of those 5 seconds
194
+ ```
195
+
196
+ ## Adapters
197
+
198
+ | adapter | modes | notes |
199
+ |---|---|---|
200
+ | `httpx` | sync + async | the reference adapter; cleanest seam |
201
+ | `httpx2` | sync + async | Pydantic's httpx successor; identical public names |
202
+ | `aiohttp` | async | requires aiohttp >= 3.12 (client middleware) |
203
+ | `requests` | sync | closes requests' no-default-timeout hole |
204
+ | `urllib3` | sync | home of `RetryMode.DELEGATED` |
205
+
206
+ Third-party adapters plug in via `register_adapter("name", "module:Class", "module:CAPS")`.
207
+
208
+ ## License
209
+
210
+ Apache-2.0
@@ -0,0 +1,102 @@
1
+ clientwright/__init__.py,sha256=VFrgOcqolBGIkwT-joFCKq1UqyayZ7DmuJxTbTApS8k,5305
2
+ clientwright/__version__.py,sha256=IOEzhCbX916JIkvZr03GFY4bl9YqSkfjdqLO4qJ2FOs,50
3
+ clientwright/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ clientwright/adapters/__init__.py,sha256=jwgB4L8hAtVzrS3GXjOM0ZWIKUrJJzctuaB1jkiH30I,106
5
+ clientwright/adapters/_httpx_shared.py,sha256=RGeuEUwQXp_8dotk2zcm31DT4Bs-5PSJ2jH1WzUwB-E,31813
6
+ clientwright/adapters/_lazy.py,sha256=Vu6aXppMkYQgu7AF7gEcZ6-ra3ReTvEwyP_LcMNFsVk,1156
7
+ clientwright/adapters/aiohttp/__init__.py,sha256=7lRobBHE1Ppeg53ByfSCdIz4Kr_UgLpgha-mhQO6PKQ,1415
8
+ clientwright/adapters/aiohttp/_imports.py,sha256=rjOt8kd6xnjenb5Zkd6dRsiVeYi9gaBee2lGXVpx7rQ,1099
9
+ clientwright/adapters/aiohttp/adapter.py,sha256=2CVuTk4DnC6TzbInvBFx_edx-oJgsaPzyHzTlRIjrQY,10112
10
+ clientwright/adapters/aiohttp/capabilities.py,sha256=dH6QEyXNvNefVZ-Nl8tw_uhD-wJKOabyENeij2wqmsY,3538
11
+ clientwright/adapters/aiohttp/classify.py,sha256=QvEyus3iCwyvHqI5YGYFJipoUFWhYif5jMGQb8fHs7E,2243
12
+ clientwright/adapters/aiohttp/errors.py,sha256=HAizlzmKfDqbSB-eNsylOArtYCZSMXGzc6ufw9niCQ0,2034
13
+ clientwright/adapters/aiohttp/middleware.py,sha256=tVJCG2ZUdUFae9EbG9n4eQ9aQRXGD6Dgea5pEgmqkcQ,3832
14
+ clientwright/adapters/aiohttp/normalize.py,sha256=YaKasiD9ruurYtLAwCVbXiO7BTmFt1wmWoWXX8P3q0A,2390
15
+ clientwright/adapters/aiohttp/options.py,sha256=bXc0Qmplzo11rUknshAs-fXksFyF619CpfSd7bTACro,629
16
+ clientwright/adapters/aiohttp/trace.py,sha256=xiIKNNXsuHAYazLZz4JOfQz3mARL28AYrIbIjRM8HgE,4530
17
+ clientwright/adapters/aiohttp/views.py,sha256=diGcCDnvGYClw-zIDPjjQoCHhHb1QaM-bjrFsMYG2Xg,3921
18
+ clientwright/adapters/httpx/__init__.py,sha256=b9azYsEXW-cv0lk4x1dBLA2AZ87uE77IyR6NOMkSHho,1417
19
+ clientwright/adapters/httpx/_imports.py,sha256=WJl4eKJwsZ6fV4hS-aGxN61VJasM2hwkacCpmf-IVhc,564
20
+ clientwright/adapters/httpx/adapter.py,sha256=cK6giIydVuBfnmAkaG2GYlsiKhex9XZ66JwgEzT9Mok,1327
21
+ clientwright/adapters/httpx/capabilities.py,sha256=urnXy_rdmRiouRpqhkOTKX7DdQsy8dtOyWUgzwdoD6g,226
22
+ clientwright/adapters/httpx/classify.py,sha256=SdQEMJa5mr3JlAtUCZBanuXZX90PrUJImYkcP_3upb4,361
23
+ clientwright/adapters/httpx/errors.py,sha256=xRPd9jCJFuUDzTCLhHUqSStwSIwByie5M1lSt19wTYs,1110
24
+ clientwright/adapters/httpx/normalize.py,sha256=A1kzYupyfwRs60hNmQhBKCuP4saPccv_gVnFwtXrHrw,856
25
+ clientwright/adapters/httpx/normalize_sync.py,sha256=4OtdoPegVepOKAap9wdx0_nSuG728MKEnnnSzwsu0-A,847
26
+ clientwright/adapters/httpx/transport.py,sha256=0R0cCCict_0LYJAZx86-Dv8bQaOib4GUgG5UxB2vLDg,1002
27
+ clientwright/adapters/httpx/views.py,sha256=zRR5NzwRxj27Hs6-P1Mffh8majmLXoN2IB-ifABolH8,1304
28
+ clientwright/adapters/httpx2/__init__.py,sha256=bR_rs0hIxcaitrGzV9QZCfTwHW_k-wILau3LN2tzl1g,1485
29
+ clientwright/adapters/httpx2/_imports.py,sha256=ts2tMt9nRfvzMH-LFf1MKsAM7HIK_WqEjqsXYG5XV-k,646
30
+ clientwright/adapters/httpx2/adapter.py,sha256=76QP1btDc7nz_5lq3vjla0bFTGFusd7lTSziuwjrUI8,1183
31
+ clientwright/adapters/httpx2/capabilities.py,sha256=hM0eRx4EDHg3gE79bYrgB_VaTIDK9IMdtCkPRivGBOc,229
32
+ clientwright/adapters/httpx2/classify.py,sha256=Hh5uQIh4n2mTggefhQ2CA38gHBje3C3Lx2-ycmge9Vo,364
33
+ clientwright/adapters/httpx2/errors.py,sha256=1gHx9WLxcMHHmiO0yHnf1dkPh5qsJY2h7abPlZGTTMk,1169
34
+ clientwright/adapters/httpx2/normalize.py,sha256=xd7LsBfG6H2qTmWY7zeSkuRtlRhtRt_AtOC4JugxDbA,860
35
+ clientwright/adapters/httpx2/normalize_sync.py,sha256=NlQjrbYuyr1erhgGfcs7fpMH2aLFVOL8BqJz8OhHoxY,850
36
+ clientwright/adapters/httpx2/transport.py,sha256=FmL42IMVHueArMi0K0K4x3R8xaphI-MvOijbJBanp-E,789
37
+ clientwright/adapters/httpx2/views.py,sha256=ETB7qpu7-t5aIIdbyznaap-EHDPZUVzQSEfZU34YDVA,1217
38
+ clientwright/adapters/observability/__init__.py,sha256=vCDrA3ZXNAYn1A8QbISF-OQB6VEtZxJXSLBLBSSRJng,787
39
+ clientwright/adapters/observability/_metrics/__init__.py,sha256=4oy8sErcTaXi2KqQ6Qacg6iw6WFxJVonnF0DniZY8tw,24
40
+ clientwright/adapters/observability/_metrics/prometheus.py,sha256=ja3zzxCItZZhzeU77LegAtZbmraS5Tpg5wmsoy-Ramg,7257
41
+ clientwright/adapters/observability/_tracing/__init__.py,sha256=MoUaNFNBM2MLUNBHpt1DmkOlYpU4hAzua4hcy4XeaGY,80
42
+ clientwright/adapters/observability/_tracing/otel.py,sha256=kOFTMj2YaXtsQYJDdbbKbJnS4QenBsurVWjxOB24Ph0,2324
43
+ clientwright/adapters/requests/__init__.py,sha256=JTIoZdoMOIlwTRdsol85akQg-ojV2kfZqA4la3_2fWg,1465
44
+ clientwright/adapters/requests/_imports.py,sha256=rKIMjnNmzeXvjt2I7bvnaxZE73CEPboQiCoUF9Xc5go,778
45
+ clientwright/adapters/requests/adapter.py,sha256=Vj4H_CRq6ChxeCSAFD1Gu_SbKP7-1M6SgPPiskCz1gY,9170
46
+ clientwright/adapters/requests/capabilities.py,sha256=WNUy-8GpyD_-JRIdiZSho6wiDGMnY3k6ibwdwfhdZ00,3308
47
+ clientwright/adapters/requests/classify.py,sha256=6WUPd6uiTs8L3J20GGgvu7piBTMfhXlEXavnOIYHn8g,2435
48
+ clientwright/adapters/requests/errors.py,sha256=HM92Av9U76YEcQc3BqZTwM2ZXOWk_juaMqasUQb6-4k,1389
49
+ clientwright/adapters/requests/normalize.py,sha256=FxTHWcVrBxQSTmRKZ8vOc76zT_vJJYiyTeoEMIyxHK0,2364
50
+ clientwright/adapters/requests/views.py,sha256=_gXgRnAzDyxjTLskNIECoZxT7t21V_5ooxOZo12FpxE,4215
51
+ clientwright/adapters/urllib3/__init__.py,sha256=Hv68u5d0aeiI2yfahcZlgP8-XqjOHxo0fEW2d5UR-DI,1567
52
+ clientwright/adapters/urllib3/_imports.py,sha256=62Xqbk-26uaywjWYsIS-xdp1PHFLfapGONM_C3A-AK0,577
53
+ clientwright/adapters/urllib3/adapter.py,sha256=3O0_-rzYwZ6bxiYX1TbPVX7gMuAZzelOjIfFrilRPj8,11779
54
+ clientwright/adapters/urllib3/capabilities.py,sha256=NzoluBLNTo7Fo48KKsMKdTpLIPNSx2_sonSi4sDC0kA,3739
55
+ clientwright/adapters/urllib3/classify.py,sha256=1HDXaVYhpM9yUj_8vH7TSBf8UcnyiqIAMfeYO2f8rpw,1762
56
+ clientwright/adapters/urllib3/errors.py,sha256=3XPFxNlz7xe0PxCvvBHdnqu0HHdHl0r1Wf-lM48H2dk,1938
57
+ clientwright/adapters/urllib3/normalize.py,sha256=PBEaFH1l_Dxloex7TdI5H6lQ_4RUa8Pt8ygVWFwLeyc,1801
58
+ clientwright/adapters/urllib3/views.py,sha256=L_tsZ1yqR618UTzxuJJ8yDm4j7xYbdv51wyWJJ0pdaA,3893
59
+ clientwright/contrib/__init__.py,sha256=0rneB-yKmU7BXsNX0gJ-g6kwFOWV-XUhMREXvj-dT8k,96
60
+ clientwright/contrib/deadline.py,sha256=LThVPkeNQUnSvUplR2HJtL98L30RrOibM2z-LfokLZ0,3685
61
+ clientwright/contrib/dishka.py,sha256=rr7-R6mRRXTeg7Bj0r9IfHD25UkWHCa2weZcZCerY54,2972
62
+ clientwright/core/__init__.py,sha256=8SCohi1xxW-c1onlMwawmasOBhG4zWYZywziq5qf7kU,256
63
+ clientwright/core/capabilities.py,sha256=swJIngczmK33Iu1UMdUPezEm391769t-6Qwt905l9XM,5184
64
+ clientwright/core/config.py,sha256=83EPH3aZo40-OpesrC7dNnhkkR1cwUl7TMschNcsE8c,12012
65
+ clientwright/core/errors.py,sha256=PHihA1LzhTcnnTnD672GXjSsHxGLVP-kJQkQNY32tOM,3744
66
+ clientwright/core/model.py,sha256=7O_A6JDIPKPOexAAQN5aJDM2-shAqfx4M3KGlalaaAI,3863
67
+ clientwright/core/native.py,sha256=4ch1Min3QrX0MY1WgHJ_iO9U5tGqYc8w8WRRyfungV4,3317
68
+ clientwright/core/options.py,sha256=mU8Z7MTY3LORRDRBEfWa4C07M4Cs6YyPrVTPiXPS43A,1459
69
+ clientwright/core/plan.py,sha256=0sHnKmRaaoWCi0d70lJTEk2Ud-_8GerMVDZO9Xul2lw,6836
70
+ clientwright/core/registry.py,sha256=e7OHpWRrc80gtAFczZxXsCER2204w671mUDjMECbPtQ,2762
71
+ clientwright/core/balancer/__init__.py,sha256=8wNJ6T6IZ3r63SNeZlO1lIyzArj_3jr76uOloTX0WZM,77
72
+ clientwright/core/balancer/policy.py,sha256=bKSBppc30ev-_5GG0qj3eEArYC5k4AXOjJgQLORe9VM,660
73
+ clientwright/core/contracts/__init__.py,sha256=Igpus2dYZpm-SiPuI-Qjjoq9P6I3OZ87BFMsjnwTcq8,1259
74
+ clientwright/core/contracts/adapter.py,sha256=LclR2W6lNvv3du3VlOMlbP4h9HLK1VRQXJOWQUoBihU,2031
75
+ clientwright/core/contracts/context.py,sha256=VPTuc5jJ979DsT9mttsCRNLZI021tc4aWOxVx2PAX2Y,867
76
+ clientwright/core/contracts/message.py,sha256=Qj6AjWnBURpTPpOTFU6Lp1IAiT6DY03JWptKnc9GOzg,3730
77
+ clientwright/core/contracts/observability.py,sha256=kwmbCCvmKbswe5mOwStBGdDkD4_teaNBpvklHO2BTgw,2612
78
+ clientwright/core/contracts/settings.py,sha256=i91kGHulYIKj-fnjHnGecUTKi3yMrv68el5YkAyLOh8,3761
79
+ clientwright/core/engine/__init__.py,sha256=8R4vSMN4cKQfo7C-V2AFYAyDIyr3Oao50J2BUqj_jQo,78
80
+ clientwright/core/engine/aio.py,sha256=-vG9zZoeRyXhomNsQgSd4HqMGS0XK75YmPMTo_aWh5A,10840
81
+ clientwright/core/engine/base.py,sha256=NqQk53qI0txDt-h_1nXJDTnxHZzjUsNeKse9S-JhT0c,2120
82
+ clientwright/core/engine/redirects.py,sha256=ypNCBZWL0pdY2B4Jf2OId13NnVZGE1D9Ok-SlyWZ2jM,2016
83
+ clientwright/core/engine/suppress.py,sha256=dERDmmQiCzQSfQjHg7kJ4zJhWU9ZNHoOt57ujuzLD5s,740
84
+ clientwright/core/engine/sync.py,sha256=LVK2PYOkxTz4iKTngmJtxEQ4-gP_iyICqAQGvV3XEMk,10353
85
+ clientwright/core/policy/__init__.py,sha256=kevw6wSG1CvJ8qRl_yxhVx0U4sEtEKarXwPMggj_Ffc,78
86
+ clientwright/core/policy/budget.py,sha256=SwJuSUCkA_jOwloKVLS2OAAaCkTpoUlyOBVho42jz1k,2658
87
+ clientwright/core/policy/circuit.py,sha256=y4ghWX02xYWoOxcd3U8g85AyetWGQOkKLr18f2oRjDo,6572
88
+ clientwright/core/policy/concurrency.py,sha256=0s3_WZ4aCLnPeZd54NfkM94OSBpX1kbX5K3P1h7n3fs,3211
89
+ clientwright/core/policy/retry.py,sha256=B0Xg6wpCLSYUMvDRhSl9_BPWjiY4hWZ25MrgdC4aBcY,3057
90
+ clientwright/core/policy/timeout.py,sha256=SklL4FDuhM5BSVgMoAfCBnB0E2_zdInzZsj6ts_YI34,2476
91
+ clientwright/core/telemetry/__init__.py,sha256=qm_typj2W6PCc7br6Wx251cy8EgVx8TUZQ67qVwjIik,244
92
+ clientwright/core/telemetry/emitter.py,sha256=0ajLUwytqiu7cLZNmt9v8DRcwExzSn7fTY625RRZGwo,6314
93
+ clientwright/core/telemetry/names.py,sha256=PIvAXFdqRP30zjBq7vwij2YTw2YoR4enw0cOE1gix0g,2290
94
+ clientwright/core/telemetry/null.py,sha256=hk3FVKqyACqlgOJJV-PFAQc-74ZGjGqmbbHmKc9F-Jw,2133
95
+ clientwright/core/telemetry/redaction.py,sha256=nDlRmNVZYy7-gQHo9Uc6DwzN5qb32sk1iTjM5aEOY_4,832
96
+ clientwright/core/testing/__init__.py,sha256=dCuqJ2APDxwCeuOjOO4z4rMuE-ao4EofxB_3h8g942g,288
97
+ clientwright/core/testing/doubles.py,sha256=Xkp8NdMKscT0fAy8OBY-oagXfxir8omBex0oBIVwC9U,3379
98
+ clientwright/core/testing/origin.py,sha256=dyPqQEnBPaxQSCNEkaEhINqJlqWwrUNLM0CF0F_QY6k,7568
99
+ clientwright-0.1.0.dist-info/METADATA,sha256=cGQWD74feG9_0poIuGkM1UjZ5CSqM43xMuUL9A-XazE,8793
100
+ clientwright-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
101
+ clientwright-0.1.0.dist-info/licenses/LICENSE,sha256=HqasrACfRh7mgdbiXMVrs_X8dOCZed1b8KXY7i6Ch14,10239
102
+ clientwright-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any