httpware 0.11.0__tar.gz → 0.13.0__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.11.0 → httpware-0.13.0}/PKG-INFO +1 -1
- {httpware-0.11.0 → httpware-0.13.0}/pyproject.toml +1 -1
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/client.py +486 -6
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/middleware/resilience/circuit_breaker.py +132 -12
- {httpware-0.11.0 → httpware-0.13.0}/README.md +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/__init__.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/_internal/__init__.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/_internal/exception_mapping.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/_internal/import_checker.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/_internal/observability.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/_internal/redaction.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/_internal/status.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/decoders/__init__.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/decoders/msgspec.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/decoders/pydantic.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/errors.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/middleware/__init__.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/middleware/chain.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/middleware/resilience/__init__.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/middleware/resilience/_backoff.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/middleware/resilience/budget.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/middleware/resilience/bulkhead.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/middleware/resilience/retry.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/middleware/resilience/timeout.py +0 -0
- {httpware-0.11.0 → httpware-0.13.0}/src/httpware/py.typed +0 -0
|
@@ -228,7 +228,7 @@ class AsyncClient:
|
|
|
228
228
|
"""Delegate request construction to the wrapped httpx2.AsyncClient."""
|
|
229
229
|
return self._httpx2_client.build_request(method, url, **kwargs)
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
def _prepare_request( # noqa: PLR0913, C901 — mirrors httpx2 per-method signatures; kwargs-forwarding complexity is structural
|
|
232
232
|
self,
|
|
233
233
|
method: str,
|
|
234
234
|
url: str,
|
|
@@ -242,8 +242,7 @@ class AsyncClient:
|
|
|
242
242
|
content: typing.Any | None = None,
|
|
243
243
|
data: typing.Any | None = None,
|
|
244
244
|
files: typing.Any | None = None,
|
|
245
|
-
|
|
246
|
-
) -> httpx2.Response | T:
|
|
245
|
+
) -> httpx2.Request:
|
|
247
246
|
kwargs: dict[str, typing.Any] = {}
|
|
248
247
|
if params is not None:
|
|
249
248
|
kwargs["params"] = params
|
|
@@ -266,8 +265,70 @@ class AsyncClient:
|
|
|
266
265
|
request = self._httpx2_client.build_request(method, url, **kwargs)
|
|
267
266
|
if _is_streaming_body_async(content) or _is_streaming_body_async(data) or _is_streaming_body_async(files):
|
|
268
267
|
request.extensions[STREAMING_BODY_MARKER] = True
|
|
268
|
+
return request
|
|
269
|
+
|
|
270
|
+
async def _request_with_body( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
271
|
+
self,
|
|
272
|
+
method: str,
|
|
273
|
+
url: str,
|
|
274
|
+
*,
|
|
275
|
+
params: typing.Any | None = None,
|
|
276
|
+
headers: typing.Any | None = None,
|
|
277
|
+
cookies: typing.Any | None = None,
|
|
278
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
279
|
+
extensions: typing.Any | None = None,
|
|
280
|
+
json: typing.Any | None = None,
|
|
281
|
+
content: typing.Any | None = None,
|
|
282
|
+
data: typing.Any | None = None,
|
|
283
|
+
files: typing.Any | None = None,
|
|
284
|
+
response_model: type[T] | None = None,
|
|
285
|
+
) -> httpx2.Response | T:
|
|
286
|
+
request = self._prepare_request(
|
|
287
|
+
method,
|
|
288
|
+
url,
|
|
289
|
+
params=params,
|
|
290
|
+
headers=headers,
|
|
291
|
+
cookies=cookies,
|
|
292
|
+
timeout=timeout,
|
|
293
|
+
extensions=extensions,
|
|
294
|
+
json=json,
|
|
295
|
+
content=content,
|
|
296
|
+
data=data,
|
|
297
|
+
files=files,
|
|
298
|
+
)
|
|
269
299
|
return await self.send(request, response_model=response_model)
|
|
270
300
|
|
|
301
|
+
async def _request_with_body_with_response( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
302
|
+
self,
|
|
303
|
+
method: str,
|
|
304
|
+
url: str,
|
|
305
|
+
*,
|
|
306
|
+
params: typing.Any | None = None,
|
|
307
|
+
headers: typing.Any | None = None,
|
|
308
|
+
cookies: typing.Any | None = None,
|
|
309
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
310
|
+
extensions: typing.Any | None = None,
|
|
311
|
+
json: typing.Any | None = None,
|
|
312
|
+
content: typing.Any | None = None,
|
|
313
|
+
data: typing.Any | None = None,
|
|
314
|
+
files: typing.Any | None = None,
|
|
315
|
+
response_model: type[T],
|
|
316
|
+
) -> tuple[httpx2.Response, T]:
|
|
317
|
+
request = self._prepare_request(
|
|
318
|
+
method,
|
|
319
|
+
url,
|
|
320
|
+
params=params,
|
|
321
|
+
headers=headers,
|
|
322
|
+
cookies=cookies,
|
|
323
|
+
timeout=timeout,
|
|
324
|
+
extensions=extensions,
|
|
325
|
+
json=json,
|
|
326
|
+
content=content,
|
|
327
|
+
data=data,
|
|
328
|
+
files=files,
|
|
329
|
+
)
|
|
330
|
+
return await self.send_with_response(request, response_model=response_model)
|
|
331
|
+
|
|
271
332
|
@typing.overload
|
|
272
333
|
async def get(
|
|
273
334
|
self,
|
|
@@ -317,6 +378,29 @@ class AsyncClient:
|
|
|
317
378
|
response_model=response_model,
|
|
318
379
|
)
|
|
319
380
|
|
|
381
|
+
async def get_with_response( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
382
|
+
self,
|
|
383
|
+
url: str,
|
|
384
|
+
*,
|
|
385
|
+
params: typing.Any | None = None,
|
|
386
|
+
headers: typing.Any | None = None,
|
|
387
|
+
cookies: typing.Any | None = None,
|
|
388
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
389
|
+
extensions: typing.Any | None = None,
|
|
390
|
+
response_model: type[T],
|
|
391
|
+
) -> tuple[httpx2.Response, T]:
|
|
392
|
+
"""Send a GET request; return (response, decoded body)."""
|
|
393
|
+
return await self._request_with_body_with_response(
|
|
394
|
+
"GET",
|
|
395
|
+
url,
|
|
396
|
+
params=params,
|
|
397
|
+
headers=headers,
|
|
398
|
+
cookies=cookies,
|
|
399
|
+
timeout=timeout,
|
|
400
|
+
extensions=extensions,
|
|
401
|
+
response_model=response_model,
|
|
402
|
+
)
|
|
403
|
+
|
|
320
404
|
@typing.overload
|
|
321
405
|
async def post(
|
|
322
406
|
self,
|
|
@@ -382,6 +466,37 @@ class AsyncClient:
|
|
|
382
466
|
response_model=response_model,
|
|
383
467
|
)
|
|
384
468
|
|
|
469
|
+
async def post_with_response( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
470
|
+
self,
|
|
471
|
+
url: str,
|
|
472
|
+
*,
|
|
473
|
+
params: typing.Any | None = None,
|
|
474
|
+
headers: typing.Any | None = None,
|
|
475
|
+
cookies: typing.Any | None = None,
|
|
476
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
477
|
+
extensions: typing.Any | None = None,
|
|
478
|
+
json: typing.Any | None = None,
|
|
479
|
+
content: typing.Any | None = None,
|
|
480
|
+
data: typing.Any | None = None,
|
|
481
|
+
files: typing.Any | None = None,
|
|
482
|
+
response_model: type[T],
|
|
483
|
+
) -> tuple[httpx2.Response, T]:
|
|
484
|
+
"""Send a POST request; return (response, decoded body)."""
|
|
485
|
+
return await self._request_with_body_with_response(
|
|
486
|
+
"POST",
|
|
487
|
+
url,
|
|
488
|
+
params=params,
|
|
489
|
+
headers=headers,
|
|
490
|
+
cookies=cookies,
|
|
491
|
+
timeout=timeout,
|
|
492
|
+
extensions=extensions,
|
|
493
|
+
json=json,
|
|
494
|
+
content=content,
|
|
495
|
+
data=data,
|
|
496
|
+
files=files,
|
|
497
|
+
response_model=response_model,
|
|
498
|
+
)
|
|
499
|
+
|
|
385
500
|
@typing.overload
|
|
386
501
|
async def put(
|
|
387
502
|
self,
|
|
@@ -447,6 +562,37 @@ class AsyncClient:
|
|
|
447
562
|
response_model=response_model,
|
|
448
563
|
)
|
|
449
564
|
|
|
565
|
+
async def put_with_response( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
566
|
+
self,
|
|
567
|
+
url: str,
|
|
568
|
+
*,
|
|
569
|
+
params: typing.Any | None = None,
|
|
570
|
+
headers: typing.Any | None = None,
|
|
571
|
+
cookies: typing.Any | None = None,
|
|
572
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
573
|
+
extensions: typing.Any | None = None,
|
|
574
|
+
json: typing.Any | None = None,
|
|
575
|
+
content: typing.Any | None = None,
|
|
576
|
+
data: typing.Any | None = None,
|
|
577
|
+
files: typing.Any | None = None,
|
|
578
|
+
response_model: type[T],
|
|
579
|
+
) -> tuple[httpx2.Response, T]:
|
|
580
|
+
"""Send a PUT request; return (response, decoded body)."""
|
|
581
|
+
return await self._request_with_body_with_response(
|
|
582
|
+
"PUT",
|
|
583
|
+
url,
|
|
584
|
+
params=params,
|
|
585
|
+
headers=headers,
|
|
586
|
+
cookies=cookies,
|
|
587
|
+
timeout=timeout,
|
|
588
|
+
extensions=extensions,
|
|
589
|
+
json=json,
|
|
590
|
+
content=content,
|
|
591
|
+
data=data,
|
|
592
|
+
files=files,
|
|
593
|
+
response_model=response_model,
|
|
594
|
+
)
|
|
595
|
+
|
|
450
596
|
@typing.overload
|
|
451
597
|
async def patch(
|
|
452
598
|
self,
|
|
@@ -512,6 +658,37 @@ class AsyncClient:
|
|
|
512
658
|
response_model=response_model,
|
|
513
659
|
)
|
|
514
660
|
|
|
661
|
+
async def patch_with_response( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
662
|
+
self,
|
|
663
|
+
url: str,
|
|
664
|
+
*,
|
|
665
|
+
params: typing.Any | None = None,
|
|
666
|
+
headers: typing.Any | None = None,
|
|
667
|
+
cookies: typing.Any | None = None,
|
|
668
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
669
|
+
extensions: typing.Any | None = None,
|
|
670
|
+
json: typing.Any | None = None,
|
|
671
|
+
content: typing.Any | None = None,
|
|
672
|
+
data: typing.Any | None = None,
|
|
673
|
+
files: typing.Any | None = None,
|
|
674
|
+
response_model: type[T],
|
|
675
|
+
) -> tuple[httpx2.Response, T]:
|
|
676
|
+
"""Send a PATCH request; return (response, decoded body)."""
|
|
677
|
+
return await self._request_with_body_with_response(
|
|
678
|
+
"PATCH",
|
|
679
|
+
url,
|
|
680
|
+
params=params,
|
|
681
|
+
headers=headers,
|
|
682
|
+
cookies=cookies,
|
|
683
|
+
timeout=timeout,
|
|
684
|
+
extensions=extensions,
|
|
685
|
+
json=json,
|
|
686
|
+
content=content,
|
|
687
|
+
data=data,
|
|
688
|
+
files=files,
|
|
689
|
+
response_model=response_model,
|
|
690
|
+
)
|
|
691
|
+
|
|
515
692
|
@typing.overload
|
|
516
693
|
async def delete(
|
|
517
694
|
self,
|
|
@@ -577,6 +754,37 @@ class AsyncClient:
|
|
|
577
754
|
response_model=response_model,
|
|
578
755
|
)
|
|
579
756
|
|
|
757
|
+
async def delete_with_response( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
758
|
+
self,
|
|
759
|
+
url: str,
|
|
760
|
+
*,
|
|
761
|
+
params: typing.Any | None = None,
|
|
762
|
+
headers: typing.Any | None = None,
|
|
763
|
+
cookies: typing.Any | None = None,
|
|
764
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
765
|
+
extensions: typing.Any | None = None,
|
|
766
|
+
json: typing.Any | None = None,
|
|
767
|
+
content: typing.Any | None = None,
|
|
768
|
+
data: typing.Any | None = None,
|
|
769
|
+
files: typing.Any | None = None,
|
|
770
|
+
response_model: type[T],
|
|
771
|
+
) -> tuple[httpx2.Response, T]:
|
|
772
|
+
"""Send a DELETE request; return (response, decoded body)."""
|
|
773
|
+
return await self._request_with_body_with_response(
|
|
774
|
+
"DELETE",
|
|
775
|
+
url,
|
|
776
|
+
params=params,
|
|
777
|
+
headers=headers,
|
|
778
|
+
cookies=cookies,
|
|
779
|
+
timeout=timeout,
|
|
780
|
+
extensions=extensions,
|
|
781
|
+
json=json,
|
|
782
|
+
content=content,
|
|
783
|
+
data=data,
|
|
784
|
+
files=files,
|
|
785
|
+
response_model=response_model,
|
|
786
|
+
)
|
|
787
|
+
|
|
580
788
|
@typing.overload
|
|
581
789
|
async def head(
|
|
582
790
|
self,
|
|
@@ -743,6 +951,38 @@ class AsyncClient:
|
|
|
743
951
|
response_model=response_model,
|
|
744
952
|
)
|
|
745
953
|
|
|
954
|
+
async def request_with_response( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
955
|
+
self,
|
|
956
|
+
method: str,
|
|
957
|
+
url: str,
|
|
958
|
+
*,
|
|
959
|
+
params: typing.Any | None = None,
|
|
960
|
+
headers: typing.Any | None = None,
|
|
961
|
+
cookies: typing.Any | None = None,
|
|
962
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
963
|
+
extensions: typing.Any | None = None,
|
|
964
|
+
json: typing.Any | None = None,
|
|
965
|
+
content: typing.Any | None = None,
|
|
966
|
+
data: typing.Any | None = None,
|
|
967
|
+
files: typing.Any | None = None,
|
|
968
|
+
response_model: type[T],
|
|
969
|
+
) -> tuple[httpx2.Response, T]:
|
|
970
|
+
"""Send a request with an explicit method; return (response, decoded body)."""
|
|
971
|
+
return await self._request_with_body_with_response(
|
|
972
|
+
method,
|
|
973
|
+
url,
|
|
974
|
+
params=params,
|
|
975
|
+
headers=headers,
|
|
976
|
+
cookies=cookies,
|
|
977
|
+
timeout=timeout,
|
|
978
|
+
extensions=extensions,
|
|
979
|
+
json=json,
|
|
980
|
+
content=content,
|
|
981
|
+
data=data,
|
|
982
|
+
files=files,
|
|
983
|
+
response_model=response_model,
|
|
984
|
+
)
|
|
985
|
+
|
|
746
986
|
@contextlib.asynccontextmanager
|
|
747
987
|
async def stream( # noqa: PLR0913, C901 — mirrors httpx2 per-method signatures; kwargs-forwarding complexity is structural
|
|
748
988
|
self,
|
|
@@ -1003,7 +1243,7 @@ class Client:
|
|
|
1003
1243
|
"""Delegate request construction to the wrapped httpx2.Client."""
|
|
1004
1244
|
return self._httpx2_client.build_request(method, url, **kwargs)
|
|
1005
1245
|
|
|
1006
|
-
def
|
|
1246
|
+
def _prepare_request( # noqa: PLR0913, C901 — mirrors httpx2 per-method signatures; kwargs-forwarding complexity is structural
|
|
1007
1247
|
self,
|
|
1008
1248
|
method: str,
|
|
1009
1249
|
url: str,
|
|
@@ -1017,8 +1257,7 @@ class Client:
|
|
|
1017
1257
|
content: typing.Any | None = None,
|
|
1018
1258
|
data: typing.Any | None = None,
|
|
1019
1259
|
files: typing.Any | None = None,
|
|
1020
|
-
|
|
1021
|
-
) -> httpx2.Response | T:
|
|
1260
|
+
) -> httpx2.Request:
|
|
1022
1261
|
kwargs: dict[str, typing.Any] = {}
|
|
1023
1262
|
if params is not None:
|
|
1024
1263
|
kwargs["params"] = params
|
|
@@ -1041,8 +1280,70 @@ class Client:
|
|
|
1041
1280
|
request = self._httpx2_client.build_request(method, url, **kwargs)
|
|
1042
1281
|
if _is_streaming_body_sync(content) or _is_streaming_body_sync(data) or _is_streaming_body_sync(files):
|
|
1043
1282
|
request.extensions[STREAMING_BODY_MARKER] = True
|
|
1283
|
+
return request
|
|
1284
|
+
|
|
1285
|
+
def _request_with_body( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
1286
|
+
self,
|
|
1287
|
+
method: str,
|
|
1288
|
+
url: str,
|
|
1289
|
+
*,
|
|
1290
|
+
params: typing.Any | None = None,
|
|
1291
|
+
headers: typing.Any | None = None,
|
|
1292
|
+
cookies: typing.Any | None = None,
|
|
1293
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
1294
|
+
extensions: typing.Any | None = None,
|
|
1295
|
+
json: typing.Any | None = None,
|
|
1296
|
+
content: typing.Any | None = None,
|
|
1297
|
+
data: typing.Any | None = None,
|
|
1298
|
+
files: typing.Any | None = None,
|
|
1299
|
+
response_model: type[T] | None = None,
|
|
1300
|
+
) -> httpx2.Response | T:
|
|
1301
|
+
request = self._prepare_request(
|
|
1302
|
+
method,
|
|
1303
|
+
url,
|
|
1304
|
+
params=params,
|
|
1305
|
+
headers=headers,
|
|
1306
|
+
cookies=cookies,
|
|
1307
|
+
timeout=timeout,
|
|
1308
|
+
extensions=extensions,
|
|
1309
|
+
json=json,
|
|
1310
|
+
content=content,
|
|
1311
|
+
data=data,
|
|
1312
|
+
files=files,
|
|
1313
|
+
)
|
|
1044
1314
|
return self.send(request, response_model=response_model)
|
|
1045
1315
|
|
|
1316
|
+
def _request_with_body_with_response( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
1317
|
+
self,
|
|
1318
|
+
method: str,
|
|
1319
|
+
url: str,
|
|
1320
|
+
*,
|
|
1321
|
+
params: typing.Any | None = None,
|
|
1322
|
+
headers: typing.Any | None = None,
|
|
1323
|
+
cookies: typing.Any | None = None,
|
|
1324
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
1325
|
+
extensions: typing.Any | None = None,
|
|
1326
|
+
json: typing.Any | None = None,
|
|
1327
|
+
content: typing.Any | None = None,
|
|
1328
|
+
data: typing.Any | None = None,
|
|
1329
|
+
files: typing.Any | None = None,
|
|
1330
|
+
response_model: type[T],
|
|
1331
|
+
) -> tuple[httpx2.Response, T]:
|
|
1332
|
+
request = self._prepare_request(
|
|
1333
|
+
method,
|
|
1334
|
+
url,
|
|
1335
|
+
params=params,
|
|
1336
|
+
headers=headers,
|
|
1337
|
+
cookies=cookies,
|
|
1338
|
+
timeout=timeout,
|
|
1339
|
+
extensions=extensions,
|
|
1340
|
+
json=json,
|
|
1341
|
+
content=content,
|
|
1342
|
+
data=data,
|
|
1343
|
+
files=files,
|
|
1344
|
+
)
|
|
1345
|
+
return self.send_with_response(request, response_model=response_model)
|
|
1346
|
+
|
|
1046
1347
|
@typing.overload
|
|
1047
1348
|
def get(
|
|
1048
1349
|
self,
|
|
@@ -1092,6 +1393,29 @@ class Client:
|
|
|
1092
1393
|
response_model=response_model,
|
|
1093
1394
|
)
|
|
1094
1395
|
|
|
1396
|
+
def get_with_response( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
1397
|
+
self,
|
|
1398
|
+
url: str,
|
|
1399
|
+
*,
|
|
1400
|
+
params: typing.Any | None = None,
|
|
1401
|
+
headers: typing.Any | None = None,
|
|
1402
|
+
cookies: typing.Any | None = None,
|
|
1403
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
1404
|
+
extensions: typing.Any | None = None,
|
|
1405
|
+
response_model: type[T],
|
|
1406
|
+
) -> tuple[httpx2.Response, T]:
|
|
1407
|
+
"""Send a GET request; return (response, decoded body)."""
|
|
1408
|
+
return self._request_with_body_with_response(
|
|
1409
|
+
"GET",
|
|
1410
|
+
url,
|
|
1411
|
+
params=params,
|
|
1412
|
+
headers=headers,
|
|
1413
|
+
cookies=cookies,
|
|
1414
|
+
timeout=timeout,
|
|
1415
|
+
extensions=extensions,
|
|
1416
|
+
response_model=response_model,
|
|
1417
|
+
)
|
|
1418
|
+
|
|
1095
1419
|
@typing.overload
|
|
1096
1420
|
def post(
|
|
1097
1421
|
self,
|
|
@@ -1157,6 +1481,37 @@ class Client:
|
|
|
1157
1481
|
response_model=response_model,
|
|
1158
1482
|
)
|
|
1159
1483
|
|
|
1484
|
+
def post_with_response( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
1485
|
+
self,
|
|
1486
|
+
url: str,
|
|
1487
|
+
*,
|
|
1488
|
+
params: typing.Any | None = None,
|
|
1489
|
+
headers: typing.Any | None = None,
|
|
1490
|
+
cookies: typing.Any | None = None,
|
|
1491
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
1492
|
+
extensions: typing.Any | None = None,
|
|
1493
|
+
json: typing.Any | None = None,
|
|
1494
|
+
content: typing.Any | None = None,
|
|
1495
|
+
data: typing.Any | None = None,
|
|
1496
|
+
files: typing.Any | None = None,
|
|
1497
|
+
response_model: type[T],
|
|
1498
|
+
) -> tuple[httpx2.Response, T]:
|
|
1499
|
+
"""Send a POST request; return (response, decoded body)."""
|
|
1500
|
+
return self._request_with_body_with_response(
|
|
1501
|
+
"POST",
|
|
1502
|
+
url,
|
|
1503
|
+
params=params,
|
|
1504
|
+
headers=headers,
|
|
1505
|
+
cookies=cookies,
|
|
1506
|
+
timeout=timeout,
|
|
1507
|
+
extensions=extensions,
|
|
1508
|
+
json=json,
|
|
1509
|
+
content=content,
|
|
1510
|
+
data=data,
|
|
1511
|
+
files=files,
|
|
1512
|
+
response_model=response_model,
|
|
1513
|
+
)
|
|
1514
|
+
|
|
1160
1515
|
@typing.overload
|
|
1161
1516
|
def put(
|
|
1162
1517
|
self,
|
|
@@ -1222,6 +1577,37 @@ class Client:
|
|
|
1222
1577
|
response_model=response_model,
|
|
1223
1578
|
)
|
|
1224
1579
|
|
|
1580
|
+
def put_with_response( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
1581
|
+
self,
|
|
1582
|
+
url: str,
|
|
1583
|
+
*,
|
|
1584
|
+
params: typing.Any | None = None,
|
|
1585
|
+
headers: typing.Any | None = None,
|
|
1586
|
+
cookies: typing.Any | None = None,
|
|
1587
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
1588
|
+
extensions: typing.Any | None = None,
|
|
1589
|
+
json: typing.Any | None = None,
|
|
1590
|
+
content: typing.Any | None = None,
|
|
1591
|
+
data: typing.Any | None = None,
|
|
1592
|
+
files: typing.Any | None = None,
|
|
1593
|
+
response_model: type[T],
|
|
1594
|
+
) -> tuple[httpx2.Response, T]:
|
|
1595
|
+
"""Send a PUT request; return (response, decoded body)."""
|
|
1596
|
+
return self._request_with_body_with_response(
|
|
1597
|
+
"PUT",
|
|
1598
|
+
url,
|
|
1599
|
+
params=params,
|
|
1600
|
+
headers=headers,
|
|
1601
|
+
cookies=cookies,
|
|
1602
|
+
timeout=timeout,
|
|
1603
|
+
extensions=extensions,
|
|
1604
|
+
json=json,
|
|
1605
|
+
content=content,
|
|
1606
|
+
data=data,
|
|
1607
|
+
files=files,
|
|
1608
|
+
response_model=response_model,
|
|
1609
|
+
)
|
|
1610
|
+
|
|
1225
1611
|
@typing.overload
|
|
1226
1612
|
def patch(
|
|
1227
1613
|
self,
|
|
@@ -1287,6 +1673,37 @@ class Client:
|
|
|
1287
1673
|
response_model=response_model,
|
|
1288
1674
|
)
|
|
1289
1675
|
|
|
1676
|
+
def patch_with_response( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
1677
|
+
self,
|
|
1678
|
+
url: str,
|
|
1679
|
+
*,
|
|
1680
|
+
params: typing.Any | None = None,
|
|
1681
|
+
headers: typing.Any | None = None,
|
|
1682
|
+
cookies: typing.Any | None = None,
|
|
1683
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
1684
|
+
extensions: typing.Any | None = None,
|
|
1685
|
+
json: typing.Any | None = None,
|
|
1686
|
+
content: typing.Any | None = None,
|
|
1687
|
+
data: typing.Any | None = None,
|
|
1688
|
+
files: typing.Any | None = None,
|
|
1689
|
+
response_model: type[T],
|
|
1690
|
+
) -> tuple[httpx2.Response, T]:
|
|
1691
|
+
"""Send a PATCH request; return (response, decoded body)."""
|
|
1692
|
+
return self._request_with_body_with_response(
|
|
1693
|
+
"PATCH",
|
|
1694
|
+
url,
|
|
1695
|
+
params=params,
|
|
1696
|
+
headers=headers,
|
|
1697
|
+
cookies=cookies,
|
|
1698
|
+
timeout=timeout,
|
|
1699
|
+
extensions=extensions,
|
|
1700
|
+
json=json,
|
|
1701
|
+
content=content,
|
|
1702
|
+
data=data,
|
|
1703
|
+
files=files,
|
|
1704
|
+
response_model=response_model,
|
|
1705
|
+
)
|
|
1706
|
+
|
|
1290
1707
|
@typing.overload
|
|
1291
1708
|
def delete(
|
|
1292
1709
|
self,
|
|
@@ -1352,6 +1769,37 @@ class Client:
|
|
|
1352
1769
|
response_model=response_model,
|
|
1353
1770
|
)
|
|
1354
1771
|
|
|
1772
|
+
def delete_with_response( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
1773
|
+
self,
|
|
1774
|
+
url: str,
|
|
1775
|
+
*,
|
|
1776
|
+
params: typing.Any | None = None,
|
|
1777
|
+
headers: typing.Any | None = None,
|
|
1778
|
+
cookies: typing.Any | None = None,
|
|
1779
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
1780
|
+
extensions: typing.Any | None = None,
|
|
1781
|
+
json: typing.Any | None = None,
|
|
1782
|
+
content: typing.Any | None = None,
|
|
1783
|
+
data: typing.Any | None = None,
|
|
1784
|
+
files: typing.Any | None = None,
|
|
1785
|
+
response_model: type[T],
|
|
1786
|
+
) -> tuple[httpx2.Response, T]:
|
|
1787
|
+
"""Send a DELETE request; return (response, decoded body)."""
|
|
1788
|
+
return self._request_with_body_with_response(
|
|
1789
|
+
"DELETE",
|
|
1790
|
+
url,
|
|
1791
|
+
params=params,
|
|
1792
|
+
headers=headers,
|
|
1793
|
+
cookies=cookies,
|
|
1794
|
+
timeout=timeout,
|
|
1795
|
+
extensions=extensions,
|
|
1796
|
+
json=json,
|
|
1797
|
+
content=content,
|
|
1798
|
+
data=data,
|
|
1799
|
+
files=files,
|
|
1800
|
+
response_model=response_model,
|
|
1801
|
+
)
|
|
1802
|
+
|
|
1355
1803
|
@typing.overload
|
|
1356
1804
|
def head(
|
|
1357
1805
|
self,
|
|
@@ -1518,6 +1966,38 @@ class Client:
|
|
|
1518
1966
|
response_model=response_model,
|
|
1519
1967
|
)
|
|
1520
1968
|
|
|
1969
|
+
def request_with_response( # noqa: PLR0913 — mirrors httpx2 per-method signatures
|
|
1970
|
+
self,
|
|
1971
|
+
method: str,
|
|
1972
|
+
url: str,
|
|
1973
|
+
*,
|
|
1974
|
+
params: typing.Any | None = None,
|
|
1975
|
+
headers: typing.Any | None = None,
|
|
1976
|
+
cookies: typing.Any | None = None,
|
|
1977
|
+
timeout: typing.Any = httpx2.USE_CLIENT_DEFAULT,
|
|
1978
|
+
extensions: typing.Any | None = None,
|
|
1979
|
+
json: typing.Any | None = None,
|
|
1980
|
+
content: typing.Any | None = None,
|
|
1981
|
+
data: typing.Any | None = None,
|
|
1982
|
+
files: typing.Any | None = None,
|
|
1983
|
+
response_model: type[T],
|
|
1984
|
+
) -> tuple[httpx2.Response, T]:
|
|
1985
|
+
"""Send a request with an explicit method; return (response, decoded body)."""
|
|
1986
|
+
return self._request_with_body_with_response(
|
|
1987
|
+
method,
|
|
1988
|
+
url,
|
|
1989
|
+
params=params,
|
|
1990
|
+
headers=headers,
|
|
1991
|
+
cookies=cookies,
|
|
1992
|
+
timeout=timeout,
|
|
1993
|
+
extensions=extensions,
|
|
1994
|
+
json=json,
|
|
1995
|
+
content=content,
|
|
1996
|
+
data=data,
|
|
1997
|
+
files=files,
|
|
1998
|
+
response_model=response_model,
|
|
1999
|
+
)
|
|
2000
|
+
|
|
1521
2001
|
@contextlib.contextmanager
|
|
1522
2002
|
def stream( # noqa: PLR0913, C901 — mirrors httpx2 per-method signatures; kwargs-forwarding complexity is structural
|
|
1523
2003
|
self,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""CircuitBreaker + AsyncCircuitBreaker —
|
|
1
|
+
"""CircuitBreaker + AsyncCircuitBreaker — consecutive-failure and failure-rate circuit breakers.
|
|
2
2
|
|
|
3
3
|
See planning/specs/2026-06-13-circuit-breaker-and-timeout-design.md for the contract.
|
|
4
4
|
|
|
@@ -17,6 +17,15 @@ State machine (classic / consecutive-failure):
|
|
|
17
17
|
HALF_OPEN — admit exactly one probe at a time; success_threshold consecutive probe
|
|
18
18
|
successes close the circuit; one probe failure re-opens it.
|
|
19
19
|
|
|
20
|
+
Trip modes:
|
|
21
|
+
Classic (default) — opens when consecutive counted-failures reach failure_threshold.
|
|
22
|
+
Set failure_threshold to use this mode; leave failure_rate_threshold unset.
|
|
23
|
+
Rate (opt-in) — opens when the failure rate over a rolling window_seconds window
|
|
24
|
+
meets or exceeds failure_rate_threshold, provided at least minimum_calls
|
|
25
|
+
outcomes have been observed in that window. Set failure_rate_threshold to
|
|
26
|
+
activate; failure_threshold is ignored in this mode.
|
|
27
|
+
Half-open recovery and event names are identical across both modes.
|
|
28
|
+
|
|
20
29
|
The lock-free _CircuitBreakerState holds the transition logic, shared by both wrappers.
|
|
21
30
|
AsyncCircuitBreaker relies on asyncio atomicity (no await inside a transition) plus a
|
|
22
31
|
single-event-loop guard; CircuitBreaker (sync) serializes transitions with a
|
|
@@ -42,6 +51,9 @@ from httpware.middleware import AsyncNext, Next
|
|
|
42
51
|
_FAILURE_THRESHOLD_INVALID = "failure_threshold must be >= 1"
|
|
43
52
|
_RESET_TIMEOUT_INVALID = "reset_timeout must be >= 0"
|
|
44
53
|
_SUCCESS_THRESHOLD_INVALID = "success_threshold must be >= 1"
|
|
54
|
+
_FAILURE_RATE_THRESHOLD_INVALID = "failure_rate_threshold must be in (0, 1]"
|
|
55
|
+
_WINDOW_SECONDS_INVALID = "window_seconds must be > 0"
|
|
56
|
+
_MINIMUM_CALLS_INVALID = "minimum_calls must be >= 1"
|
|
45
57
|
_CROSS_LOOP_MSG = (
|
|
46
58
|
"AsyncCircuitBreaker is bound to a single event loop. First seen on {first!r}; "
|
|
47
59
|
"current request is on {current!r}. Use one AsyncCircuitBreaker per loop; "
|
|
@@ -50,6 +62,8 @@ _CROSS_LOOP_MSG = (
|
|
|
50
62
|
|
|
51
63
|
_DEFAULT_FAILURE_STATUS_CODES = frozenset(range(500, 600))
|
|
52
64
|
|
|
65
|
+
_BUCKET_COUNT = 10
|
|
66
|
+
|
|
53
67
|
_ROLE_CLOSED = "closed"
|
|
54
68
|
_ROLE_PROBE = "probe"
|
|
55
69
|
|
|
@@ -62,6 +76,56 @@ class _CircuitState(enum.Enum):
|
|
|
62
76
|
HALF_OPEN = "half_open"
|
|
63
77
|
|
|
64
78
|
|
|
79
|
+
class _RollingWindow:
|
|
80
|
+
"""Time-bucketed success/failure counters over a rolling window.
|
|
81
|
+
|
|
82
|
+
`window_seconds` is split into `_BUCKET_COUNT` buckets. Each bucket holds
|
|
83
|
+
[successes, failures] tagged with the integer time-slot it represents; a
|
|
84
|
+
bucket whose slot is stale is reset on write, and `totals` filters to the
|
|
85
|
+
live slot range so data older than the window never counts. Every method is
|
|
86
|
+
synchronous and reads `now` from its caller (so the breaker's critical
|
|
87
|
+
section owns the clock read).
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, window_seconds: float) -> None:
|
|
91
|
+
self._bucket_width = window_seconds / _BUCKET_COUNT
|
|
92
|
+
self._slot = [-1] * _BUCKET_COUNT
|
|
93
|
+
self._success = [0] * _BUCKET_COUNT
|
|
94
|
+
self._failure = [0] * _BUCKET_COUNT
|
|
95
|
+
|
|
96
|
+
def _current_slot(self, now: float) -> int:
|
|
97
|
+
return int(now // self._bucket_width)
|
|
98
|
+
|
|
99
|
+
def record(self, now: float, *, failed: bool) -> None:
|
|
100
|
+
slot = self._current_slot(now)
|
|
101
|
+
index = slot % _BUCKET_COUNT
|
|
102
|
+
if self._slot[index] != slot: # bucket reused for a new slot — evict
|
|
103
|
+
self._slot[index] = slot
|
|
104
|
+
self._success[index] = 0
|
|
105
|
+
self._failure[index] = 0
|
|
106
|
+
if failed:
|
|
107
|
+
self._failure[index] += 1
|
|
108
|
+
else:
|
|
109
|
+
self._success[index] += 1
|
|
110
|
+
|
|
111
|
+
def totals(self, now: float) -> tuple[int, int]:
|
|
112
|
+
"""Return (total, failures) across buckets still inside the window at `now`."""
|
|
113
|
+
slot = self._current_slot(now)
|
|
114
|
+
oldest = slot - _BUCKET_COUNT + 1
|
|
115
|
+
total = 0
|
|
116
|
+
failures = 0
|
|
117
|
+
for i in range(_BUCKET_COUNT):
|
|
118
|
+
if oldest <= self._slot[i] <= slot:
|
|
119
|
+
total += self._success[i] + self._failure[i]
|
|
120
|
+
failures += self._failure[i]
|
|
121
|
+
return total, failures
|
|
122
|
+
|
|
123
|
+
def clear(self) -> None:
|
|
124
|
+
self._slot = [-1] * _BUCKET_COUNT
|
|
125
|
+
self._success = [0] * _BUCKET_COUNT
|
|
126
|
+
self._failure = [0] * _BUCKET_COUNT
|
|
127
|
+
|
|
128
|
+
|
|
65
129
|
class _CircuitBreakerState:
|
|
66
130
|
"""Lock-free circuit-breaker state machine shared by the sync + async wrappers.
|
|
67
131
|
|
|
@@ -70,13 +134,16 @@ class _CircuitBreakerState:
|
|
|
70
134
|
inside a transition); the sync wrapper wraps each call in a threading.Lock.
|
|
71
135
|
"""
|
|
72
136
|
|
|
73
|
-
def __init__(
|
|
137
|
+
def __init__( # noqa: PLR0913 — breaker state has many orthogonal knobs; a dataclass would be worse
|
|
74
138
|
self,
|
|
75
139
|
*,
|
|
76
140
|
failure_threshold: int,
|
|
77
141
|
reset_timeout: float,
|
|
78
142
|
success_threshold: int,
|
|
79
143
|
failure_status_codes: Collection[int] | None,
|
|
144
|
+
failure_rate_threshold: float | None,
|
|
145
|
+
window_seconds: float,
|
|
146
|
+
minimum_calls: int,
|
|
80
147
|
now: Callable[[], float],
|
|
81
148
|
) -> None:
|
|
82
149
|
if failure_threshold < 1:
|
|
@@ -85,6 +152,12 @@ class _CircuitBreakerState:
|
|
|
85
152
|
raise ValueError(_RESET_TIMEOUT_INVALID)
|
|
86
153
|
if success_threshold < 1:
|
|
87
154
|
raise ValueError(_SUCCESS_THRESHOLD_INVALID)
|
|
155
|
+
if failure_rate_threshold is not None and not (0.0 < failure_rate_threshold <= 1.0):
|
|
156
|
+
raise ValueError(_FAILURE_RATE_THRESHOLD_INVALID)
|
|
157
|
+
if window_seconds <= 0:
|
|
158
|
+
raise ValueError(_WINDOW_SECONDS_INVALID)
|
|
159
|
+
if minimum_calls < 1:
|
|
160
|
+
raise ValueError(_MINIMUM_CALLS_INVALID)
|
|
88
161
|
self._failure_threshold = failure_threshold
|
|
89
162
|
self._reset_timeout = reset_timeout
|
|
90
163
|
self._success_threshold = success_threshold
|
|
@@ -93,6 +166,11 @@ class _CircuitBreakerState:
|
|
|
93
166
|
self._failure_status_codes = (
|
|
94
167
|
frozenset(failure_status_codes) if failure_status_codes is not None else _DEFAULT_FAILURE_STATUS_CODES
|
|
95
168
|
)
|
|
169
|
+
self._failure_rate_threshold = failure_rate_threshold
|
|
170
|
+
self._minimum_calls = minimum_calls
|
|
171
|
+
self._rate_mode = failure_rate_threshold is not None
|
|
172
|
+
self._window = _RollingWindow(window_seconds) if self._rate_mode else None
|
|
173
|
+
self._window_seconds = window_seconds
|
|
96
174
|
self._now = now
|
|
97
175
|
self._state = _CircuitState.CLOSED
|
|
98
176
|
self._consecutive_failures = 0
|
|
@@ -140,22 +218,30 @@ class _CircuitBreakerState:
|
|
|
140
218
|
if role == _ROLE_PROBE:
|
|
141
219
|
self._probe_in_flight = False
|
|
142
220
|
if self._state is _CircuitState.CLOSED:
|
|
143
|
-
self.
|
|
221
|
+
if self._rate_mode:
|
|
222
|
+
self._record_outcome(request, failed=False)
|
|
223
|
+
else:
|
|
224
|
+
self._consecutive_failures = 0
|
|
144
225
|
elif self._state is _CircuitState.HALF_OPEN:
|
|
145
226
|
self._consecutive_successes += 1
|
|
146
227
|
if self._consecutive_successes >= self._success_threshold:
|
|
147
228
|
self._state = _CircuitState.CLOSED
|
|
148
229
|
self._consecutive_failures = 0
|
|
149
230
|
self._consecutive_successes = 0
|
|
231
|
+
if self._rate_mode:
|
|
232
|
+
self._window.clear() # ty: ignore[unresolved-attribute]
|
|
150
233
|
self._emit(request, "circuit.closed", logging.INFO, "circuit closed — service recovered", {})
|
|
151
234
|
|
|
152
235
|
def on_failure(self, role: str, request: httpx2.Request) -> None:
|
|
153
236
|
if role == _ROLE_PROBE:
|
|
154
237
|
self._probe_in_flight = False
|
|
155
238
|
if self._state is _CircuitState.CLOSED:
|
|
156
|
-
self.
|
|
157
|
-
|
|
158
|
-
|
|
239
|
+
if self._rate_mode:
|
|
240
|
+
self._record_outcome(request, failed=True)
|
|
241
|
+
else:
|
|
242
|
+
self._consecutive_failures += 1
|
|
243
|
+
if self._consecutive_failures >= self._failure_threshold:
|
|
244
|
+
self._open(request, failures=self._consecutive_failures)
|
|
159
245
|
elif self._state is _CircuitState.HALF_OPEN:
|
|
160
246
|
self._open(request, failures=1) # 1 = the single probe failure that re-opened the circuit
|
|
161
247
|
|
|
@@ -164,19 +250,41 @@ class _CircuitBreakerState:
|
|
|
164
250
|
if role == _ROLE_PROBE:
|
|
165
251
|
self._probe_in_flight = False
|
|
166
252
|
|
|
167
|
-
def
|
|
253
|
+
def _enter_open(self, request: httpx2.Request, message: str, attributes: dict[str, typing.Any]) -> None:
|
|
168
254
|
self._state = _CircuitState.OPEN
|
|
169
255
|
self._opened_at = self._now()
|
|
170
256
|
self._consecutive_failures = 0
|
|
171
257
|
self._consecutive_successes = 0
|
|
172
|
-
self._emit(
|
|
258
|
+
self._emit(request, "circuit.opened", logging.WARNING, message, attributes)
|
|
259
|
+
|
|
260
|
+
def _open(self, request: httpx2.Request, *, failures: int) -> None:
|
|
261
|
+
self._enter_open(
|
|
173
262
|
request,
|
|
174
|
-
"circuit.opened",
|
|
175
|
-
logging.WARNING,
|
|
176
263
|
"circuit opened — failure threshold reached",
|
|
177
264
|
{"failure_threshold": self._failure_threshold, "failures": failures},
|
|
178
265
|
)
|
|
179
266
|
|
|
267
|
+
def _open_rate(self, request: httpx2.Request, *, total: int, failures: int) -> None:
|
|
268
|
+
self._enter_open(
|
|
269
|
+
request,
|
|
270
|
+
"circuit opened — failure rate threshold reached",
|
|
271
|
+
{
|
|
272
|
+
"failure_rate": failures / total,
|
|
273
|
+
"failure_rate_threshold": self._failure_rate_threshold,
|
|
274
|
+
"window_seconds": self._window_seconds,
|
|
275
|
+
"observed_calls": total,
|
|
276
|
+
},
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def _record_outcome(self, request: httpx2.Request, *, failed: bool) -> None:
|
|
280
|
+
# Only reached in rate mode, where _window and _failure_rate_threshold are non-None.
|
|
281
|
+
now = self._now()
|
|
282
|
+
self._window.record(now, failed=failed) # ty: ignore[unresolved-attribute]
|
|
283
|
+
total, failures = self._window.totals(now) # ty: ignore[unresolved-attribute]
|
|
284
|
+
threshold = self._failure_rate_threshold
|
|
285
|
+
if threshold is not None and total >= self._minimum_calls and failures / total >= threshold:
|
|
286
|
+
self._open_rate(request, total=total, failures=failures)
|
|
287
|
+
|
|
180
288
|
def _emit(
|
|
181
289
|
self,
|
|
182
290
|
request: httpx2.Request,
|
|
@@ -197,13 +305,16 @@ class _CircuitBreakerState:
|
|
|
197
305
|
class AsyncCircuitBreaker:
|
|
198
306
|
"""Async classic circuit breaker middleware. See the module docstring for the contract."""
|
|
199
307
|
|
|
200
|
-
def __init__(
|
|
308
|
+
def __init__( # noqa: PLR0913 — breaker has many orthogonal knobs; a dataclass would be worse
|
|
201
309
|
self,
|
|
202
310
|
*,
|
|
203
311
|
failure_threshold: int = 5,
|
|
204
312
|
reset_timeout: float = 30.0,
|
|
205
313
|
success_threshold: int = 1,
|
|
206
314
|
failure_status_codes: Collection[int] | None = None,
|
|
315
|
+
failure_rate_threshold: float | None = None,
|
|
316
|
+
window_seconds: float = 30.0,
|
|
317
|
+
minimum_calls: int = 20,
|
|
207
318
|
_now: Callable[[], float] = time.monotonic,
|
|
208
319
|
) -> None:
|
|
209
320
|
self._state = _CircuitBreakerState(
|
|
@@ -211,6 +322,9 @@ class AsyncCircuitBreaker:
|
|
|
211
322
|
reset_timeout=reset_timeout,
|
|
212
323
|
success_threshold=success_threshold,
|
|
213
324
|
failure_status_codes=failure_status_codes,
|
|
325
|
+
failure_rate_threshold=failure_rate_threshold,
|
|
326
|
+
window_seconds=window_seconds,
|
|
327
|
+
minimum_calls=minimum_calls,
|
|
214
328
|
now=_now,
|
|
215
329
|
)
|
|
216
330
|
self._loop: asyncio.AbstractEventLoop | None = None
|
|
@@ -261,13 +375,16 @@ class CircuitBreaker:
|
|
|
261
375
|
(one shared circuit); a sync instance cannot be shared with an AsyncClient.
|
|
262
376
|
"""
|
|
263
377
|
|
|
264
|
-
def __init__(
|
|
378
|
+
def __init__( # noqa: PLR0913 — breaker has many orthogonal knobs; a dataclass would be worse
|
|
265
379
|
self,
|
|
266
380
|
*,
|
|
267
381
|
failure_threshold: int = 5,
|
|
268
382
|
reset_timeout: float = 30.0,
|
|
269
383
|
success_threshold: int = 1,
|
|
270
384
|
failure_status_codes: Collection[int] | None = None,
|
|
385
|
+
failure_rate_threshold: float | None = None,
|
|
386
|
+
window_seconds: float = 30.0,
|
|
387
|
+
minimum_calls: int = 20,
|
|
271
388
|
_now: Callable[[], float] = time.monotonic,
|
|
272
389
|
) -> None:
|
|
273
390
|
self._state = _CircuitBreakerState(
|
|
@@ -275,6 +392,9 @@ class CircuitBreaker:
|
|
|
275
392
|
reset_timeout=reset_timeout,
|
|
276
393
|
success_threshold=success_threshold,
|
|
277
394
|
failure_status_codes=failure_status_codes,
|
|
395
|
+
failure_rate_threshold=failure_rate_threshold,
|
|
396
|
+
window_seconds=window_seconds,
|
|
397
|
+
minimum_calls=minimum_calls,
|
|
278
398
|
now=_now,
|
|
279
399
|
)
|
|
280
400
|
self._lock = threading.Lock()
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|