clientwright 0.2.1__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.1 → clientwright-0.2.2}/CHANGELOG.md +8 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/PKG-INFO +1 -1
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/__init__.py +2 -0
- clientwright-0.2.2/clientwright/__version__.py +1 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/_httpx_shared.py +73 -14
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/__init__.py +3 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/capabilities.py +6 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/errors.py +14 -1
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/normalize.py +7 -3
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/__init__.py +3 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/errors.py +7 -2
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/normalize.py +2 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/normalize_sync.py +2 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/__init__.py +3 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/errors.py +7 -2
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/normalize.py +2 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/normalize_sync.py +2 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/capabilities.py +10 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/normalize.py +4 -1
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/capabilities.py +10 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/normalize.py +4 -1
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/capabilities.py +1 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/config.py +2 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/contracts/message.py +9 -4
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/engine/aio.py +52 -29
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/engine/sync.py +35 -24
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/errors.py +9 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/model.py +1 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/testing/origin.py +14 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/helpers/engine.py +4 -1
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/adapters/test_aiohttp.py +28 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/adapters/test_httpx_async.py +35 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/adapters/test_httpx_sync.py +16 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/chaos/test_origin_faults.py +9 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/parity/test_semantics.py +21 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/aiohttp/test_units.py +10 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/httpx/test_units.py +9 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/test_httpx_shared.py +116 -2
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/engine/test_aio.py +50 -12
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_plan.py +9 -0
- clientwright-0.2.1/clientwright/__version__.py +0 -1
- {clientwright-0.2.1 → clientwright-0.2.2}/.gitignore +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/LICENSE +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/README.md +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/_lazy.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/_imports.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/adapter.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/classify.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/middleware.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/options.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/trace.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/views.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/_imports.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/adapter.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/capabilities.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/classify.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/transport.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/views.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/_imports.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/adapter.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/capabilities.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/classify.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/transport.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/views.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/observability/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/observability/_metrics/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/observability/_metrics/prometheus.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/observability/_tracing/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/observability/_tracing/otel.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/_imports.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/adapter.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/classify.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/errors.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/views.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/_imports.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/adapter.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/classify.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/errors.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/views.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/contrib/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/contrib/deadline.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/contrib/dishka.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/balancer/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/balancer/policy.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/contracts/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/contracts/adapter.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/contracts/context.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/contracts/observability.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/contracts/settings.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/engine/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/engine/base.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/engine/redirects.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/engine/suppress.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/native.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/options.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/plan.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/policy/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/policy/budget.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/policy/circuit.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/policy/concurrency.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/policy/retry.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/policy/timeout.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/registry.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/telemetry/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/telemetry/emitter.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/telemetry/names.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/telemetry/null.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/telemetry/redaction.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/testing/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/testing/doubles.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/py.typed +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/pyproject.toml +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/conftest.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/helpers/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/helpers/drivers.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/helpers/telemetry.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/helpers/views.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/adapters/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/adapters/test_httpx2.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/adapters/test_requests.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/adapters/test_urllib3.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/chaos/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/chaos/test_faults.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/conftest.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/contrib/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/contrib/test_deadline.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/contrib/test_dishka.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/parity/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/live/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/live/conftest.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/live/test_public_endpoints.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/live/test_tls_and_dns.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/aiohttp/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/aiohttp/test_build.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/aiohttp/test_normalize.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/aiohttp/test_trace.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/conftest.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/httpx/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/httpx2/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/httpx2/test_units.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/observability/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/observability/test_exports.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/observability/test_otel.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/observability/test_prometheus.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/requests/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/requests/test_adapter.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/requests/test_normalize.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/requests/test_units.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/test_bare_install.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/test_httpx_family_parity.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/urllib3/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/urllib3/test_adapter.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/urllib3/test_normalize.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/urllib3/test_units.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/contrib/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/contrib/test_deadline.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/contrib/test_deadline_context.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/contrib/test_dishka.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/engine/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/engine/test_base.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/engine/test_redirects.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/engine/test_suppress.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/engine/test_sync.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_budget.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_circuit.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_circuit_properties.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_concurrency.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_retry.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_retry_properties.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_timeout.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_timeout_properties.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/telemetry/__init__.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/telemetry/test_emitter.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/telemetry/test_masking.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/telemetry/test_redaction.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/telemetry/test_redaction_properties.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_balancer.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_capabilities.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_capabilities_properties.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_config.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_enum_coercion.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_errors.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_model.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_options.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_registry.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_settings.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/test_docs_snippets.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/test_sdk_imports_are_guarded.py +0 -0
- {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/test_version.py +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
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
|
+
|
|
3
11
|
## [0.2.1](https://github.com/bedrock-python/clientwright/compare/clientwright-v0.2.0...clientwright-v0.2.1) (2026-09-06)
|
|
4
12
|
|
|
5
13
|
|
|
@@ -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/
|
|
@@ -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,
|
|
@@ -117,6 +118,7 @@ __all__ = [
|
|
|
117
118
|
"UNSET",
|
|
118
119
|
"AdapterCapabilities",
|
|
119
120
|
"AdapterDeps",
|
|
121
|
+
"AttemptTimeoutError",
|
|
120
122
|
"CallError",
|
|
121
123
|
"CallOptions",
|
|
122
124
|
"CallerOverride",
|
|
@@ -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
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
|
|
@@ -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
|
|
@@ -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,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",
|
|
@@ -7,7 +7,7 @@ mirror the httpx adapter on purpose.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
from ...core.errors import CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
|
|
10
|
+
from ...core.errors import AttemptTimeoutError, CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
|
|
11
11
|
from .._httpx_shared import make_error_translator
|
|
12
12
|
from ._imports import httpx2
|
|
13
13
|
|
|
@@ -20,15 +20,20 @@ class HttpxDeadlineExceededError(DeadlineExceededError, httpx2.TimeoutException)
|
|
|
20
20
|
"""Total deadline exhausted, catchable as httpx2.TimeoutException."""
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
class HttpxAttemptTimeoutError(AttemptTimeoutError, httpx2.TimeoutException):
|
|
24
|
+
"""Attempt ceiling exhausted, catchable as httpx2.TimeoutException."""
|
|
25
|
+
|
|
26
|
+
|
|
23
27
|
class HttpxTooManyRedirectsError(TooManyRedirectsError, httpx2.TooManyRedirects):
|
|
24
28
|
"""Owned redirect limit exceeded, catchable as httpx2.TooManyRedirects."""
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
translate_call_error = make_error_translator(
|
|
28
|
-
HttpxCircuitOpenError, HttpxDeadlineExceededError, HttpxTooManyRedirectsError
|
|
32
|
+
HttpxCircuitOpenError, HttpxDeadlineExceededError, HttpxTooManyRedirectsError, HttpxAttemptTimeoutError
|
|
29
33
|
)
|
|
30
34
|
|
|
31
35
|
__all__ = [
|
|
36
|
+
"HttpxAttemptTimeoutError",
|
|
32
37
|
"HttpxCircuitOpenError",
|
|
33
38
|
"HttpxDeadlineExceededError",
|
|
34
39
|
"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 httpx2
|
|
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=httpx2,
|
|
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 httpx2
|
|
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=httpx2,
|
|
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
|
|
|
@@ -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.ABSENT,
|
|
27
|
+
Capability.DEADLINE_COVERS_BODY: Support.ABSENT,
|
|
27
28
|
Capability.POOL_LIMIT_TOTAL: Support.ABSENT,
|
|
28
29
|
Capability.POOL_LIMIT_PER_HOST: Support.NATIVE,
|
|
29
30
|
Capability.KEEPALIVE: Support.DEGRADED,
|
|
@@ -55,12 +56,17 @@ CAPABILITIES = AdapterCapabilities(
|
|
|
55
56
|
}
|
|
56
57
|
),
|
|
57
58
|
collapses={
|
|
59
|
+
FailureKind.ATTEMPT_TIMEOUT: FailureKind.READ_TIMEOUT,
|
|
58
60
|
FailureKind.POOL_TIMEOUT: FailureKind.CONNECT_TIMEOUT,
|
|
59
61
|
FailureKind.PROTOCOL_ERROR: FailureKind.DISCONNECTED,
|
|
60
62
|
FailureKind.WRITE_TIMEOUT: FailureKind.TOTAL_TIMEOUT,
|
|
61
63
|
},
|
|
62
64
|
notes={
|
|
63
65
|
"sync_only": "requests has no async client; build_async raises.",
|
|
66
|
+
"attempt_timeout": (
|
|
67
|
+
"A sync runtime cannot cancel an attempt, so no ceiling ever fires; the stall it would have caught "
|
|
68
|
+
"arrives as the clamped read phase."
|
|
69
|
+
),
|
|
64
70
|
"no_session_timeout": (
|
|
65
71
|
"requests has NO session-level timeout default - a bare session.get() hangs forever. The engine "
|
|
66
72
|
"closes that hole: every attempt is sent with the planned (connect, read) tuple."
|
|
@@ -74,6 +80,10 @@ CAPABILITIES = AdapterCapabilities(
|
|
|
74
80
|
"base_url": "requests has no base_url; the build rejects a config that sets one.",
|
|
75
81
|
"per_call_options": "No request extensions; route/idempotency travel via the call_options() context manager.",
|
|
76
82
|
"protocol_error": "requests folds protocol violations into ConnectionError; they surface as disconnected.",
|
|
83
|
+
"deadline_covers_body": (
|
|
84
|
+
"Session.send reads the body above the seam: the total stops at the headers and only the read timeout "
|
|
85
|
+
"bounds a dripping body."
|
|
86
|
+
),
|
|
77
87
|
},
|
|
78
88
|
)
|
|
79
89
|
|
|
@@ -8,6 +8,7 @@ from typing import Any
|
|
|
8
8
|
from ...core.contracts.message import RequestView, ResponseView
|
|
9
9
|
from ...core.engine.base import default_response_outcome
|
|
10
10
|
from ...core.model import ConnMetrics, FailureKind, Outcome
|
|
11
|
+
from ...core.policy.budget import Deadline
|
|
11
12
|
from ._imports import requests
|
|
12
13
|
from .classify import classify_error
|
|
13
14
|
from .views import RequestsRequestView, RequestsResponseView
|
|
@@ -51,7 +52,9 @@ class SyncRequestsNormalizer:
|
|
|
51
52
|
except Exception:
|
|
52
53
|
return None
|
|
53
54
|
|
|
54
|
-
def wrap_stream(
|
|
55
|
+
def wrap_stream(
|
|
56
|
+
self, response: ResponseView, on_done: Callable[[Outcome, float], None], deadline: Deadline
|
|
57
|
+
) -> None:
|
|
55
58
|
# Session.send consumes the body ABOVE this seam (unless stream=True);
|
|
56
59
|
# body read is not instrumented. Declared in capabilities.
|
|
57
60
|
return None
|
|
@@ -24,6 +24,7 @@ CAPABILITIES = AdapterCapabilities(
|
|
|
24
24
|
Capability.TIMEOUT_WRITE: Support.ABSENT,
|
|
25
25
|
Capability.TIMEOUT_POOL: Support.NATIVE,
|
|
26
26
|
Capability.DEADLINE_HARD: Support.ABSENT,
|
|
27
|
+
Capability.DEADLINE_COVERS_BODY: Support.ABSENT,
|
|
27
28
|
Capability.POOL_LIMIT_TOTAL: Support.ABSENT,
|
|
28
29
|
Capability.POOL_LIMIT_PER_HOST: Support.NATIVE,
|
|
29
30
|
Capability.KEEPALIVE: Support.DEGRADED,
|
|
@@ -56,11 +57,16 @@ CAPABILITIES = AdapterCapabilities(
|
|
|
56
57
|
}
|
|
57
58
|
),
|
|
58
59
|
collapses={
|
|
60
|
+
FailureKind.ATTEMPT_TIMEOUT: FailureKind.READ_TIMEOUT,
|
|
59
61
|
FailureKind.PROTOCOL_ERROR: FailureKind.DISCONNECTED,
|
|
60
62
|
FailureKind.WRITE_TIMEOUT: FailureKind.TOTAL_TIMEOUT,
|
|
61
63
|
},
|
|
62
64
|
notes={
|
|
63
65
|
"sync_only": "urllib3 has no async client; build_async raises.",
|
|
66
|
+
"attempt_timeout": (
|
|
67
|
+
"A sync runtime cannot cancel an attempt, so no ceiling ever fires; the stall it would have caught "
|
|
68
|
+
"arrives as the clamped read phase."
|
|
69
|
+
),
|
|
64
70
|
"seam": (
|
|
65
71
|
"The engine is injected as an INSTANCE urlopen on a genuine PoolManager (type(client) is "
|
|
66
72
|
"urllib3.PoolManager); recursive native redirect hops re-enter it and pass straight through."
|
|
@@ -80,6 +86,10 @@ CAPABILITIES = AdapterCapabilities(
|
|
|
80
86
|
"per_call_options": "No request object; route/idempotency travel via the call_options() context manager.",
|
|
81
87
|
"base_url": "urllib3 has no base_url; the build rejects a config that sets one.",
|
|
82
88
|
"proxy": "An explicit proxy builds a genuine urllib3.ProxyManager; env proxies are not read (dropped).",
|
|
89
|
+
"deadline_covers_body": (
|
|
90
|
+
"urlopen preloads or streams the body above the seam: the total stops at the headers and only the read "
|
|
91
|
+
"timeout bounds a dripping body."
|
|
92
|
+
),
|
|
83
93
|
},
|
|
84
94
|
)
|
|
85
95
|
|