clientwright 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clientwright-0.1.0 → clientwright-0.2.0}/CHANGELOG.md +11 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/PKG-INFO +1 -1
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/__init__.py +2 -0
- clientwright-0.2.0/clientwright/__version__.py +1 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/config.py +21 -3
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/contracts/__init__.py +2 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/contracts/observability.py +19 -1
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/telemetry/emitter.py +15 -2
- clientwright-0.2.0/tests/unit/core/telemetry/test_masking.py +140 -0
- clientwright-0.1.0/clientwright/__version__.py +0 -1
- {clientwright-0.1.0 → clientwright-0.2.0}/.gitignore +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/LICENSE +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/README.md +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/_httpx_shared.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/_lazy.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/_imports.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/adapter.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/capabilities.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/classify.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/errors.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/middleware.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/normalize.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/options.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/trace.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/views.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/_imports.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/adapter.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/capabilities.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/classify.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/errors.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/normalize.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/normalize_sync.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/transport.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/views.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/_imports.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/adapter.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/capabilities.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/classify.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/errors.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/normalize.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/normalize_sync.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/transport.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/views.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/observability/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/observability/_metrics/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/observability/_metrics/prometheus.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/observability/_tracing/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/observability/_tracing/otel.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/_imports.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/adapter.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/capabilities.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/classify.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/errors.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/normalize.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/views.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/_imports.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/adapter.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/capabilities.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/classify.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/errors.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/normalize.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/views.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/contrib/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/contrib/deadline.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/contrib/dishka.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/balancer/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/balancer/policy.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/capabilities.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/contracts/adapter.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/contracts/context.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/contracts/message.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/contracts/settings.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/engine/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/engine/aio.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/engine/base.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/engine/redirects.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/engine/suppress.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/engine/sync.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/errors.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/model.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/native.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/options.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/plan.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/policy/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/policy/budget.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/policy/circuit.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/policy/concurrency.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/policy/retry.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/policy/timeout.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/registry.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/telemetry/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/telemetry/names.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/telemetry/null.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/telemetry/redaction.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/testing/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/testing/doubles.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/testing/origin.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/py.typed +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/pyproject.toml +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/conftest.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/helpers/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/helpers/drivers.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/helpers/engine.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/helpers/views.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/adapters/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/adapters/test_aiohttp.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/adapters/test_httpx2.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/adapters/test_httpx_async.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/adapters/test_httpx_sync.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/adapters/test_requests.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/adapters/test_urllib3.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/chaos/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/chaos/test_faults.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/chaos/test_origin_faults.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/conftest.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/contrib/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/contrib/test_deadline.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/contrib/test_dishka.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/parity/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/parity/test_semantics.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/live/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/live/conftest.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/live/test_public_endpoints.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/live/test_tls_and_dns.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/aiohttp/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/aiohttp/test_build.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/aiohttp/test_normalize.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/aiohttp/test_trace.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/aiohttp/test_units.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/conftest.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/httpx/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/httpx/test_units.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/httpx2/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/httpx2/test_units.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/observability/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/observability/test_exports.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/observability/test_otel.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/observability/test_prometheus.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/requests/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/requests/test_adapter.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/requests/test_normalize.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/requests/test_units.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/test_bare_install.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/test_httpx_family_parity.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/test_httpx_shared.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/urllib3/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/urllib3/test_adapter.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/urllib3/test_normalize.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/urllib3/test_units.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/contrib/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/contrib/test_deadline.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/contrib/test_deadline_context.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/contrib/test_dishka.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/engine/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/engine/test_aio.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/engine/test_base.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/engine/test_redirects.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/engine/test_suppress.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/engine/test_sync.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_budget.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_circuit.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_circuit_properties.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_concurrency.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_retry.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_retry_properties.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_timeout.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_timeout_properties.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/telemetry/__init__.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/telemetry/test_emitter.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/telemetry/test_redaction.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/telemetry/test_redaction_properties.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_balancer.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_capabilities.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_capabilities_properties.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_config.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_enum_coercion.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_errors.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_model.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_options.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_plan.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_registry.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_settings.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/test_docs_snippets.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/test_sdk_imports_are_guarded.py +0 -0
- {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/test_version.py +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0](https://github.com/bedrock-python/clientwright/compare/clientwright-v0.1.0...clientwright-v0.2.0) (2026-08-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* add url_masker seam for value-level PII masking, drop dead sensitive_headers knob
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* add url_masker seam for value-level PII masking, drop dead sensitive_headers knob ([90f74fb](https://github.com/bedrock-python/clientwright/commit/90f74fbecaafb77b2405c4d06804e3d7dad91b34))
|
|
13
|
+
|
|
3
14
|
## 0.1.0 (2026-08-14)
|
|
4
15
|
|
|
5
16
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: clientwright
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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/
|
|
@@ -52,6 +52,7 @@ from .core.contracts import (
|
|
|
52
52
|
ClientSettingsProtocol,
|
|
53
53
|
DeadlineSource,
|
|
54
54
|
HeaderProvider,
|
|
55
|
+
MaskerProtocol,
|
|
55
56
|
RetrySettingsProtocol,
|
|
56
57
|
SpanProtocol,
|
|
57
58
|
TracerProtocol,
|
|
@@ -136,6 +137,7 @@ __all__ = [
|
|
|
136
137
|
"DurationBoundary",
|
|
137
138
|
"FailureKind",
|
|
138
139
|
"HeaderProvider",
|
|
140
|
+
"MaskerProtocol",
|
|
139
141
|
"Maybe",
|
|
140
142
|
"NativeConfigError",
|
|
141
143
|
"NativeOptions",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0" # x-release-please-version
|
|
@@ -15,10 +15,13 @@ import logging
|
|
|
15
15
|
from collections.abc import Mapping
|
|
16
16
|
from dataclasses import dataclass, field
|
|
17
17
|
from enum import StrEnum
|
|
18
|
-
from typing import Final
|
|
18
|
+
from typing import TYPE_CHECKING, Final
|
|
19
19
|
|
|
20
20
|
from .model import IDEMPOTENT_METHODS, CircuitKey, FailureKind
|
|
21
21
|
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from .contracts.observability import MaskerProtocol
|
|
24
|
+
|
|
22
25
|
|
|
23
26
|
class _Unset:
|
|
24
27
|
"""Singleton sentinel distinguishing "user set this" from "default applies"."""
|
|
@@ -106,6 +109,9 @@ DEFAULT_TRIP_KINDS: Final = frozenset(
|
|
|
106
109
|
}
|
|
107
110
|
)
|
|
108
111
|
|
|
112
|
+
# Not a config knob: clientwright never emits headers into logs or spans, so
|
|
113
|
+
# there is nothing for a header list to protect. Kept as a public default for
|
|
114
|
+
# services that log headers themselves via ``redact_headers``.
|
|
109
115
|
DEFAULT_SENSITIVE_HEADERS: Final = frozenset(
|
|
110
116
|
{
|
|
111
117
|
"authorization",
|
|
@@ -261,14 +267,26 @@ _INFO_LEVEL: Final = logging.INFO
|
|
|
261
267
|
|
|
262
268
|
@dataclass(frozen=True, slots=True)
|
|
263
269
|
class ObservabilityConfig:
|
|
264
|
-
"""Which telemetry channels are active; only knobs that actually work exist here.
|
|
270
|
+
"""Which telemetry channels are active; only knobs that actually work exist here.
|
|
271
|
+
|
|
272
|
+
Two-stage URL scrubbing before anything reaches a log line or a span:
|
|
273
|
+
``sensitive_query_params`` redacts by parameter *name*, then ``url_masker``
|
|
274
|
+
(if set) sees the whole redacted URL and may scrub PII by *value* - the
|
|
275
|
+
email in a path segment that no name list can catch. See
|
|
276
|
+
:class:`~clientwright.core.contracts.observability.MaskerProtocol` for the
|
|
277
|
+
failure contract.
|
|
278
|
+
"""
|
|
265
279
|
|
|
266
280
|
logging: bool = True
|
|
267
281
|
metrics: bool = True
|
|
268
282
|
tracing: bool = True
|
|
269
283
|
success_log_level: int = _INFO_LEVEL
|
|
270
|
-
sensitive_headers: frozenset[str] = DEFAULT_SENSITIVE_HEADERS
|
|
271
284
|
sensitive_query_params: frozenset[str] = DEFAULT_SENSITIVE_QUERY_PARAMS
|
|
285
|
+
url_masker: MaskerProtocol | None = None
|
|
286
|
+
|
|
287
|
+
def __post_init__(self) -> None:
|
|
288
|
+
if self.url_masker is not None and not callable(self.url_masker):
|
|
289
|
+
raise ValueError(f"observability.url_masker must be callable or None, got {self.url_masker!r}")
|
|
272
290
|
|
|
273
291
|
|
|
274
292
|
@dataclass(frozen=True, slots=True)
|
|
@@ -9,6 +9,7 @@ from .message import RequestView as RequestView
|
|
|
9
9
|
from .message import ResponseView as ResponseView
|
|
10
10
|
from .message import SyncNormalizer as SyncNormalizer
|
|
11
11
|
from .observability import ClientMetricsProtocol as ClientMetricsProtocol
|
|
12
|
+
from .observability import MaskerProtocol as MaskerProtocol
|
|
12
13
|
from .observability import SpanProtocol as SpanProtocol
|
|
13
14
|
from .observability import TracerProtocol as TracerProtocol
|
|
14
15
|
from .settings import CircuitBreakerSettingsProtocol as CircuitBreakerSettingsProtocol
|
|
@@ -24,6 +25,7 @@ __all__ = [
|
|
|
24
25
|
"ClientSettingsProtocol",
|
|
25
26
|
"DeadlineSource",
|
|
26
27
|
"HeaderProvider",
|
|
28
|
+
"MaskerProtocol",
|
|
27
29
|
"RequestView",
|
|
28
30
|
"ResponseView",
|
|
29
31
|
"RetrySettingsProtocol",
|
|
@@ -69,6 +69,24 @@ class ClientMetricsProtocol(Protocol):
|
|
|
69
69
|
def record_uninstrumented_call(self, *, service: str, adapter: str, seam: str) -> None: ...
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
@runtime_checkable
|
|
73
|
+
class MaskerProtocol(Protocol):
|
|
74
|
+
"""Value-level masker applied to telemetry strings before they are emitted.
|
|
75
|
+
|
|
76
|
+
The key-based redaction (``sensitive_query_params``) runs first; the masker
|
|
77
|
+
then sees the whole string and may scrub PII that key lists cannot reach -
|
|
78
|
+
an email in a path segment, a card number in a free-form value. Any
|
|
79
|
+
``(str) -> str`` callable qualifies: a regex substitution, a
|
|
80
|
+
format-preserving masker, a locally hosted NER model.
|
|
81
|
+
|
|
82
|
+
Contract: synchronous, and cheap enough to run 2-3 times per HTTP call.
|
|
83
|
+
A raising masker never breaks the request - the emitter replaces the whole
|
|
84
|
+
value with ``[redacted]`` (fail closed) and warns once per client.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __call__(self, value: str) -> str: ...
|
|
88
|
+
|
|
89
|
+
|
|
72
90
|
@runtime_checkable
|
|
73
91
|
class SpanProtocol(Protocol):
|
|
74
92
|
"""One client span; ended exactly once, in a finally block."""
|
|
@@ -89,4 +107,4 @@ class TracerProtocol(Protocol):
|
|
|
89
107
|
def inject_context(self, headers: MutableMapping[str, str]) -> None: ...
|
|
90
108
|
|
|
91
109
|
|
|
92
|
-
__all__ = ["ClientMetricsProtocol", "SpanProtocol", "TracerProtocol"]
|
|
110
|
+
__all__ = ["ClientMetricsProtocol", "MaskerProtocol", "SpanProtocol", "TracerProtocol"]
|
|
@@ -15,7 +15,7 @@ from ..contracts.observability import ClientMetricsProtocol, SpanProtocol, Trace
|
|
|
15
15
|
from ..model import Attempt, Outcome, RequestInfo
|
|
16
16
|
from .names import OUTCOME_SUCCESS, ROUTE_UNKNOWN, STATUS_NONE
|
|
17
17
|
from .null import NullMetrics, NullTracer
|
|
18
|
-
from .redaction import redact_url
|
|
18
|
+
from .redaction import REDACTED, redact_url
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def outcome_label(outcome: Outcome) -> str:
|
|
@@ -57,13 +57,26 @@ class ClientTelemetry:
|
|
|
57
57
|
self._tracer: TracerProtocol = tracer if (tracer and config.tracing) else NullTracer()
|
|
58
58
|
self._logger = logging.getLogger(f"clientwright.client.{service}")
|
|
59
59
|
self._log_enabled = config.logging
|
|
60
|
+
self._masker_warned = False
|
|
60
61
|
|
|
61
62
|
@property
|
|
62
63
|
def tracer(self) -> TracerProtocol:
|
|
63
64
|
return self._tracer
|
|
64
65
|
|
|
65
66
|
def _safe_url(self, info: RequestInfo) -> str:
|
|
66
|
-
|
|
67
|
+
url = redact_url(info.url, self._config.sensitive_query_params)
|
|
68
|
+
masker = self._config.url_masker
|
|
69
|
+
if masker is None:
|
|
70
|
+
return url
|
|
71
|
+
# Fail closed: a broken masker must not publish the raw URL, and must
|
|
72
|
+
# not break the request either. Warn once per client, not per call.
|
|
73
|
+
try:
|
|
74
|
+
return masker(url)
|
|
75
|
+
except Exception:
|
|
76
|
+
if not self._masker_warned:
|
|
77
|
+
self._masker_warned = True
|
|
78
|
+
self._logger.warning("url_masker raised; emitting [redacted] instead of the URL", exc_info=True)
|
|
79
|
+
return REDACTED
|
|
67
80
|
|
|
68
81
|
def call_start(self, info: RequestInfo, started: float) -> CallObservation:
|
|
69
82
|
span = self._tracer.start_span(
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Value-level URL masking: the MaskerProtocol seam in the emitter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
from urllib.parse import quote_plus
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from clientwright.core.config import ObservabilityConfig
|
|
13
|
+
from clientwright.core.model import Outcome, RequestInfo
|
|
14
|
+
from clientwright.core.telemetry.emitter import ClientTelemetry
|
|
15
|
+
from clientwright.core.telemetry.redaction import REDACTED
|
|
16
|
+
|
|
17
|
+
# redact_url reassembles the query with urlencode, so the marker comes out
|
|
18
|
+
# percent-encoded inside URLs.
|
|
19
|
+
REDACTED_IN_URL = quote_plus(REDACTED)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from collections.abc import Mapping
|
|
23
|
+
|
|
24
|
+
INFO = RequestInfo(
|
|
25
|
+
method="GET",
|
|
26
|
+
origin="https://a:443",
|
|
27
|
+
url="https://a/users/alex@example.com/orders?token=x",
|
|
28
|
+
route="/users/{id}/orders",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class RecordingSpan:
|
|
34
|
+
attributes: dict[str, object] = field(default_factory=dict)
|
|
35
|
+
ended: bool = False
|
|
36
|
+
|
|
37
|
+
def set_attribute(self, key: str, value: str | int | float | bool) -> None:
|
|
38
|
+
self.attributes[key] = value
|
|
39
|
+
|
|
40
|
+
def record_failure(self, description: str) -> None:
|
|
41
|
+
self.attributes["failure"] = description
|
|
42
|
+
|
|
43
|
+
def end(self) -> None:
|
|
44
|
+
self.ended = True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(slots=True)
|
|
48
|
+
class RecordingTracer:
|
|
49
|
+
spans: list[RecordingSpan] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
def start_span(self, name: str, *, attributes: Mapping[str, str | int | float | bool]) -> RecordingSpan:
|
|
52
|
+
span = RecordingSpan(attributes=dict(attributes))
|
|
53
|
+
self.spans.append(span)
|
|
54
|
+
return span
|
|
55
|
+
|
|
56
|
+
def inject_context(self, headers: dict[str, str]) -> None: # pragma: no cover - unused
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def telemetry(config: ObservabilityConfig, tracer: RecordingTracer | None = None) -> ClientTelemetry:
|
|
61
|
+
return ClientTelemetry(service="svc", adapter="fake", seam="test", config=config, metrics=None, tracer=tracer)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# --- masker applied ---
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test__masker__runs_after_query_redaction_and_reaches_span_and_log(caplog: pytest.LogCaptureFixture) -> None:
|
|
68
|
+
seen: list[str] = []
|
|
69
|
+
|
|
70
|
+
def masker(value: str) -> str:
|
|
71
|
+
seen.append(value)
|
|
72
|
+
return value.replace("alex@example.com", "***")
|
|
73
|
+
|
|
74
|
+
tracer = RecordingTracer()
|
|
75
|
+
config = ObservabilityConfig(sensitive_query_params=frozenset({"token"}), url_masker=masker)
|
|
76
|
+
emitter = telemetry(config, tracer)
|
|
77
|
+
|
|
78
|
+
with caplog.at_level(logging.DEBUG, logger="clientwright.client.svc"):
|
|
79
|
+
observation = emitter.call_start(INFO, started=0.0)
|
|
80
|
+
emitter.call_end(observation, INFO, Outcome(kind=None, status_code=200), duration=0.1)
|
|
81
|
+
|
|
82
|
+
# The masker saw the query already redacted by name - never the raw token.
|
|
83
|
+
assert seen and all("token=x" not in value for value in seen)
|
|
84
|
+
assert all(f"token={REDACTED_IN_URL}" in value for value in seen)
|
|
85
|
+
# PII in the path segment is gone from the span and from every log record.
|
|
86
|
+
masked = f"https://a/users/***/orders?token={REDACTED_IN_URL}"
|
|
87
|
+
assert tracer.spans[0].attributes["url.full"] == masked
|
|
88
|
+
urls = [record.url for record in caplog.records if hasattr(record, "url")]
|
|
89
|
+
assert urls and all(url == masked for url in urls)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test__masker_none__url_unchanged_beyond_query_redaction() -> None:
|
|
93
|
+
tracer = RecordingTracer()
|
|
94
|
+
config = ObservabilityConfig(sensitive_query_params=frozenset({"token"}))
|
|
95
|
+
emitter = telemetry(config, tracer)
|
|
96
|
+
observation = emitter.call_start(INFO, started=0.0)
|
|
97
|
+
emitter.call_end(observation, INFO, Outcome(kind=None, status_code=200), duration=0.1)
|
|
98
|
+
assert tracer.spans[0].attributes["url.full"] == f"https://a/users/alex@example.com/orders?token={REDACTED_IN_URL}"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --- fail closed ---
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test__masker_raises__emits_redacted_and_never_breaks_the_call(caplog: pytest.LogCaptureFixture) -> None:
|
|
105
|
+
def broken(value: str) -> str:
|
|
106
|
+
raise RuntimeError("model not loaded")
|
|
107
|
+
|
|
108
|
+
tracer = RecordingTracer()
|
|
109
|
+
emitter = telemetry(ObservabilityConfig(url_masker=broken), tracer)
|
|
110
|
+
|
|
111
|
+
with caplog.at_level(logging.DEBUG, logger="clientwright.client.svc"):
|
|
112
|
+
observation = emitter.call_start(INFO, started=0.0)
|
|
113
|
+
emitter.call_end(observation, INFO, Outcome(kind=None, status_code=200), duration=0.1)
|
|
114
|
+
|
|
115
|
+
# Fail closed: the raw URL is not published anywhere.
|
|
116
|
+
assert tracer.spans[0].attributes["url.full"] == REDACTED
|
|
117
|
+
assert all("alex@example.com" not in str(getattr(record, "url", "")) for record in caplog.records)
|
|
118
|
+
assert tracer.spans[0].ended
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test__masker_raises__warns_once_per_client_not_per_call(caplog: pytest.LogCaptureFixture) -> None:
|
|
122
|
+
def broken(value: str) -> str:
|
|
123
|
+
raise RuntimeError("boom")
|
|
124
|
+
|
|
125
|
+
emitter = telemetry(ObservabilityConfig(url_masker=broken))
|
|
126
|
+
with caplog.at_level(logging.WARNING, logger="clientwright.client.svc"):
|
|
127
|
+
for _ in range(3):
|
|
128
|
+
observation = emitter.call_start(INFO, started=0.0)
|
|
129
|
+
emitter.call_end(observation, INFO, Outcome(kind=None, status_code=200), duration=0.1)
|
|
130
|
+
warnings = [record for record in caplog.records if record.levelno == logging.WARNING]
|
|
131
|
+
assert len(warnings) == 1
|
|
132
|
+
assert "url_masker" in warnings[0].getMessage()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# --- config validation ---
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test__url_masker_not_callable__rejected_at_config_time() -> None:
|
|
139
|
+
with pytest.raises(ValueError, match="url_masker must be callable"):
|
|
140
|
+
ObservabilityConfig(url_masker="not-a-callable") # type: ignore[arg-type]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0" # x-release-please-version
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|
{clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/observability/_metrics/__init__.py
RENAMED
|
File without changes
|
{clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/observability/_metrics/prometheus.py
RENAMED
|
File without changes
|
{clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/observability/_tracing/__init__.py
RENAMED
|
File without changes
|
{clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/observability/_tracing/otel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|
{clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/observability/test_prometheus.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|
{clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/telemetry/test_redaction_properties.py
RENAMED
|
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
|