clientwright 0.2.0__tar.gz → 0.2.2__tar.gz

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