clientwright 0.2.0__tar.gz → 0.2.2__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.
- clientwright-0.2.2/CHANGELOG.md +43 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/PKG-INFO +10 -1
- {clientwright-0.2.0 → clientwright-0.2.2}/README.md +9 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/__init__.py +4 -0
- clientwright-0.2.2/clientwright/__version__.py +1 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/_httpx_shared.py +78 -18
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/aiohttp/__init__.py +3 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/aiohttp/adapter.py +4 -3
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/aiohttp/capabilities.py +6 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/aiohttp/errors.py +14 -1
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/aiohttp/normalize.py +7 -3
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx/__init__.py +3 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx/errors.py +7 -2
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx/normalize.py +2 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx/normalize_sync.py +2 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx2/__init__.py +3 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx2/errors.py +7 -2
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx2/normalize.py +2 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx2/normalize_sync.py +2 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/requests/adapter.py +4 -3
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/requests/capabilities.py +10 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/requests/normalize.py +4 -1
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/urllib3/adapter.py +4 -3
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/urllib3/capabilities.py +10 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/urllib3/normalize.py +4 -1
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/capabilities.py +1 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/config.py +2 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/contracts/message.py +9 -4
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/engine/aio.py +54 -30
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/engine/sync.py +37 -25
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/errors.py +18 -1
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/model.py +1 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/native.py +6 -1
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/policy/retry.py +16 -2
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/telemetry/emitter.py +21 -2
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/testing/origin.py +14 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/helpers/engine.py +15 -8
- clientwright-0.2.2/tests/helpers/telemetry.py +41 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/adapters/test_aiohttp.py +47 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/adapters/test_httpx_async.py +35 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/adapters/test_httpx_sync.py +16 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/chaos/test_origin_faults.py +9 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/parity/test_semantics.py +54 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/aiohttp/test_build.py +15 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/aiohttp/test_units.py +10 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/httpx/test_units.py +10 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/requests/test_adapter.py +1 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/test_httpx_shared.py +116 -2
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/urllib3/test_adapter.py +11 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/engine/test_aio.py +77 -13
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/engine/test_sync.py +13 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/policy/test_retry.py +31 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/telemetry/test_emitter.py +40 -2
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/telemetry/test_redaction.py +7 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/test_capabilities.py +9 -1
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/test_plan.py +9 -0
- clientwright-0.2.0/CHANGELOG.md +0 -28
- clientwright-0.2.0/clientwright/__version__.py +0 -1
- {clientwright-0.2.0 → clientwright-0.2.2}/.gitignore +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/LICENSE +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/_lazy.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/aiohttp/_imports.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/aiohttp/classify.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/aiohttp/middleware.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/aiohttp/options.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/aiohttp/trace.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/aiohttp/views.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx/_imports.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx/adapter.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx/capabilities.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx/classify.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx/transport.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx/views.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx2/_imports.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx2/adapter.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx2/capabilities.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx2/classify.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx2/transport.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/httpx2/views.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/observability/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/observability/_metrics/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/observability/_metrics/prometheus.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/observability/_tracing/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/observability/_tracing/otel.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/requests/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/requests/_imports.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/requests/classify.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/requests/errors.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/requests/views.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/urllib3/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/urllib3/_imports.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/urllib3/classify.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/urllib3/errors.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/adapters/urllib3/views.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/contrib/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/contrib/deadline.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/contrib/dishka.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/balancer/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/balancer/policy.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/contracts/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/contracts/adapter.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/contracts/context.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/contracts/observability.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/contracts/settings.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/engine/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/engine/base.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/engine/redirects.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/engine/suppress.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/options.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/plan.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/policy/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/policy/budget.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/policy/circuit.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/policy/concurrency.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/policy/timeout.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/registry.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/telemetry/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/telemetry/names.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/telemetry/null.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/telemetry/redaction.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/testing/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/core/testing/doubles.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/clientwright/py.typed +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/pyproject.toml +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/conftest.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/helpers/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/helpers/drivers.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/helpers/views.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/adapters/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/adapters/test_httpx2.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/adapters/test_requests.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/adapters/test_urllib3.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/chaos/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/chaos/test_faults.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/conftest.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/contrib/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/contrib/test_deadline.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/contrib/test_dishka.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/integration/parity/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/live/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/live/conftest.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/live/test_public_endpoints.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/live/test_tls_and_dns.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/aiohttp/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/aiohttp/test_normalize.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/aiohttp/test_trace.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/conftest.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/httpx/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/httpx2/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/httpx2/test_units.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/observability/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/observability/test_exports.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/observability/test_otel.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/observability/test_prometheus.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/requests/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/requests/test_normalize.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/requests/test_units.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/test_bare_install.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/test_httpx_family_parity.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/urllib3/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/urllib3/test_normalize.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/adapters/urllib3/test_units.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/contrib/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/contrib/test_deadline.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/contrib/test_deadline_context.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/contrib/test_dishka.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/engine/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/engine/test_base.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/engine/test_redirects.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/engine/test_suppress.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/policy/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/policy/test_budget.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/policy/test_circuit.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/policy/test_circuit_properties.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/policy/test_concurrency.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/policy/test_retry_properties.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/policy/test_timeout.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/policy/test_timeout_properties.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/telemetry/__init__.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/telemetry/test_masking.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/telemetry/test_redaction_properties.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/test_balancer.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/test_capabilities_properties.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/test_config.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/test_enum_coercion.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/test_errors.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/test_model.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/test_options.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/test_registry.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/core/test_settings.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/test_docs_snippets.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/test_sdk_imports_are_guarded.py +0 -0
- {clientwright-0.2.0 → clientwright-0.2.2}/tests/unit/test_version.py +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.2](https://github.com/bedrock-python/clientwright/compare/clientwright-v0.2.1...clientwright-v0.2.2) (2026-09-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* bound the response body by the total on the httpx family ([#28](https://github.com/bedrock-python/clientwright/issues/28)) ([5767b89](https://github.com/bedrock-python/clientwright/commit/5767b891595c7385887ecb40f2bdbf01ee615977)), closes [#25](https://github.com/bedrock-python/clientwright/issues/25)
|
|
9
|
+
* give the attempt ceiling its own retryable kind and adapter error ([#26](https://github.com/bedrock-python/clientwright/issues/26)) ([e36beeb](https://github.com/bedrock-python/clientwright/commit/e36beeba169355cb7b94a1484c032a77a12995b6)), closes [#24](https://github.com/bedrock-python/clientwright/issues/24)
|
|
10
|
+
|
|
11
|
+
## [0.2.1](https://github.com/bedrock-python/clientwright/compare/clientwright-v0.2.0...clientwright-v0.2.1) (2026-09-06)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* retry gating, native override reporting and connection metrics ([#21](https://github.com/bedrock-python/clientwright/issues/21)) ([b82e2dd](https://github.com/bedrock-python/clientwright/commit/b82e2dd886fd049f1f5d4ae8c169bdabe599d585))
|
|
17
|
+
|
|
18
|
+
## [0.2.0](https://github.com/bedrock-python/clientwright/compare/clientwright-v0.1.0...clientwright-v0.2.0) (2026-08-14)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### ⚠ BREAKING CHANGES
|
|
22
|
+
|
|
23
|
+
* add url_masker seam for value-level PII masking, drop dead sensitive_headers knob
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* add url_masker seam for value-level PII masking, drop dead sensitive_headers knob ([90f74fb](https://github.com/bedrock-python/clientwright/commit/90f74fbecaafb77b2405c4d06804e3d7dad91b34))
|
|
28
|
+
|
|
29
|
+
## 0.1.0 (2026-08-14)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Features
|
|
33
|
+
|
|
34
|
+
* initial release ([14d4ee6](https://github.com/bedrock-python/clientwright/commit/14d4ee6eba85e6bfb5445b75393891f45cd9878d))
|
|
35
|
+
|
|
36
|
+
## Changelog
|
|
37
|
+
|
|
38
|
+
All notable changes to this project are documented in this file.
|
|
39
|
+
|
|
40
|
+
This file is generated by [Release Please](https://github.com/googleapis/release-please)
|
|
41
|
+
from [Conventional Commits](https://www.conventionalcommits.org/) — do not edit it by hand.
|
|
42
|
+
The project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); while the
|
|
43
|
+
version is `0.x`, minor releases may contain breaking changes.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: clientwright
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: HTTP client platform: one resilience and observability core with adapters for httpx, aiohttp, requests, urllib3 and friends
|
|
5
5
|
Project-URL: Repository, https://github.com/bedrock-python/clientwright
|
|
6
6
|
Project-URL: Documentation, https://bedrock-python.github.io/clientwright/
|
|
@@ -95,6 +95,15 @@ handle = inspect(client) # what actually got applied
|
|
|
95
95
|
print(handle.report.dropped) # config the adapter could not express
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
> [!TIP]
|
|
99
|
+
> **Building this with an AI assistant?** Hand it
|
|
100
|
+
> **[one page](https://bedrock-python.github.io/clientwright/agents/)** instead of the
|
|
101
|
+
> whole site: the whole public API with its real defaults, where the boundary between the
|
|
102
|
+
> core engine and an adapter runs, the rules that break code when they are broken, the
|
|
103
|
+
> mistakes models actually make with this API, and a map of which page to fetch for the
|
|
104
|
+
> rest. Every docs page is also served as raw Markdown at its own URL, and a **Copy page**
|
|
105
|
+
> button at the top of each one hands it straight to a chat window.
|
|
106
|
+
|
|
98
107
|
## Why
|
|
99
108
|
|
|
100
109
|
- **Instrumentation under the public API.** The seam sits in the transport
|
|
@@ -38,6 +38,15 @@ handle = inspect(client) # what actually got applied
|
|
|
38
38
|
print(handle.report.dropped) # config the adapter could not express
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
> [!TIP]
|
|
42
|
+
> **Building this with an AI assistant?** Hand it
|
|
43
|
+
> **[one page](https://bedrock-python.github.io/clientwright/agents/)** instead of the
|
|
44
|
+
> whole site: the whole public API with its real defaults, where the boundary between the
|
|
45
|
+
> core engine and an adapter runs, the rules that break code when they are broken, the
|
|
46
|
+
> mistakes models actually make with this API, and a map of which page to fetch for the
|
|
47
|
+
> rest. Every docs page is also served as raw Markdown at its own URL, and a **Copy page**
|
|
48
|
+
> button at the top of each one hands it straight to a chat window.
|
|
49
|
+
|
|
41
50
|
## Why
|
|
42
51
|
|
|
43
52
|
- **Instrumentation under the public API.** The seam sits in the transport
|
|
@@ -60,6 +60,7 @@ from .core.contracts import (
|
|
|
60
60
|
from .core.contracts.adapter import default_deps
|
|
61
61
|
from .core.contracts.settings import client_config_from_settings
|
|
62
62
|
from .core.errors import (
|
|
63
|
+
AttemptTimeoutError,
|
|
63
64
|
CallError,
|
|
64
65
|
CircuitOpenError,
|
|
65
66
|
ClientwrightError,
|
|
@@ -74,6 +75,7 @@ from .core.model import IDEMPOTENT_METHODS, CircuitKey, FailureKind, Outcome, Re
|
|
|
74
75
|
from .core.options import CallOptions, call_options, current_call_options
|
|
75
76
|
from .core.plan import ClientHandle, ClientRuntime, inspect_client
|
|
76
77
|
from .core.registry import register_adapter, registered_adapters, resolve_adapter
|
|
78
|
+
from .core.telemetry.redaction import redact_headers
|
|
77
79
|
|
|
78
80
|
# The builders are typed ``Any`` deliberately. The whole product is "you get the
|
|
79
81
|
# REAL native client", and the core cannot name ``httpx.AsyncClient`` without
|
|
@@ -116,6 +118,7 @@ __all__ = [
|
|
|
116
118
|
"UNSET",
|
|
117
119
|
"AdapterCapabilities",
|
|
118
120
|
"AdapterDeps",
|
|
121
|
+
"AttemptTimeoutError",
|
|
119
122
|
"CallError",
|
|
120
123
|
"CallOptions",
|
|
121
124
|
"CallerOverride",
|
|
@@ -175,6 +178,7 @@ __all__ = [
|
|
|
175
178
|
"inspect",
|
|
176
179
|
"inspect_client",
|
|
177
180
|
"is_set",
|
|
181
|
+
"redact_headers",
|
|
178
182
|
"register_adapter",
|
|
179
183
|
"registered_adapters",
|
|
180
184
|
"resolve_adapter",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.2" # x-release-please-version
|
|
@@ -13,6 +13,7 @@ one extra and one import path.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import asyncio
|
|
16
17
|
import ssl
|
|
17
18
|
from collections.abc import Callable, Mapping, MutableMapping
|
|
18
19
|
from typing import Any
|
|
@@ -28,10 +29,11 @@ from ..core.config import ClientConfig, RedirectMode, is_set, resolve
|
|
|
28
29
|
from ..core.contracts.adapter import AdapterDeps
|
|
29
30
|
from ..core.engine.aio import AsyncAttemptEngine
|
|
30
31
|
from ..core.engine.sync import SyncAttemptEngine
|
|
31
|
-
from ..core.errors import CallError, CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
|
|
32
|
+
from ..core.errors import AttemptTimeoutError, CallError, CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
|
|
32
33
|
from ..core.model import IDEMPOTENT_METHODS, ConnMetrics, FailureKind, Outcome, RequestInfo, ResolvedTimeouts, origin_of
|
|
33
|
-
from ..core.native import validate_native
|
|
34
|
+
from ..core.native import accepted_overrides, validate_native
|
|
34
35
|
from ..core.plan import CallPlan, ClientHandle, ClientRuntime, compile_plan, register_handle
|
|
36
|
+
from ..core.policy.budget import Deadline
|
|
35
37
|
from ..core.policy.timeout import base_timeouts
|
|
36
38
|
from ..core.telemetry.emitter import ClientTelemetry
|
|
37
39
|
|
|
@@ -84,6 +86,7 @@ def capabilities_for(adapter: str) -> AdapterCapabilities:
|
|
|
84
86
|
Capability.TIMEOUT_WRITE: Support.NATIVE,
|
|
85
87
|
Capability.TIMEOUT_POOL: Support.NATIVE,
|
|
86
88
|
Capability.DEADLINE_HARD: Support.EMULATED,
|
|
89
|
+
Capability.DEADLINE_COVERS_BODY: Support.EMULATED,
|
|
87
90
|
Capability.POOL_LIMIT_TOTAL: Support.NATIVE,
|
|
88
91
|
Capability.POOL_LIMIT_PER_HOST: Support.EMULATED,
|
|
89
92
|
Capability.KEEPALIVE: Support.NATIVE,
|
|
@@ -105,6 +108,7 @@ def capabilities_for(adapter: str) -> AdapterCapabilities:
|
|
|
105
108
|
FailureKind.READ_TIMEOUT,
|
|
106
109
|
FailureKind.WRITE_TIMEOUT,
|
|
107
110
|
FailureKind.POOL_TIMEOUT,
|
|
111
|
+
FailureKind.ATTEMPT_TIMEOUT,
|
|
108
112
|
FailureKind.TOTAL_TIMEOUT,
|
|
109
113
|
FailureKind.CONNECT_ERROR,
|
|
110
114
|
FailureKind.TLS_ERROR,
|
|
@@ -120,9 +124,17 @@ def capabilities_for(adapter: str) -> AdapterCapabilities:
|
|
|
120
124
|
collapses={FailureKind.DNS_ERROR: FailureKind.CONNECT_ERROR},
|
|
121
125
|
notes={
|
|
122
126
|
"deadline_hard": "Hard cancellation on the async client only; the sync client clamps phases (soft).",
|
|
127
|
+
"attempt_timeout": (
|
|
128
|
+
"Emitted by the async client only; the sync client drops the attempt ceiling, so a stall arrives as "
|
|
129
|
+
"read_timeout."
|
|
130
|
+
),
|
|
123
131
|
"pool_limit_per_host": "Emulated as a per-origin in-flight semaphore; limits requests, not connections.",
|
|
124
132
|
"dns_error": f"{adapter} wraps DNS failures into ConnectError; they surface as connect_error.",
|
|
125
133
|
"proxy_from_env": "Environment proxies are parsed into mounts; NO_PROXY entries match hosts literally.",
|
|
134
|
+
"deadline_covers_body": (
|
|
135
|
+
"The response body is inside the total: the async client bounds every chunk by the remaining budget, "
|
|
136
|
+
"the sync client refuses the next chunk once the budget is gone (soft)."
|
|
137
|
+
),
|
|
126
138
|
},
|
|
127
139
|
)
|
|
128
140
|
|
|
@@ -140,8 +152,6 @@ def has_ssl_cause(exc: BaseException) -> bool:
|
|
|
140
152
|
|
|
141
153
|
def classify_family_error(sdk: Any, exc: BaseException) -> FailureKind:
|
|
142
154
|
"""Exception -> FailureKind for any SDK exposing the httpx error names."""
|
|
143
|
-
import asyncio # noqa: PLC0415 - stdlib, deferred to keep module import light
|
|
144
|
-
|
|
145
155
|
if isinstance(exc, asyncio.CancelledError):
|
|
146
156
|
return FailureKind.CANCELLED
|
|
147
157
|
if isinstance(exc, sdk.ConnectTimeout):
|
|
@@ -173,6 +183,7 @@ def make_error_translator(
|
|
|
173
183
|
circuit_cls: type[CircuitOpenError],
|
|
174
184
|
deadline_cls: type[DeadlineExceededError],
|
|
175
185
|
redirects_cls: type[TooManyRedirectsError],
|
|
186
|
+
attempt_cls: type[AttemptTimeoutError],
|
|
176
187
|
) -> Callable[[CallError], BaseException]:
|
|
177
188
|
"""Build the CallError -> dual-family translator from the bound classes."""
|
|
178
189
|
|
|
@@ -181,6 +192,8 @@ def make_error_translator(
|
|
|
181
192
|
return circuit_cls(error.key, error.retry_after)
|
|
182
193
|
if isinstance(error, DeadlineExceededError):
|
|
183
194
|
return deadline_cls(error.total)
|
|
195
|
+
if isinstance(error, AttemptTimeoutError):
|
|
196
|
+
return attempt_cls(error.attempt)
|
|
184
197
|
if isinstance(error, TooManyRedirectsError):
|
|
185
198
|
return redirects_cls(error.hops)
|
|
186
199
|
return error
|
|
@@ -344,12 +357,21 @@ class FamilyResponseView:
|
|
|
344
357
|
|
|
345
358
|
|
|
346
359
|
class TimedStreamCore:
|
|
347
|
-
"""Times body consumption and reports
|
|
360
|
+
"""Times body consumption, keeps it inside the deadline and reports the end exactly once."""
|
|
348
361
|
|
|
349
|
-
def __init__(
|
|
362
|
+
def __init__(
|
|
363
|
+
self,
|
|
364
|
+
inner: Any,
|
|
365
|
+
clock: Callable[[], float],
|
|
366
|
+
on_done: Callable[[Outcome, float], None],
|
|
367
|
+
deadline: Deadline,
|
|
368
|
+
translate: Callable[[CallError], BaseException],
|
|
369
|
+
) -> None:
|
|
350
370
|
self._inner = inner
|
|
351
371
|
self._clock = clock
|
|
352
372
|
self._on_done = on_done
|
|
373
|
+
self._deadline = deadline
|
|
374
|
+
self._translate = translate
|
|
353
375
|
self._started = clock()
|
|
354
376
|
self._finished = False
|
|
355
377
|
|
|
@@ -358,14 +380,37 @@ class TimedStreamCore:
|
|
|
358
380
|
self._finished = True
|
|
359
381
|
self._on_done(outcome, self._clock() - self._started)
|
|
360
382
|
|
|
383
|
+
def _failed(self, exc: BaseException) -> None:
|
|
384
|
+
kind = FailureKind.TOTAL_TIMEOUT if isinstance(exc, DeadlineExceededError) else FailureKind.BODY_ERROR
|
|
385
|
+
self._finish(Outcome(kind=kind, exception=exc))
|
|
386
|
+
|
|
387
|
+
def _expired(self) -> BaseException:
|
|
388
|
+
return self._translate(DeadlineExceededError(self._deadline.total or 0.0))
|
|
389
|
+
|
|
390
|
+
def _check_budget(self) -> None:
|
|
391
|
+
if self._deadline.expired:
|
|
392
|
+
raise self._expired()
|
|
393
|
+
|
|
361
394
|
|
|
362
395
|
class AsyncTimedStreamMixin(TimedStreamCore):
|
|
363
396
|
async def __aiter__(self) -> Any:
|
|
397
|
+
chunks = self._inner.__aiter__()
|
|
364
398
|
try:
|
|
365
|
-
|
|
399
|
+
while True:
|
|
400
|
+
self._check_budget()
|
|
401
|
+
scope = asyncio.timeout(self._deadline.remaining())
|
|
402
|
+
try:
|
|
403
|
+
async with scope:
|
|
404
|
+
chunk = await anext(chunks)
|
|
405
|
+
except StopAsyncIteration:
|
|
406
|
+
break
|
|
407
|
+
except Exception as exc:
|
|
408
|
+
if scope.expired():
|
|
409
|
+
raise self._expired() from exc
|
|
410
|
+
raise
|
|
366
411
|
yield chunk
|
|
367
412
|
except Exception as exc:
|
|
368
|
-
self.
|
|
413
|
+
self._failed(exc)
|
|
369
414
|
raise
|
|
370
415
|
self._finish(Outcome(kind=None))
|
|
371
416
|
|
|
@@ -378,10 +423,20 @@ class AsyncTimedStreamMixin(TimedStreamCore):
|
|
|
378
423
|
|
|
379
424
|
class SyncTimedStreamMixin(TimedStreamCore):
|
|
380
425
|
def __iter__(self) -> Any:
|
|
426
|
+
# A blocked read cannot be interrupted: the deadline is soft here and
|
|
427
|
+
# refuses to START a read once the budget is gone, so the overrun is at
|
|
428
|
+
# most one chunk or one read timeout.
|
|
429
|
+
chunks = iter(self._inner)
|
|
381
430
|
try:
|
|
382
|
-
|
|
431
|
+
while True:
|
|
432
|
+
self._check_budget()
|
|
433
|
+
try:
|
|
434
|
+
chunk = next(chunks)
|
|
435
|
+
except StopIteration:
|
|
436
|
+
break
|
|
437
|
+
yield chunk
|
|
383
438
|
except Exception as exc:
|
|
384
|
-
self.
|
|
439
|
+
self._failed(exc)
|
|
385
440
|
raise
|
|
386
441
|
self._finish(Outcome(kind=None))
|
|
387
442
|
|
|
@@ -403,12 +458,14 @@ class AsyncFamilyNormalizer:
|
|
|
403
458
|
sdk: Any,
|
|
404
459
|
request_view: Callable[[Any], Any],
|
|
405
460
|
timed_stream: Callable[..., Any],
|
|
461
|
+
translate: Callable[[CallError], BaseException],
|
|
406
462
|
) -> None:
|
|
407
463
|
self._default_timeout = default_timeout
|
|
408
464
|
self._clock = clock
|
|
409
465
|
self._sdk = sdk
|
|
410
466
|
self._request_view = request_view
|
|
411
467
|
self._timed_stream = timed_stream
|
|
468
|
+
self._translate = translate
|
|
412
469
|
|
|
413
470
|
def wrap_request(self, native: Any) -> Any:
|
|
414
471
|
return self._request_view(native)
|
|
@@ -440,11 +497,11 @@ class AsyncFamilyNormalizer:
|
|
|
440
497
|
except Exception:
|
|
441
498
|
return None
|
|
442
499
|
|
|
443
|
-
def wrap_stream(self, response: Any, on_done: Callable[[Outcome, float], None]) -> None:
|
|
500
|
+
def wrap_stream(self, response: Any, on_done: Callable[[Outcome, float], None], deadline: Deadline) -> None:
|
|
444
501
|
native = response.native
|
|
445
502
|
stream = native.stream
|
|
446
503
|
if isinstance(stream, self._sdk.AsyncByteStream):
|
|
447
|
-
native.stream = self._timed_stream(stream, self._clock, on_done)
|
|
504
|
+
native.stream = self._timed_stream(stream, self._clock, on_done, deadline, self._translate)
|
|
448
505
|
|
|
449
506
|
def conn_metrics(self, response: Any) -> ConnMetrics | None:
|
|
450
507
|
return None
|
|
@@ -461,12 +518,14 @@ class SyncFamilyNormalizer:
|
|
|
461
518
|
sdk: Any,
|
|
462
519
|
request_view: Callable[[Any], Any],
|
|
463
520
|
timed_stream: Callable[..., Any],
|
|
521
|
+
translate: Callable[[CallError], BaseException],
|
|
464
522
|
) -> None:
|
|
465
523
|
self._default_timeout = default_timeout
|
|
466
524
|
self._clock = clock
|
|
467
525
|
self._sdk = sdk
|
|
468
526
|
self._request_view = request_view
|
|
469
527
|
self._timed_stream = timed_stream
|
|
528
|
+
self._translate = translate
|
|
470
529
|
|
|
471
530
|
def wrap_request(self, native: Any) -> Any:
|
|
472
531
|
return self._request_view(native)
|
|
@@ -498,11 +557,11 @@ class SyncFamilyNormalizer:
|
|
|
498
557
|
except Exception:
|
|
499
558
|
return None
|
|
500
559
|
|
|
501
|
-
def wrap_stream(self, response: Any, on_done: Callable[[Outcome, float], None]) -> None:
|
|
560
|
+
def wrap_stream(self, response: Any, on_done: Callable[[Outcome, float], None], deadline: Deadline) -> None:
|
|
502
561
|
native = response.native
|
|
503
562
|
stream = native.stream
|
|
504
563
|
if isinstance(stream, self._sdk.SyncByteStream):
|
|
505
|
-
native.stream = self._timed_stream(stream, self._clock, on_done)
|
|
564
|
+
native.stream = self._timed_stream(stream, self._clock, on_done, deadline, self._translate)
|
|
506
565
|
|
|
507
566
|
def conn_metrics(self, response: Any) -> ConnMetrics | None:
|
|
508
567
|
return None
|
|
@@ -622,7 +681,7 @@ class FamilyAdapter:
|
|
|
622
681
|
config_conflicts={},
|
|
623
682
|
)
|
|
624
683
|
|
|
625
|
-
def _compile(self, config: ClientConfig, *, sync: bool) -> CallPlan:
|
|
684
|
+
def _compile(self, config: ClientConfig, native: Mapping[str, Mapping[str, Any]], *, sync: bool) -> CallPlan:
|
|
626
685
|
applied = {
|
|
627
686
|
Capability.TIMEOUT_CONNECT,
|
|
628
687
|
Capability.TIMEOUT_READ,
|
|
@@ -636,7 +695,7 @@ class FamilyAdapter:
|
|
|
636
695
|
applied.add(Capability.HTTP2)
|
|
637
696
|
if config.proxy is not None:
|
|
638
697
|
applied.add(Capability.PROXY)
|
|
639
|
-
emulated = {Capability.TIMEOUT_TOTAL}
|
|
698
|
+
emulated = {Capability.TIMEOUT_TOTAL, Capability.DEADLINE_COVERS_BODY}
|
|
640
699
|
dropped: dict[Capability, str] = {}
|
|
641
700
|
if sync:
|
|
642
701
|
# The sync engine cannot cancel a blocked socket call: no hard
|
|
@@ -657,6 +716,7 @@ class FamilyAdapter:
|
|
|
657
716
|
applied_natively=frozenset(applied),
|
|
658
717
|
emulated=frozenset(emulated),
|
|
659
718
|
dropped=dropped,
|
|
719
|
+
native_overrides=accepted_overrides(native),
|
|
660
720
|
)
|
|
661
721
|
plan.report.enforce(config.on_unsupported)
|
|
662
722
|
return plan
|
|
@@ -699,7 +759,7 @@ class FamilyAdapter:
|
|
|
699
759
|
runtime = deps.runtime or ClientRuntime.for_config(
|
|
700
760
|
config, clock=deps.clock, circuit_listener=telemetry.circuit_state_changed
|
|
701
761
|
)
|
|
702
|
-
plan = self._compile(config, sync=False)
|
|
762
|
+
plan = self._compile(config, native, sync=False)
|
|
703
763
|
base = base_timeouts(config.timeout, NATIVE_TIMEOUT_DEFAULTS)
|
|
704
764
|
engine = AsyncAttemptEngine(
|
|
705
765
|
plan=plan,
|
|
@@ -756,7 +816,7 @@ class FamilyAdapter:
|
|
|
756
816
|
runtime = deps.runtime or ClientRuntime.for_config(
|
|
757
817
|
config, clock=deps.clock, circuit_listener=telemetry.circuit_state_changed
|
|
758
818
|
)
|
|
759
|
-
plan = self._compile(config, sync=True)
|
|
819
|
+
plan = self._compile(config, native, sync=True)
|
|
760
820
|
base = base_timeouts(config.timeout, NATIVE_TIMEOUT_DEFAULTS)
|
|
761
821
|
engine = SyncAttemptEngine(
|
|
762
822
|
plan=plan,
|
|
@@ -13,6 +13,7 @@ from .._lazy import lazy_attribute
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
from .adapter import AiohttpAdapter as AiohttpAdapter
|
|
15
15
|
from .capabilities import CAPABILITIES as CAPABILITIES
|
|
16
|
+
from .errors import AiohttpAttemptTimeoutError as AiohttpAttemptTimeoutError
|
|
16
17
|
from .errors import AiohttpCircuitOpenError as AiohttpCircuitOpenError
|
|
17
18
|
from .errors import AiohttpDeadlineExceededError as AiohttpDeadlineExceededError
|
|
18
19
|
from .errors import AiohttpTooManyRedirectsError as AiohttpTooManyRedirectsError
|
|
@@ -22,6 +23,7 @@ if TYPE_CHECKING:
|
|
|
22
23
|
_EXPORTS = {
|
|
23
24
|
"CAPABILITIES": "capabilities",
|
|
24
25
|
"AiohttpAdapter": "adapter",
|
|
26
|
+
"AiohttpAttemptTimeoutError": "errors",
|
|
25
27
|
"AiohttpCircuitOpenError": "errors",
|
|
26
28
|
"AiohttpDeadlineExceededError": "errors",
|
|
27
29
|
"AiohttpTooManyRedirectsError": "errors",
|
|
@@ -37,6 +39,7 @@ def __getattr__(name: str) -> Any:
|
|
|
37
39
|
__all__ = [
|
|
38
40
|
"CAPABILITIES",
|
|
39
41
|
"AiohttpAdapter",
|
|
42
|
+
"AiohttpAttemptTimeoutError",
|
|
40
43
|
"AiohttpCircuitOpenError",
|
|
41
44
|
"AiohttpDeadlineExceededError",
|
|
42
45
|
"AiohttpTooManyRedirectsError",
|
|
@@ -24,7 +24,7 @@ from ...core.contracts.adapter import AdapterDeps
|
|
|
24
24
|
from ...core.engine.aio import AsyncAttemptEngine
|
|
25
25
|
from ...core.errors import UnsupportedCapabilityError
|
|
26
26
|
from ...core.model import ResolvedTimeouts
|
|
27
|
-
from ...core.native import validate_native
|
|
27
|
+
from ...core.native import accepted_overrides, validate_native
|
|
28
28
|
from ...core.plan import CallPlan, ClientHandle, ClientRuntime, compile_plan, register_handle
|
|
29
29
|
from ...core.policy.timeout import base_timeouts
|
|
30
30
|
from ...core.telemetry.emitter import ClientTelemetry
|
|
@@ -116,7 +116,7 @@ class AiohttpAdapter:
|
|
|
116
116
|
config_conflicts={},
|
|
117
117
|
)
|
|
118
118
|
|
|
119
|
-
def _compile(self, config: ClientConfig) -> CallPlan:
|
|
119
|
+
def _compile(self, config: ClientConfig, native: Mapping[str, Mapping[str, Any]]) -> CallPlan:
|
|
120
120
|
applied = {
|
|
121
121
|
Capability.TIMEOUT_CONNECT,
|
|
122
122
|
Capability.TIMEOUT_READ,
|
|
@@ -146,6 +146,7 @@ class AiohttpAdapter:
|
|
|
146
146
|
applied_natively=frozenset(applied),
|
|
147
147
|
emulated=frozenset(emulated),
|
|
148
148
|
dropped=dropped,
|
|
149
|
+
native_overrides=accepted_overrides(native),
|
|
149
150
|
)
|
|
150
151
|
plan.report.enforce(config.on_unsupported)
|
|
151
152
|
return plan
|
|
@@ -179,7 +180,7 @@ class AiohttpAdapter:
|
|
|
179
180
|
runtime = deps.runtime or ClientRuntime.for_config(
|
|
180
181
|
config, clock=deps.clock, circuit_listener=telemetry.circuit_state_changed
|
|
181
182
|
)
|
|
182
|
-
plan = self._compile(config)
|
|
183
|
+
plan = self._compile(config, native)
|
|
183
184
|
base = base_timeouts(config.timeout, _NATIVE_TIMEOUT_DEFAULTS)
|
|
184
185
|
engine = AsyncAttemptEngine(
|
|
185
186
|
plan=plan,
|
|
@@ -24,6 +24,7 @@ CAPABILITIES = AdapterCapabilities(
|
|
|
24
24
|
Capability.TIMEOUT_WRITE: Support.ABSENT,
|
|
25
25
|
Capability.TIMEOUT_POOL: Support.ABSENT,
|
|
26
26
|
Capability.DEADLINE_HARD: Support.EMULATED,
|
|
27
|
+
Capability.DEADLINE_COVERS_BODY: Support.ABSENT,
|
|
27
28
|
Capability.POOL_LIMIT_TOTAL: Support.NATIVE,
|
|
28
29
|
Capability.POOL_LIMIT_PER_HOST: Support.NATIVE,
|
|
29
30
|
Capability.KEEPALIVE: Support.NATIVE,
|
|
@@ -43,6 +44,7 @@ CAPABILITIES = AdapterCapabilities(
|
|
|
43
44
|
{
|
|
44
45
|
FailureKind.CONNECT_TIMEOUT,
|
|
45
46
|
FailureKind.READ_TIMEOUT,
|
|
47
|
+
FailureKind.ATTEMPT_TIMEOUT,
|
|
46
48
|
FailureKind.TOTAL_TIMEOUT,
|
|
47
49
|
FailureKind.CONNECT_ERROR,
|
|
48
50
|
FailureKind.DNS_ERROR,
|
|
@@ -74,6 +76,10 @@ CAPABILITIES = AdapterCapabilities(
|
|
|
74
76
|
),
|
|
75
77
|
"ceil_threshold": "ClientTimeout.ceil_threshold is raised so aiohttp never ceils deadlines to whole seconds.",
|
|
76
78
|
"body_duration": "The middleware returns at headers; body read is not instrumented (no body_duration metric).",
|
|
79
|
+
"deadline_covers_body": (
|
|
80
|
+
"The body streams outside the seam through aiohttp's StreamReader: the total stops at the headers and "
|
|
81
|
+
"only sock_read bounds a dripping body."
|
|
82
|
+
),
|
|
77
83
|
"max_keepalive": "aiohttp has no cap on the NUMBER of keep-alive connections; pool.max_keepalive is ignored.",
|
|
78
84
|
},
|
|
79
85
|
)
|
|
@@ -6,7 +6,13 @@ keeps working when clientwright raises on its own authority.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from ...core.errors import
|
|
9
|
+
from ...core.errors import (
|
|
10
|
+
AttemptTimeoutError,
|
|
11
|
+
CallError,
|
|
12
|
+
CircuitOpenError,
|
|
13
|
+
DeadlineExceededError,
|
|
14
|
+
TooManyRedirectsError,
|
|
15
|
+
)
|
|
10
16
|
from ._imports import aiohttp
|
|
11
17
|
|
|
12
18
|
|
|
@@ -18,6 +24,10 @@ class AiohttpDeadlineExceededError(DeadlineExceededError, aiohttp.ServerTimeoutE
|
|
|
18
24
|
"""Total deadline exhausted, catchable as asyncio.TimeoutError and aiohttp.ClientError."""
|
|
19
25
|
|
|
20
26
|
|
|
27
|
+
class AiohttpAttemptTimeoutError(AttemptTimeoutError, aiohttp.ServerTimeoutError):
|
|
28
|
+
"""Attempt ceiling exhausted, catchable as asyncio.TimeoutError and aiohttp.ClientError."""
|
|
29
|
+
|
|
30
|
+
|
|
21
31
|
class AiohttpTooManyRedirectsError(TooManyRedirectsError, aiohttp.TooManyRedirects):
|
|
22
32
|
"""Owned redirect limit exceeded, catchable as aiohttp.TooManyRedirects.
|
|
23
33
|
|
|
@@ -44,12 +54,15 @@ def translate_call_error(error: CallError) -> BaseException:
|
|
|
44
54
|
return AiohttpCircuitOpenError(error.key, error.retry_after)
|
|
45
55
|
if isinstance(error, DeadlineExceededError):
|
|
46
56
|
return AiohttpDeadlineExceededError(error.total)
|
|
57
|
+
if isinstance(error, AttemptTimeoutError):
|
|
58
|
+
return AiohttpAttemptTimeoutError(error.attempt)
|
|
47
59
|
if isinstance(error, TooManyRedirectsError):
|
|
48
60
|
return AiohttpTooManyRedirectsError(error.hops)
|
|
49
61
|
return error
|
|
50
62
|
|
|
51
63
|
|
|
52
64
|
__all__ = [
|
|
65
|
+
"AiohttpAttemptTimeoutError",
|
|
53
66
|
"AiohttpCircuitOpenError",
|
|
54
67
|
"AiohttpDeadlineExceededError",
|
|
55
68
|
"AiohttpTooManyRedirectsError",
|
|
@@ -9,6 +9,7 @@ from typing import Any
|
|
|
9
9
|
from ...core.contracts.message import RequestView, ResponseView
|
|
10
10
|
from ...core.engine.base import default_response_outcome
|
|
11
11
|
from ...core.model import ConnMetrics, FailureKind, Outcome
|
|
12
|
+
from ...core.policy.budget import Deadline
|
|
12
13
|
from ._imports import aiohttp
|
|
13
14
|
from .classify import classify_error
|
|
14
15
|
from .trace import current_conn_metrics
|
|
@@ -52,9 +53,12 @@ class AsyncAiohttpNormalizer:
|
|
|
52
53
|
except Exception:
|
|
53
54
|
return None
|
|
54
55
|
|
|
55
|
-
def wrap_stream(
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
def wrap_stream(
|
|
57
|
+
self, response: ResponseView, on_done: Callable[[Outcome, float], None], deadline: Deadline
|
|
58
|
+
) -> None:
|
|
59
|
+
# The middleware returns at headers; the body streams outside the seam
|
|
60
|
+
# through aiohttp's own StreamReader, so neither telemetry nor the
|
|
61
|
+
# deadline reaches it. Declared in capabilities, not silently skipped.
|
|
58
62
|
return None
|
|
59
63
|
|
|
60
64
|
def conn_metrics(self, response: ResponseView) -> ConnMetrics | None:
|
|
@@ -13,6 +13,7 @@ from .._lazy import lazy_attribute
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
from .adapter import HttpxAdapter as HttpxAdapter
|
|
15
15
|
from .capabilities import CAPABILITIES as CAPABILITIES
|
|
16
|
+
from .errors import HttpxAttemptTimeoutError as HttpxAttemptTimeoutError
|
|
16
17
|
from .errors import HttpxCircuitOpenError as HttpxCircuitOpenError
|
|
17
18
|
from .errors import HttpxDeadlineExceededError as HttpxDeadlineExceededError
|
|
18
19
|
from .errors import HttpxTooManyRedirectsError as HttpxTooManyRedirectsError
|
|
@@ -22,6 +23,7 @@ if TYPE_CHECKING:
|
|
|
22
23
|
_EXPORTS = {
|
|
23
24
|
"CAPABILITIES": "capabilities",
|
|
24
25
|
"HttpxAdapter": "adapter",
|
|
26
|
+
"HttpxAttemptTimeoutError": "errors",
|
|
25
27
|
"HttpxCircuitOpenError": "errors",
|
|
26
28
|
"HttpxDeadlineExceededError": "errors",
|
|
27
29
|
"HttpxTooManyRedirectsError": "errors",
|
|
@@ -39,6 +41,7 @@ __all__ = [
|
|
|
39
41
|
"IDEMPOTENT_EXTENSION",
|
|
40
42
|
"ROUTE_EXTENSION",
|
|
41
43
|
"HttpxAdapter",
|
|
44
|
+
"HttpxAttemptTimeoutError",
|
|
42
45
|
"HttpxCircuitOpenError",
|
|
43
46
|
"HttpxDeadlineExceededError",
|
|
44
47
|
"HttpxTooManyRedirectsError",
|
|
@@ -6,7 +6,7 @@ keeps working when clientwright raises on its own authority.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from ...core.errors import CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
|
|
9
|
+
from ...core.errors import AttemptTimeoutError, CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
|
|
10
10
|
from .._httpx_shared import make_error_translator
|
|
11
11
|
from ._imports import httpx
|
|
12
12
|
|
|
@@ -19,15 +19,20 @@ class HttpxDeadlineExceededError(DeadlineExceededError, httpx.TimeoutException):
|
|
|
19
19
|
"""Total deadline exhausted, catchable as httpx.TimeoutException."""
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
class HttpxAttemptTimeoutError(AttemptTimeoutError, httpx.TimeoutException):
|
|
23
|
+
"""Attempt ceiling exhausted, catchable as httpx.TimeoutException."""
|
|
24
|
+
|
|
25
|
+
|
|
22
26
|
class HttpxTooManyRedirectsError(TooManyRedirectsError, httpx.TooManyRedirects):
|
|
23
27
|
"""Owned redirect limit exceeded, catchable as httpx.TooManyRedirects."""
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
translate_call_error = make_error_translator(
|
|
27
|
-
HttpxCircuitOpenError, HttpxDeadlineExceededError, HttpxTooManyRedirectsError
|
|
31
|
+
HttpxCircuitOpenError, HttpxDeadlineExceededError, HttpxTooManyRedirectsError, HttpxAttemptTimeoutError
|
|
28
32
|
)
|
|
29
33
|
|
|
30
34
|
__all__ = [
|
|
35
|
+
"HttpxAttemptTimeoutError",
|
|
31
36
|
"HttpxCircuitOpenError",
|
|
32
37
|
"HttpxDeadlineExceededError",
|
|
33
38
|
"HttpxTooManyRedirectsError",
|
|
@@ -6,6 +6,7 @@ from collections.abc import Callable
|
|
|
6
6
|
|
|
7
7
|
from .._httpx_shared import AsyncFamilyNormalizer, AsyncTimedStreamMixin
|
|
8
8
|
from ._imports import httpx
|
|
9
|
+
from .errors import translate_call_error
|
|
9
10
|
from .views import HttpxRequestView
|
|
10
11
|
|
|
11
12
|
|
|
@@ -21,6 +22,7 @@ class AsyncHttpxNormalizer(AsyncFamilyNormalizer):
|
|
|
21
22
|
sdk=httpx,
|
|
22
23
|
request_view=lambda native: HttpxRequestView(native, default_timeout),
|
|
23
24
|
timed_stream=_TimedAsyncStream,
|
|
25
|
+
translate=translate_call_error,
|
|
24
26
|
)
|
|
25
27
|
|
|
26
28
|
|
|
@@ -6,6 +6,7 @@ from collections.abc import Callable
|
|
|
6
6
|
|
|
7
7
|
from .._httpx_shared import SyncFamilyNormalizer, SyncTimedStreamMixin
|
|
8
8
|
from ._imports import httpx
|
|
9
|
+
from .errors import translate_call_error
|
|
9
10
|
from .views import HttpxRequestView
|
|
10
11
|
|
|
11
12
|
|
|
@@ -21,6 +22,7 @@ class SyncHttpxNormalizer(SyncFamilyNormalizer):
|
|
|
21
22
|
sdk=httpx,
|
|
22
23
|
request_view=lambda native: HttpxRequestView(native, default_timeout),
|
|
23
24
|
timed_stream=_TimedSyncStream,
|
|
25
|
+
translate=translate_call_error,
|
|
24
26
|
)
|
|
25
27
|
|
|
26
28
|
|
|
@@ -14,6 +14,7 @@ from .._lazy import lazy_attribute
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from .adapter import HttpxAdapter as HttpxAdapter
|
|
16
16
|
from .capabilities import CAPABILITIES as CAPABILITIES
|
|
17
|
+
from .errors import HttpxAttemptTimeoutError as HttpxAttemptTimeoutError
|
|
17
18
|
from .errors import HttpxCircuitOpenError as HttpxCircuitOpenError
|
|
18
19
|
from .errors import HttpxDeadlineExceededError as HttpxDeadlineExceededError
|
|
19
20
|
from .errors import HttpxTooManyRedirectsError as HttpxTooManyRedirectsError
|
|
@@ -23,6 +24,7 @@ if TYPE_CHECKING:
|
|
|
23
24
|
_EXPORTS = {
|
|
24
25
|
"CAPABILITIES": "capabilities",
|
|
25
26
|
"HttpxAdapter": "adapter",
|
|
27
|
+
"HttpxAttemptTimeoutError": "errors",
|
|
26
28
|
"HttpxCircuitOpenError": "errors",
|
|
27
29
|
"HttpxDeadlineExceededError": "errors",
|
|
28
30
|
"HttpxTooManyRedirectsError": "errors",
|
|
@@ -40,6 +42,7 @@ __all__ = [
|
|
|
40
42
|
"IDEMPOTENT_EXTENSION",
|
|
41
43
|
"ROUTE_EXTENSION",
|
|
42
44
|
"HttpxAdapter",
|
|
45
|
+
"HttpxAttemptTimeoutError",
|
|
43
46
|
"HttpxCircuitOpenError",
|
|
44
47
|
"HttpxDeadlineExceededError",
|
|
45
48
|
"HttpxTooManyRedirectsError",
|