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.
Files changed (198) hide show
  1. {clientwright-0.1.0 → clientwright-0.2.0}/CHANGELOG.md +11 -0
  2. {clientwright-0.1.0 → clientwright-0.2.0}/PKG-INFO +1 -1
  3. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/__init__.py +2 -0
  4. clientwright-0.2.0/clientwright/__version__.py +1 -0
  5. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/config.py +21 -3
  6. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/contracts/__init__.py +2 -0
  7. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/contracts/observability.py +19 -1
  8. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/telemetry/emitter.py +15 -2
  9. clientwright-0.2.0/tests/unit/core/telemetry/test_masking.py +140 -0
  10. clientwright-0.1.0/clientwright/__version__.py +0 -1
  11. {clientwright-0.1.0 → clientwright-0.2.0}/.gitignore +0 -0
  12. {clientwright-0.1.0 → clientwright-0.2.0}/LICENSE +0 -0
  13. {clientwright-0.1.0 → clientwright-0.2.0}/README.md +0 -0
  14. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/__init__.py +0 -0
  15. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/_httpx_shared.py +0 -0
  16. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/_lazy.py +0 -0
  17. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/__init__.py +0 -0
  18. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/_imports.py +0 -0
  19. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/adapter.py +0 -0
  20. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/capabilities.py +0 -0
  21. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/classify.py +0 -0
  22. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/errors.py +0 -0
  23. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/middleware.py +0 -0
  24. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/normalize.py +0 -0
  25. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/options.py +0 -0
  26. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/trace.py +0 -0
  27. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/aiohttp/views.py +0 -0
  28. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/__init__.py +0 -0
  29. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/_imports.py +0 -0
  30. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/adapter.py +0 -0
  31. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/capabilities.py +0 -0
  32. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/classify.py +0 -0
  33. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/errors.py +0 -0
  34. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/normalize.py +0 -0
  35. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/normalize_sync.py +0 -0
  36. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/transport.py +0 -0
  37. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx/views.py +0 -0
  38. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/__init__.py +0 -0
  39. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/_imports.py +0 -0
  40. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/adapter.py +0 -0
  41. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/capabilities.py +0 -0
  42. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/classify.py +0 -0
  43. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/errors.py +0 -0
  44. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/normalize.py +0 -0
  45. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/normalize_sync.py +0 -0
  46. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/transport.py +0 -0
  47. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/httpx2/views.py +0 -0
  48. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/observability/__init__.py +0 -0
  49. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/observability/_metrics/__init__.py +0 -0
  50. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/observability/_metrics/prometheus.py +0 -0
  51. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/observability/_tracing/__init__.py +0 -0
  52. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/observability/_tracing/otel.py +0 -0
  53. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/__init__.py +0 -0
  54. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/_imports.py +0 -0
  55. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/adapter.py +0 -0
  56. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/capabilities.py +0 -0
  57. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/classify.py +0 -0
  58. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/errors.py +0 -0
  59. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/normalize.py +0 -0
  60. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/requests/views.py +0 -0
  61. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/__init__.py +0 -0
  62. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/_imports.py +0 -0
  63. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/adapter.py +0 -0
  64. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/capabilities.py +0 -0
  65. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/classify.py +0 -0
  66. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/errors.py +0 -0
  67. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/normalize.py +0 -0
  68. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/adapters/urllib3/views.py +0 -0
  69. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/contrib/__init__.py +0 -0
  70. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/contrib/deadline.py +0 -0
  71. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/contrib/dishka.py +0 -0
  72. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/__init__.py +0 -0
  73. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/balancer/__init__.py +0 -0
  74. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/balancer/policy.py +0 -0
  75. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/capabilities.py +0 -0
  76. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/contracts/adapter.py +0 -0
  77. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/contracts/context.py +0 -0
  78. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/contracts/message.py +0 -0
  79. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/contracts/settings.py +0 -0
  80. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/engine/__init__.py +0 -0
  81. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/engine/aio.py +0 -0
  82. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/engine/base.py +0 -0
  83. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/engine/redirects.py +0 -0
  84. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/engine/suppress.py +0 -0
  85. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/engine/sync.py +0 -0
  86. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/errors.py +0 -0
  87. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/model.py +0 -0
  88. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/native.py +0 -0
  89. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/options.py +0 -0
  90. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/plan.py +0 -0
  91. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/policy/__init__.py +0 -0
  92. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/policy/budget.py +0 -0
  93. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/policy/circuit.py +0 -0
  94. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/policy/concurrency.py +0 -0
  95. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/policy/retry.py +0 -0
  96. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/policy/timeout.py +0 -0
  97. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/registry.py +0 -0
  98. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/telemetry/__init__.py +0 -0
  99. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/telemetry/names.py +0 -0
  100. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/telemetry/null.py +0 -0
  101. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/telemetry/redaction.py +0 -0
  102. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/testing/__init__.py +0 -0
  103. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/testing/doubles.py +0 -0
  104. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/core/testing/origin.py +0 -0
  105. {clientwright-0.1.0 → clientwright-0.2.0}/clientwright/py.typed +0 -0
  106. {clientwright-0.1.0 → clientwright-0.2.0}/pyproject.toml +0 -0
  107. {clientwright-0.1.0 → clientwright-0.2.0}/tests/__init__.py +0 -0
  108. {clientwright-0.1.0 → clientwright-0.2.0}/tests/conftest.py +0 -0
  109. {clientwright-0.1.0 → clientwright-0.2.0}/tests/helpers/__init__.py +0 -0
  110. {clientwright-0.1.0 → clientwright-0.2.0}/tests/helpers/drivers.py +0 -0
  111. {clientwright-0.1.0 → clientwright-0.2.0}/tests/helpers/engine.py +0 -0
  112. {clientwright-0.1.0 → clientwright-0.2.0}/tests/helpers/views.py +0 -0
  113. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/__init__.py +0 -0
  114. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/adapters/__init__.py +0 -0
  115. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/adapters/test_aiohttp.py +0 -0
  116. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/adapters/test_httpx2.py +0 -0
  117. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/adapters/test_httpx_async.py +0 -0
  118. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/adapters/test_httpx_sync.py +0 -0
  119. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/adapters/test_requests.py +0 -0
  120. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/adapters/test_urllib3.py +0 -0
  121. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/chaos/__init__.py +0 -0
  122. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/chaos/test_faults.py +0 -0
  123. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/chaos/test_origin_faults.py +0 -0
  124. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/conftest.py +0 -0
  125. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/contrib/__init__.py +0 -0
  126. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/contrib/test_deadline.py +0 -0
  127. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/contrib/test_dishka.py +0 -0
  128. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/parity/__init__.py +0 -0
  129. {clientwright-0.1.0 → clientwright-0.2.0}/tests/integration/parity/test_semantics.py +0 -0
  130. {clientwright-0.1.0 → clientwright-0.2.0}/tests/live/__init__.py +0 -0
  131. {clientwright-0.1.0 → clientwright-0.2.0}/tests/live/conftest.py +0 -0
  132. {clientwright-0.1.0 → clientwright-0.2.0}/tests/live/test_public_endpoints.py +0 -0
  133. {clientwright-0.1.0 → clientwright-0.2.0}/tests/live/test_tls_and_dns.py +0 -0
  134. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/__init__.py +0 -0
  135. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/__init__.py +0 -0
  136. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/aiohttp/__init__.py +0 -0
  137. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/aiohttp/test_build.py +0 -0
  138. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/aiohttp/test_normalize.py +0 -0
  139. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/aiohttp/test_trace.py +0 -0
  140. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/aiohttp/test_units.py +0 -0
  141. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/conftest.py +0 -0
  142. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/httpx/__init__.py +0 -0
  143. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/httpx/test_units.py +0 -0
  144. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/httpx2/__init__.py +0 -0
  145. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/httpx2/test_units.py +0 -0
  146. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/observability/__init__.py +0 -0
  147. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/observability/test_exports.py +0 -0
  148. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/observability/test_otel.py +0 -0
  149. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/observability/test_prometheus.py +0 -0
  150. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/requests/__init__.py +0 -0
  151. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/requests/test_adapter.py +0 -0
  152. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/requests/test_normalize.py +0 -0
  153. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/requests/test_units.py +0 -0
  154. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/test_bare_install.py +0 -0
  155. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/test_httpx_family_parity.py +0 -0
  156. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/test_httpx_shared.py +0 -0
  157. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/urllib3/__init__.py +0 -0
  158. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/urllib3/test_adapter.py +0 -0
  159. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/urllib3/test_normalize.py +0 -0
  160. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/adapters/urllib3/test_units.py +0 -0
  161. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/contrib/__init__.py +0 -0
  162. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/contrib/test_deadline.py +0 -0
  163. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/contrib/test_deadline_context.py +0 -0
  164. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/contrib/test_dishka.py +0 -0
  165. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/__init__.py +0 -0
  166. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/engine/__init__.py +0 -0
  167. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/engine/test_aio.py +0 -0
  168. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/engine/test_base.py +0 -0
  169. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/engine/test_redirects.py +0 -0
  170. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/engine/test_suppress.py +0 -0
  171. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/engine/test_sync.py +0 -0
  172. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/__init__.py +0 -0
  173. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_budget.py +0 -0
  174. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_circuit.py +0 -0
  175. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_circuit_properties.py +0 -0
  176. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_concurrency.py +0 -0
  177. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_retry.py +0 -0
  178. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_retry_properties.py +0 -0
  179. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_timeout.py +0 -0
  180. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/policy/test_timeout_properties.py +0 -0
  181. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/telemetry/__init__.py +0 -0
  182. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/telemetry/test_emitter.py +0 -0
  183. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/telemetry/test_redaction.py +0 -0
  184. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/telemetry/test_redaction_properties.py +0 -0
  185. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_balancer.py +0 -0
  186. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_capabilities.py +0 -0
  187. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_capabilities_properties.py +0 -0
  188. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_config.py +0 -0
  189. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_enum_coercion.py +0 -0
  190. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_errors.py +0 -0
  191. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_model.py +0 -0
  192. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_options.py +0 -0
  193. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_plan.py +0 -0
  194. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_registry.py +0 -0
  195. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/core/test_settings.py +0 -0
  196. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/test_docs_snippets.py +0 -0
  197. {clientwright-0.1.0 → clientwright-0.2.0}/tests/unit/test_sdk_imports_are_guarded.py +0 -0
  198. {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.1.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
- return redact_url(info.url, self._config.sensitive_query_params)
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