clientwright 0.3.0__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clientwright-0.3.0 → clientwright-0.4.1}/CHANGELOG.md +14 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/PKG-INFO +4 -1
- {clientwright-0.3.0 → clientwright-0.4.1}/README.md +3 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/__init__.py +2 -0
- clientwright-0.4.1/clientwright/__version__.py +1 -0
- clientwright-0.4.1/clientwright/contrib/dishka.py +120 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/contrib/settings.py +5 -1
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/contracts/__init__.py +2 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/contracts/settings.py +33 -3
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/contrib/test_dishka.py +42 -0
- clientwright-0.4.1/tests/unit/contrib/test_dishka.py +124 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/contrib/test_settings.py +22 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/test_settings.py +10 -0
- clientwright-0.3.0/clientwright/__version__.py +0 -1
- clientwright-0.3.0/clientwright/contrib/dishka.py +0 -80
- clientwright-0.3.0/tests/unit/contrib/test_dishka.py +0 -28
- {clientwright-0.3.0 → clientwright-0.4.1}/.gitignore +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/LICENSE +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/_httpx_shared.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/_lazy.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/aiohttp/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/aiohttp/_imports.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/aiohttp/adapter.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/aiohttp/capabilities.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/aiohttp/classify.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/aiohttp/errors.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/aiohttp/middleware.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/aiohttp/normalize.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/aiohttp/options.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/aiohttp/trace.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/aiohttp/views.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx/_imports.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx/adapter.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx/capabilities.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx/classify.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx/errors.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx/normalize.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx/normalize_sync.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx/transport.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx/views.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx2/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx2/_imports.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx2/adapter.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx2/capabilities.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx2/classify.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx2/errors.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx2/normalize.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx2/normalize_sync.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx2/transport.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/httpx2/views.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/observability/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/observability/_metrics/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/observability/_metrics/prometheus.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/observability/_tracing/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/observability/_tracing/otel.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/requests/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/requests/_imports.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/requests/adapter.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/requests/capabilities.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/requests/classify.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/requests/errors.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/requests/normalize.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/requests/views.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/urllib3/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/urllib3/_imports.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/urllib3/adapter.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/urllib3/capabilities.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/urllib3/classify.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/urllib3/errors.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/urllib3/normalize.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/adapters/urllib3/views.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/contrib/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/contrib/deadline.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/balancer/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/balancer/policy.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/capabilities.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/config.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/contracts/adapter.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/contracts/context.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/contracts/message.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/contracts/observability.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/engine/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/engine/aio.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/engine/base.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/engine/redirects.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/engine/suppress.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/engine/sync.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/errors.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/model.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/native.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/options.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/plan.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/policy/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/policy/budget.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/policy/circuit.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/policy/concurrency.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/policy/retry.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/policy/timeout.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/registry.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/telemetry/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/telemetry/emitter.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/telemetry/names.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/telemetry/null.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/telemetry/redaction.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/testing/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/testing/doubles.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/core/testing/origin.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/clientwright/py.typed +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/pyproject.toml +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/conftest.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/helpers/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/helpers/drivers.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/helpers/engine.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/helpers/telemetry.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/helpers/views.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/adapters/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/adapters/test_aiohttp.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/adapters/test_httpx2.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/adapters/test_httpx_async.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/adapters/test_httpx_sync.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/adapters/test_requests.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/adapters/test_urllib3.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/chaos/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/chaos/test_faults.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/chaos/test_origin_faults.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/conftest.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/contrib/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/contrib/test_deadline.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/parity/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/integration/parity/test_semantics.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/live/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/live/conftest.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/live/test_public_endpoints.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/live/test_tls_and_dns.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/aiohttp/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/aiohttp/test_build.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/aiohttp/test_normalize.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/aiohttp/test_trace.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/aiohttp/test_units.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/conftest.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/httpx/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/httpx/test_units.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/httpx2/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/httpx2/test_units.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/observability/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/observability/test_exports.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/observability/test_otel.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/observability/test_prometheus.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/requests/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/requests/test_adapter.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/requests/test_normalize.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/requests/test_units.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/test_bare_install.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/test_httpx_family_parity.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/test_httpx_shared.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/urllib3/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/urllib3/test_adapter.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/urllib3/test_normalize.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/adapters/urllib3/test_units.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/contrib/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/contrib/test_deadline.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/contrib/test_deadline_context.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/engine/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/engine/test_aio.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/engine/test_base.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/engine/test_redirects.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/engine/test_suppress.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/engine/test_sync.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/policy/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/policy/test_budget.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/policy/test_circuit.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/policy/test_circuit_properties.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/policy/test_concurrency.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/policy/test_retry.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/policy/test_retry_properties.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/policy/test_timeout.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/policy/test_timeout_properties.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/telemetry/__init__.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/telemetry/test_emitter.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/telemetry/test_masking.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/telemetry/test_redaction.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/telemetry/test_redaction_properties.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/test_balancer.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/test_capabilities.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/test_capabilities_properties.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/test_config.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/test_enum_coercion.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/test_errors.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/test_model.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/test_options.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/test_plan.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/core/test_registry.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/test_docs_snippets.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/test_sdk_imports_are_guarded.py +0 -0
- {clientwright-0.3.0 → clientwright-0.4.1}/tests/unit/test_version.py +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.1](https://github.com/bedrock-python/clientwright/compare/clientwright-v0.4.0...clientwright-v0.4.1) (2026-09-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* client_config_from_settings passes a BaseClientSettings through to to_config ([fc3af5e](https://github.com/bedrock-python/clientwright/commit/fc3af5e21821c8c506f10c43733aa4d66c563c39)), closes [#38](https://github.com/bedrock-python/clientwright/issues/38)
|
|
9
|
+
|
|
10
|
+
## [0.4.0](https://github.com/bedrock-python/clientwright/compare/clientwright-v0.3.0...clientwright-v0.4.0) (2026-09-14)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* component= and client_type= on ClientwrightProvider, and the circuit-state gauge on its runtime ([0ac6cdf](https://github.com/bedrock-python/clientwright/commit/0ac6cdfd179e1e5bd57ba3d4a49c95892cf6d844)), closes [#35](https://github.com/bedrock-python/clientwright/issues/35)
|
|
16
|
+
|
|
3
17
|
## [0.3.0](https://github.com/bedrock-python/clientwright/compare/clientwright-v0.2.2...clientwright-v0.3.0) (2026-09-14)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: clientwright
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
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/
|
|
@@ -189,6 +189,9 @@ retry traffic by default), body replayability and the remaining deadline.
|
|
|
189
189
|
`AdapterDeps(runtime=...)` - otherwise breaker state dies with every request.
|
|
190
190
|
With `clientwright[dishka]`, `contrib.dishka.ClientwrightProvider` does both:
|
|
191
191
|
APP-scope runtime and a generator provide that closes the client in `finally`.
|
|
192
|
+
Several upstreams in one container are Dishka components — one
|
|
193
|
+
`ClientwrightProvider("httpx", config, component="github-api", client_type=httpx.AsyncClient)`
|
|
194
|
+
per upstream, injected as `Annotated[httpx.AsyncClient, FromComponent("github-api")]`.
|
|
192
195
|
|
|
193
196
|
## Deadline budgets
|
|
194
197
|
|
|
@@ -127,6 +127,9 @@ retry traffic by default), body replayability and the remaining deadline.
|
|
|
127
127
|
`AdapterDeps(runtime=...)` - otherwise breaker state dies with every request.
|
|
128
128
|
With `clientwright[dishka]`, `contrib.dishka.ClientwrightProvider` does both:
|
|
129
129
|
APP-scope runtime and a generator provide that closes the client in `finally`.
|
|
130
|
+
Several upstreams in one container are Dishka components — one
|
|
131
|
+
`ClientwrightProvider("httpx", config, component="github-api", client_type=httpx.AsyncClient)`
|
|
132
|
+
per upstream, injected as `Annotated[httpx.AsyncClient, FromComponent("github-api")]`.
|
|
130
133
|
|
|
131
134
|
## Deadline budgets
|
|
132
135
|
|
|
@@ -55,6 +55,7 @@ from .core.contracts import (
|
|
|
55
55
|
MaskerProtocol,
|
|
56
56
|
RetrySettingsProtocol,
|
|
57
57
|
SpanProtocol,
|
|
58
|
+
SupportsToConfig,
|
|
58
59
|
TracerProtocol,
|
|
59
60
|
)
|
|
60
61
|
from .core.contracts.adapter import default_deps
|
|
@@ -158,6 +159,7 @@ __all__ = [
|
|
|
158
159
|
"SeamGranularity",
|
|
159
160
|
"SpanProtocol",
|
|
160
161
|
"Support",
|
|
162
|
+
"SupportsToConfig",
|
|
161
163
|
"TimeoutConfig",
|
|
162
164
|
"TlsConfig",
|
|
163
165
|
"TooManyRedirectsError",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.1" # x-release-please-version
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Dishka integration (``[dishka]`` extra): APP-scope runtime, leak-free lifecycle.
|
|
2
|
+
|
|
3
|
+
Two lessons from the legacy kit are structural here:
|
|
4
|
+
|
|
5
|
+
- ``ClientRuntime`` (circuits, retry budgets, per-origin limiters) is APP
|
|
6
|
+
scope. A REQUEST-scoped runtime resets breaker state on every request and
|
|
7
|
+
turns the circuit breaker into decoration.
|
|
8
|
+
- The client is provided by a GENERATOR provide whose finally closes it - the
|
|
9
|
+
legacy provider parked ``aclose`` on an AsyncExitStack that nothing ever
|
|
10
|
+
closed, leaking a client per request.
|
|
11
|
+
|
|
12
|
+
Usage::
|
|
13
|
+
|
|
14
|
+
container = make_async_container(
|
|
15
|
+
ClientwrightProvider("httpx", config),
|
|
16
|
+
...,
|
|
17
|
+
)
|
|
18
|
+
handle = await container.get(ClientHandle[Any])
|
|
19
|
+
client: httpx.AsyncClient = handle.client
|
|
20
|
+
|
|
21
|
+
Several upstreams in one container are Dishka components: one provider per
|
|
22
|
+
upstream, each in its own component, and ``client_type`` for injection sites
|
|
23
|
+
that want the native client rather than the handle::
|
|
24
|
+
|
|
25
|
+
container = make_async_container(
|
|
26
|
+
ClientwrightProvider("httpx", login_config, component="github-oauth", client_type=httpx.AsyncClient),
|
|
27
|
+
ClientwrightProvider("httpx", api_config, component="github-api", client_type=httpx.AsyncClient),
|
|
28
|
+
)
|
|
29
|
+
api = await container.get(httpx.AsyncClient, component="github-api")
|
|
30
|
+
|
|
31
|
+
The runtime built here carries the ``http_client_circuit_state`` listener the
|
|
32
|
+
way a runtime built by an adapter does; a runtime passed in through
|
|
33
|
+
``deps.runtime`` is handed back as it came.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from collections.abc import AsyncIterator
|
|
39
|
+
from dataclasses import replace
|
|
40
|
+
from typing import Any
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
from dishka import Provider, Scope, provide
|
|
44
|
+
except ImportError as exc: # pragma: no cover - exercised only without the extra
|
|
45
|
+
raise ImportError("Dishka integration requires clientwright[dishka]; install it.") from exc
|
|
46
|
+
|
|
47
|
+
from ..core.config import ClientConfig
|
|
48
|
+
from ..core.contracts.adapter import AdapterDeps, default_deps
|
|
49
|
+
from ..core.plan import ClientHandle, ClientRuntime
|
|
50
|
+
from ..core.registry import resolve_adapter
|
|
51
|
+
from ..core.telemetry.emitter import ClientTelemetry
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ClientwrightProvider(Provider):
|
|
55
|
+
"""One async native client with an APP-scope runtime and guaranteed close.
|
|
56
|
+
|
|
57
|
+
``component`` puts everything the provider gives out in a Dishka component,
|
|
58
|
+
which is how several upstreams share one container: resolve with
|
|
59
|
+
``container.get(..., component="github-api")`` or inject with
|
|
60
|
+
``Annotated[..., FromComponent("github-api")]``. ``client_type`` also
|
|
61
|
+
provides the native client under that type (``httpx.AsyncClient``, ...),
|
|
62
|
+
the same object the handle holds.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
scope = Scope.APP
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
adapter: str,
|
|
70
|
+
config: ClientConfig,
|
|
71
|
+
deps: AdapterDeps | None = None,
|
|
72
|
+
*,
|
|
73
|
+
component: str | None = None,
|
|
74
|
+
client_type: type[Any] | None = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
super().__init__(component=component)
|
|
77
|
+
self._adapter = adapter
|
|
78
|
+
self._config = config
|
|
79
|
+
self._deps = deps or default_deps()
|
|
80
|
+
if client_type is not None:
|
|
81
|
+
self.provide(self._native_client, provides=client_type)
|
|
82
|
+
|
|
83
|
+
@provide
|
|
84
|
+
def client_runtime(self) -> ClientRuntime:
|
|
85
|
+
if self._deps.runtime is not None:
|
|
86
|
+
return self._deps.runtime
|
|
87
|
+
# An adapter wires the circuit-state gauge only to a runtime it builds
|
|
88
|
+
# itself; this runtime is built here, so the wiring happens here.
|
|
89
|
+
adapter = resolve_adapter(self._adapter)()
|
|
90
|
+
telemetry = ClientTelemetry(
|
|
91
|
+
service=self._config.service_name,
|
|
92
|
+
adapter=adapter.name,
|
|
93
|
+
seam=adapter.capabilities.seam,
|
|
94
|
+
config=self._config.observability,
|
|
95
|
+
metrics=self._deps.metrics,
|
|
96
|
+
tracer=None,
|
|
97
|
+
)
|
|
98
|
+
return ClientRuntime.for_config(
|
|
99
|
+
self._config, clock=self._deps.clock, circuit_listener=telemetry.circuit_state_changed
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@provide
|
|
103
|
+
async def client_handle(self, runtime: ClientRuntime) -> AsyncIterator[ClientHandle[Any]]:
|
|
104
|
+
adapter = resolve_adapter(self._adapter)()
|
|
105
|
+
handle: ClientHandle[Any] = adapter.build_async(self._config, replace(self._deps, runtime=runtime))
|
|
106
|
+
try:
|
|
107
|
+
yield handle
|
|
108
|
+
finally:
|
|
109
|
+
# The whole point: close travels WITH the provide, not on a stack
|
|
110
|
+
# nobody closes.
|
|
111
|
+
if handle.aclose is not None:
|
|
112
|
+
await handle.aclose()
|
|
113
|
+
elif handle.close is not None:
|
|
114
|
+
handle.close()
|
|
115
|
+
|
|
116
|
+
def _native_client(self, handle: ClientHandle[Any]) -> Any:
|
|
117
|
+
return handle.client
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
__all__ = ["ClientwrightProvider"]
|
|
@@ -283,7 +283,11 @@ class BaseClientSettings(BaseModel):
|
|
|
283
283
|
)
|
|
284
284
|
|
|
285
285
|
def to_config(self, service_name: str) -> ClientConfig:
|
|
286
|
-
"""The ``ClientConfig`` these settings describe, labelled ``service_name``.
|
|
286
|
+
"""The ``ClientConfig`` these settings describe, labelled ``service_name``.
|
|
287
|
+
|
|
288
|
+
The one translation for the shipped models. ``client_config_from_settings``
|
|
289
|
+
is the flat legacy route and passes a ``BaseClientSettings`` straight here.
|
|
290
|
+
"""
|
|
287
291
|
return ClientConfig(
|
|
288
292
|
service_name=service_name,
|
|
289
293
|
base_url=self.base_url,
|
|
@@ -15,6 +15,7 @@ from .observability import TracerProtocol as TracerProtocol
|
|
|
15
15
|
from .settings import CircuitBreakerSettingsProtocol as CircuitBreakerSettingsProtocol
|
|
16
16
|
from .settings import ClientSettingsProtocol as ClientSettingsProtocol
|
|
17
17
|
from .settings import RetrySettingsProtocol as RetrySettingsProtocol
|
|
18
|
+
from .settings import SupportsToConfig as SupportsToConfig
|
|
18
19
|
|
|
19
20
|
__all__ = [
|
|
20
21
|
"AdapterDeps",
|
|
@@ -30,6 +31,7 @@ __all__ = [
|
|
|
30
31
|
"ResponseView",
|
|
31
32
|
"RetrySettingsProtocol",
|
|
32
33
|
"SpanProtocol",
|
|
34
|
+
"SupportsToConfig",
|
|
33
35
|
"SyncNormalizer",
|
|
34
36
|
"TracerProtocol",
|
|
35
37
|
]
|
|
@@ -4,6 +4,12 @@ Services keep their own settings models (pydantic or otherwise); anything with
|
|
|
4
4
|
these read-only properties satisfies the contract - the org convention set by
|
|
5
5
|
grpc-client-kit. ``client_config_from_settings`` converts the structural shape
|
|
6
6
|
into a ``ClientConfig``.
|
|
7
|
+
|
|
8
|
+
This is the flat, legacy shape. The shipped models in
|
|
9
|
+
``clientwright.contrib.settings`` are the nested one and carry their own
|
|
10
|
+
translation, ``BaseClientSettings.to_config(service_name)``; the converter
|
|
11
|
+
passes anything with a ``to_config`` through to it, so either call builds
|
|
12
|
+
the same config.
|
|
7
13
|
"""
|
|
8
14
|
|
|
9
15
|
from __future__ import annotations
|
|
@@ -50,7 +56,12 @@ class CircuitBreakerSettingsProtocol(Protocol):
|
|
|
50
56
|
|
|
51
57
|
@runtime_checkable
|
|
52
58
|
class ClientSettingsProtocol(Protocol):
|
|
53
|
-
"""Mirror of the legacy ``RestClientSettingsProtocol`` reachable surface.
|
|
59
|
+
"""Mirror of the legacy ``RestClientSettingsProtocol`` reachable surface.
|
|
60
|
+
|
|
61
|
+
Flat names for a settings model of your own. The shipped nested models
|
|
62
|
+
(``clientwright.contrib.settings.BaseClientSettings``) do not satisfy this
|
|
63
|
+
protocol; they carry ``to_config(service_name)`` instead.
|
|
64
|
+
"""
|
|
54
65
|
|
|
55
66
|
@property
|
|
56
67
|
def base_url(self) -> str: ...
|
|
@@ -89,8 +100,26 @@ class ClientSettingsProtocol(Protocol):
|
|
|
89
100
|
def circuit_breaker(self) -> CircuitBreakerSettingsProtocol | None: ...
|
|
90
101
|
|
|
91
102
|
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
@runtime_checkable
|
|
104
|
+
class SupportsToConfig(Protocol):
|
|
105
|
+
"""Settings that build their own ``ClientConfig`` - the shape of ``BaseClientSettings``."""
|
|
106
|
+
|
|
107
|
+
def to_config(self, service_name: str) -> ClientConfig: ...
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def client_config_from_settings(settings: ClientSettingsProtocol | SupportsToConfig, service_name: str) -> ClientConfig:
|
|
111
|
+
"""Build a ClientConfig from the structural settings shape used by services.
|
|
112
|
+
|
|
113
|
+
For a settings object that builds its own config - the shipped
|
|
114
|
+
``clientwright.contrib.settings.BaseClientSettings`` - this is
|
|
115
|
+
``settings.to_config(service_name)``, and that is what it returns: the
|
|
116
|
+
nested sections (pool, tls, proxy, headers, redirects, observability, ...)
|
|
117
|
+
all reach the config. Call ``to_config`` directly where you can; the flat
|
|
118
|
+
``ClientSettingsProtocol`` route is for a model of your own that predates
|
|
119
|
+
the shipped ones.
|
|
120
|
+
"""
|
|
121
|
+
if isinstance(settings, SupportsToConfig):
|
|
122
|
+
return settings.to_config(service_name)
|
|
94
123
|
retry_settings = settings.retry
|
|
95
124
|
retry = (
|
|
96
125
|
RetryConfig(
|
|
@@ -136,5 +165,6 @@ __all__ = [
|
|
|
136
165
|
"CircuitBreakerSettingsProtocol",
|
|
137
166
|
"ClientSettingsProtocol",
|
|
138
167
|
"RetrySettingsProtocol",
|
|
168
|
+
"SupportsToConfig",
|
|
139
169
|
"client_config_from_settings",
|
|
140
170
|
]
|
|
@@ -65,3 +65,45 @@ async def test__two_container_generations__breaker_state_lives_in_the_runtime(or
|
|
|
65
65
|
await rebuilt.client.get("/status/500") # rejected locally: state survived the rebuild
|
|
66
66
|
assert origin.request_count("/status/500") == 1
|
|
67
67
|
await second.close()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def test__two_components__each_upstream_has_its_own_breaker_and_the_trip_reaches_metrics(
|
|
71
|
+
origin: OriginServer,
|
|
72
|
+
) -> None:
|
|
73
|
+
# The reporter's shape: two upstreams in one container, each on its own
|
|
74
|
+
# runtime, each closed with the container - and the tripped breaker is
|
|
75
|
+
# exported although the provider, not the adapter, built the runtime.
|
|
76
|
+
metrics = RecordingMetrics()
|
|
77
|
+
deps = AdapterDeps(metrics=metrics)
|
|
78
|
+
|
|
79
|
+
def config(service: str) -> ClientConfig:
|
|
80
|
+
return ClientConfig(
|
|
81
|
+
service_name=service,
|
|
82
|
+
base_url=origin.url,
|
|
83
|
+
retry=None,
|
|
84
|
+
circuit_breaker=CircuitBreakerConfig(fail_threshold=1, recovery_timeout=60.0),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
container = make_async_container(
|
|
88
|
+
ClientwrightProvider(
|
|
89
|
+
"httpx", config("github-oauth"), deps, component="github-oauth", client_type=httpx.AsyncClient
|
|
90
|
+
),
|
|
91
|
+
ClientwrightProvider(
|
|
92
|
+
"httpx", config("github-api"), deps, component="github-api", client_type=httpx.AsyncClient
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
login = await container.get(httpx.AsyncClient, component="github-oauth")
|
|
96
|
+
api = await container.get(httpx.AsyncClient, component="github-api")
|
|
97
|
+
assert login is not api
|
|
98
|
+
|
|
99
|
+
trip = await api.get("/status/500")
|
|
100
|
+
assert trip.status_code == 500 # one 5xx signal arms github-api's breaker
|
|
101
|
+
with pytest.raises(httpx.HTTPError):
|
|
102
|
+
await api.get("/status/500") # rejected locally
|
|
103
|
+
assert (await login.get("/status/500")).status_code == 500 # github-oauth's breaker is its own
|
|
104
|
+
assert origin.request_count("/status/500") == 2
|
|
105
|
+
assert [record["state"] for record in metrics.circuit_states] == ["open", "open"]
|
|
106
|
+
|
|
107
|
+
await container.close()
|
|
108
|
+
assert login.is_closed
|
|
109
|
+
assert api.is_closed
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""contrib units: the dishka provider lifecycle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated, Any
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
pytest.importorskip("dishka", reason="requires the [dishka] extra")
|
|
10
|
+
httpx = pytest.importorskip("httpx", reason="requires the [httpx] extra")
|
|
11
|
+
|
|
12
|
+
from dishka import FromComponent, Provider, Scope, make_async_container, provide # noqa: E402
|
|
13
|
+
|
|
14
|
+
import clientwright # noqa: E402
|
|
15
|
+
from clientwright import AdapterDeps, CircuitBreakerConfig, FailureKind, Outcome # noqa: E402
|
|
16
|
+
from clientwright.contrib.dishka import ClientwrightProvider # noqa: E402
|
|
17
|
+
from clientwright.core.plan import ClientHandle, ClientRuntime # noqa: E402
|
|
18
|
+
from clientwright.core.telemetry import names # noqa: E402
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def test__provider__builds_client_and_closes_it_with_container() -> None:
|
|
22
|
+
config = clientwright.ClientConfig(service_name="di-test")
|
|
23
|
+
container = make_async_container(ClientwrightProvider("httpx", config))
|
|
24
|
+
handle = await container.get(ClientHandle[Any])
|
|
25
|
+
assert type(handle.client) is httpx.AsyncClient
|
|
26
|
+
runtime = await container.get(ClientRuntime)
|
|
27
|
+
assert handle.runtime is runtime # APP-scope runtime shared through DI
|
|
28
|
+
assert not handle.client.is_closed
|
|
29
|
+
await container.close()
|
|
30
|
+
assert handle.client.is_closed # the generator provide's finally ran
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def test__request_scope__leaves_the_client_open_until_the_container_closes() -> None:
|
|
34
|
+
# Close travels with the APP-scope provide: a request scope coming and going
|
|
35
|
+
# must not touch the client (the legacy kit parked aclose on a
|
|
36
|
+
# request-scoped exit stack and leaked a client per request).
|
|
37
|
+
config = clientwright.ClientConfig(service_name="di-scope")
|
|
38
|
+
container = make_async_container(ClientwrightProvider("httpx", config))
|
|
39
|
+
async with container() as request:
|
|
40
|
+
handle = await request.get(ClientHandle[Any])
|
|
41
|
+
assert not handle.client.is_closed
|
|
42
|
+
await container.close()
|
|
43
|
+
assert handle.client.is_closed
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def test__two_components__serve_two_upstreams_from_one_container() -> None:
|
|
47
|
+
login = clientwright.ClientConfig(service_name="github-oauth", base_url="https://github.com")
|
|
48
|
+
api = clientwright.ClientConfig(service_name="github-api", base_url="https://api.github.com")
|
|
49
|
+
container = make_async_container(
|
|
50
|
+
ClientwrightProvider("httpx", login, component="github-oauth"),
|
|
51
|
+
ClientwrightProvider("httpx", api, component="github-api"),
|
|
52
|
+
)
|
|
53
|
+
login_handle = await container.get(ClientHandle[Any], component="github-oauth")
|
|
54
|
+
api_handle = await container.get(ClientHandle[Any], component="github-api")
|
|
55
|
+
assert login_handle is not api_handle
|
|
56
|
+
assert login_handle.plan.config.service_name == "github-oauth"
|
|
57
|
+
assert api_handle.plan.config.service_name == "github-api"
|
|
58
|
+
# Each component resolves its own runtime: two upstreams are two breakers.
|
|
59
|
+
assert login_handle.runtime is not api_handle.runtime
|
|
60
|
+
assert login_handle.runtime is await container.get(ClientRuntime, component="github-oauth")
|
|
61
|
+
await container.close()
|
|
62
|
+
assert login_handle.client.is_closed
|
|
63
|
+
assert api_handle.client.is_closed
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def test__client_type__provides_the_native_client_the_handle_holds() -> None:
|
|
67
|
+
config = clientwright.ClientConfig(service_name="di-typed")
|
|
68
|
+
container = make_async_container(ClientwrightProvider("httpx", config, client_type=httpx.AsyncClient))
|
|
69
|
+
client = await container.get(httpx.AsyncClient)
|
|
70
|
+
handle = await container.get(ClientHandle[Any])
|
|
71
|
+
assert client is handle.client
|
|
72
|
+
await container.close()
|
|
73
|
+
assert client.is_closed
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class GitHubOAuthProvider:
|
|
77
|
+
"""The reporter's shape: two clientwright clients handed to one object."""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
login_client: Annotated[httpx.AsyncClient, FromComponent("github-oauth")],
|
|
82
|
+
api_client: Annotated[httpx.AsyncClient, FromComponent("github-api")],
|
|
83
|
+
) -> None:
|
|
84
|
+
self.login_client = login_client
|
|
85
|
+
self.api_client = api_client
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ServiceProvider(Provider):
|
|
89
|
+
scope = Scope.APP
|
|
90
|
+
github = provide(GitHubOAuthProvider)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def test__client_type_per_component__injects_each_upstream_by_name() -> None:
|
|
94
|
+
login = clientwright.ClientConfig(service_name="github-oauth", base_url="https://github.com")
|
|
95
|
+
api = clientwright.ClientConfig(service_name="github-api", base_url="https://api.github.com")
|
|
96
|
+
container = make_async_container(
|
|
97
|
+
ClientwrightProvider("httpx", login, component="github-oauth", client_type=httpx.AsyncClient),
|
|
98
|
+
ClientwrightProvider("httpx", api, component="github-api", client_type=httpx.AsyncClient),
|
|
99
|
+
ServiceProvider(),
|
|
100
|
+
)
|
|
101
|
+
github = await container.get(GitHubOAuthProvider)
|
|
102
|
+
login_handle = await container.get(ClientHandle[Any], component="github-oauth")
|
|
103
|
+
api_handle = await container.get(ClientHandle[Any], component="github-api")
|
|
104
|
+
assert github.login_client is login_handle.client
|
|
105
|
+
assert github.api_client is api_handle.client
|
|
106
|
+
await container.close()
|
|
107
|
+
assert github.login_client.is_closed
|
|
108
|
+
assert github.api_client.is_closed
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def test__runtime_built_by_provider__moves_the_circuit_state_gauge() -> None:
|
|
112
|
+
prometheus_client = pytest.importorskip("prometheus_client", reason="requires the [metrics] extra")
|
|
113
|
+
from clientwright.adapters.observability import PrometheusClientMetrics # noqa: PLC0415 - behind the extra
|
|
114
|
+
|
|
115
|
+
registry = prometheus_client.CollectorRegistry()
|
|
116
|
+
config = clientwright.ClientConfig(service_name="di-gauge", circuit_breaker=CircuitBreakerConfig(fail_threshold=1))
|
|
117
|
+
deps = AdapterDeps(metrics=PrometheusClientMetrics(registry=registry))
|
|
118
|
+
container = make_async_container(ClientwrightProvider("httpx", config, deps))
|
|
119
|
+
handle = await container.get(ClientHandle[Any])
|
|
120
|
+
assert handle.runtime.circuits is not None
|
|
121
|
+
handle.runtime.circuits.record("https://wh.example.com:443", Outcome(kind=FailureKind.STATUS, status_code=503))
|
|
122
|
+
labels = {"service": "di-gauge", "adapter": "httpx", "key": "https://wh.example.com:443"}
|
|
123
|
+
assert registry.get_sample_value(names.CIRCUIT_STATE, labels) == 2.0 # open
|
|
124
|
+
await container.close()
|
|
@@ -27,6 +27,7 @@ from clientwright import (
|
|
|
27
27
|
TlsConfig,
|
|
28
28
|
UnsupportedPolicy,
|
|
29
29
|
build_sync,
|
|
30
|
+
client_config_from_settings,
|
|
30
31
|
)
|
|
31
32
|
from clientwright.contrib import settings as settings_module
|
|
32
33
|
from clientwright.contrib.settings import (
|
|
@@ -203,6 +204,27 @@ def test__bad_values__fail_at_load_with_the_field_path() -> None:
|
|
|
203
204
|
BaseProxySettings(url="http://proxy:3128", from_env=True)
|
|
204
205
|
|
|
205
206
|
|
|
207
|
+
# --- the legacy converter ---
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test__the_legacy_converter__passes_a_base_client_settings_through_to_to_config() -> None:
|
|
211
|
+
settings = BaseClientSettings(
|
|
212
|
+
base_url="https://auth.example.com",
|
|
213
|
+
pool={"max_connections": 7},
|
|
214
|
+
retry={"multiplier": 3.0},
|
|
215
|
+
tls={"verify": False},
|
|
216
|
+
proxy={"url": "http://proxy:3128"},
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
config = client_config_from_settings(settings, "auth-orchestrator")
|
|
220
|
+
|
|
221
|
+
assert config == settings.to_config("auth-orchestrator")
|
|
222
|
+
assert config.pool.max_connections == 7
|
|
223
|
+
assert config.retry is not None and config.retry.multiplier == 3.0
|
|
224
|
+
assert config.tls == TlsConfig(verify=False)
|
|
225
|
+
assert config.proxy == ProxyConfig(url="http://proxy:3128")
|
|
226
|
+
|
|
227
|
+
|
|
206
228
|
def test__a_config_from_settings__builds_the_native_client() -> None:
|
|
207
229
|
httpx = pytest.importorskip("httpx", reason="requires the [httpx] extra")
|
|
208
230
|
config = BaseClientSettings(base_url="https://api.example.com", pool={"http2": False}).to_config("api")
|
|
@@ -32,3 +32,13 @@ def test__structural_settings__mapped_into_config() -> None:
|
|
|
32
32
|
assert config.retry is not None and config.retry.max_attempts == 5
|
|
33
33
|
assert config.circuit_breaker is None
|
|
34
34
|
assert config.observability.tracing is False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test__settings_with_to_config__are_passed_through_to_it() -> None:
|
|
38
|
+
class Settings:
|
|
39
|
+
def to_config(self, service_name: str) -> clientwright.ClientConfig:
|
|
40
|
+
return clientwright.ClientConfig(service_name=service_name, base_url="https://api.example.com")
|
|
41
|
+
|
|
42
|
+
config = clientwright.client_config_from_settings(Settings(), "svc")
|
|
43
|
+
assert config == clientwright.ClientConfig(service_name="svc", base_url="https://api.example.com")
|
|
44
|
+
assert isinstance(Settings(), clientwright.SupportsToConfig)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.0" # x-release-please-version
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
"""Dishka integration (``[dishka]`` extra): APP-scope runtime, leak-free lifecycle.
|
|
2
|
-
|
|
3
|
-
Two lessons from the legacy kit are structural here:
|
|
4
|
-
|
|
5
|
-
- ``ClientRuntime`` (circuits, retry budgets, per-origin limiters) is APP
|
|
6
|
-
scope. A REQUEST-scoped runtime resets breaker state on every request and
|
|
7
|
-
turns the circuit breaker into decoration.
|
|
8
|
-
- The client is provided by a GENERATOR provide whose finally closes it - the
|
|
9
|
-
legacy provider parked ``aclose`` on an AsyncExitStack that nothing ever
|
|
10
|
-
closed, leaking a client per request.
|
|
11
|
-
|
|
12
|
-
Usage::
|
|
13
|
-
|
|
14
|
-
container = make_async_container(
|
|
15
|
-
ClientwrightProvider("httpx", config),
|
|
16
|
-
...,
|
|
17
|
-
)
|
|
18
|
-
handle = await container.get(ClientHandle)
|
|
19
|
-
client: httpx.AsyncClient = handle.client
|
|
20
|
-
|
|
21
|
-
Known limitation: the ``http_client_circuit_state`` gauge is wired when the
|
|
22
|
-
ADAPTER builds the runtime; a runtime built here (to be shared) has no
|
|
23
|
-
telemetry listener, so state-change events are not exported as a gauge.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
from __future__ import annotations
|
|
27
|
-
|
|
28
|
-
from collections.abc import AsyncIterator
|
|
29
|
-
from dataclasses import replace
|
|
30
|
-
from typing import Any
|
|
31
|
-
|
|
32
|
-
try:
|
|
33
|
-
from dishka import Provider, Scope, provide
|
|
34
|
-
except ImportError as exc: # pragma: no cover - exercised only without the extra
|
|
35
|
-
raise ImportError("Dishka integration requires clientwright[dishka]; install it.") from exc
|
|
36
|
-
|
|
37
|
-
from ..core.config import ClientConfig
|
|
38
|
-
from ..core.contracts.adapter import AdapterDeps, default_deps
|
|
39
|
-
from ..core.plan import ClientHandle, ClientRuntime
|
|
40
|
-
from ..core.registry import resolve_adapter
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class ClientwrightProvider(Provider):
|
|
44
|
-
"""One async native client with an APP-scope runtime and guaranteed close.
|
|
45
|
-
|
|
46
|
-
For several upstreams, instantiate one provider per upstream in separate
|
|
47
|
-
containers, or subclass and add typed aliases (e.g. a provider returning
|
|
48
|
-
``httpx.AsyncClient`` from the handle) for ergonomic injection.
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
scope = Scope.APP
|
|
52
|
-
|
|
53
|
-
def __init__(self, adapter: str, config: ClientConfig, deps: AdapterDeps | None = None) -> None:
|
|
54
|
-
super().__init__()
|
|
55
|
-
self._adapter = adapter
|
|
56
|
-
self._config = config
|
|
57
|
-
self._deps = deps or default_deps()
|
|
58
|
-
|
|
59
|
-
@provide
|
|
60
|
-
def client_runtime(self) -> ClientRuntime:
|
|
61
|
-
if self._deps.runtime is not None:
|
|
62
|
-
return self._deps.runtime
|
|
63
|
-
return ClientRuntime.for_config(self._config, clock=self._deps.clock)
|
|
64
|
-
|
|
65
|
-
@provide
|
|
66
|
-
async def client_handle(self, runtime: ClientRuntime) -> AsyncIterator[ClientHandle[Any]]:
|
|
67
|
-
adapter = resolve_adapter(self._adapter)()
|
|
68
|
-
handle: ClientHandle[Any] = adapter.build_async(self._config, replace(self._deps, runtime=runtime))
|
|
69
|
-
try:
|
|
70
|
-
yield handle
|
|
71
|
-
finally:
|
|
72
|
-
# The whole point: close travels WITH the provide, not on a stack
|
|
73
|
-
# nobody closes.
|
|
74
|
-
if handle.aclose is not None:
|
|
75
|
-
await handle.aclose()
|
|
76
|
-
elif handle.close is not None:
|
|
77
|
-
handle.close()
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
__all__ = ["ClientwrightProvider"]
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"""contrib units: the dishka provider lifecycle."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
|
-
pytest.importorskip("dishka", reason="requires the [dishka] extra")
|
|
10
|
-
httpx = pytest.importorskip("httpx", reason="requires the [httpx] extra")
|
|
11
|
-
|
|
12
|
-
from dishka import make_async_container # noqa: E402
|
|
13
|
-
|
|
14
|
-
import clientwright # noqa: E402
|
|
15
|
-
from clientwright.contrib.dishka import ClientwrightProvider # noqa: E402
|
|
16
|
-
from clientwright.core.plan import ClientHandle, ClientRuntime # noqa: E402
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
async def test__provider__builds_client_and_closes_it_with_container() -> None:
|
|
20
|
-
config = clientwright.ClientConfig(service_name="di-test")
|
|
21
|
-
container = make_async_container(ClientwrightProvider("httpx", config))
|
|
22
|
-
handle = await container.get(ClientHandle[Any])
|
|
23
|
-
assert type(handle.client) is httpx.AsyncClient
|
|
24
|
-
runtime = await container.get(ClientRuntime)
|
|
25
|
-
assert handle.runtime is runtime # APP-scope runtime shared through DI
|
|
26
|
-
assert not handle.client.is_closed
|
|
27
|
-
await container.close()
|
|
28
|
-
assert handle.client.is_closed # the generator provide's finally ran
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|