clientwright 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. {clientwright-0.2.1 → clientwright-0.2.2}/CHANGELOG.md +8 -0
  2. {clientwright-0.2.1 → clientwright-0.2.2}/PKG-INFO +1 -1
  3. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/__init__.py +2 -0
  4. clientwright-0.2.2/clientwright/__version__.py +1 -0
  5. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/_httpx_shared.py +73 -14
  6. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/__init__.py +3 -0
  7. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/capabilities.py +6 -0
  8. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/errors.py +14 -1
  9. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/normalize.py +7 -3
  10. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/__init__.py +3 -0
  11. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/errors.py +7 -2
  12. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/normalize.py +2 -0
  13. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/normalize_sync.py +2 -0
  14. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/__init__.py +3 -0
  15. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/errors.py +7 -2
  16. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/normalize.py +2 -0
  17. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/normalize_sync.py +2 -0
  18. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/capabilities.py +10 -0
  19. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/normalize.py +4 -1
  20. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/capabilities.py +10 -0
  21. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/normalize.py +4 -1
  22. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/capabilities.py +1 -0
  23. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/config.py +2 -0
  24. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/contracts/message.py +9 -4
  25. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/engine/aio.py +52 -29
  26. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/engine/sync.py +35 -24
  27. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/errors.py +9 -0
  28. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/model.py +1 -0
  29. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/testing/origin.py +14 -0
  30. {clientwright-0.2.1 → clientwright-0.2.2}/tests/helpers/engine.py +4 -1
  31. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/adapters/test_aiohttp.py +28 -0
  32. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/adapters/test_httpx_async.py +35 -0
  33. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/adapters/test_httpx_sync.py +16 -0
  34. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/chaos/test_origin_faults.py +9 -0
  35. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/parity/test_semantics.py +21 -0
  36. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/aiohttp/test_units.py +10 -0
  37. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/httpx/test_units.py +9 -0
  38. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/test_httpx_shared.py +116 -2
  39. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/engine/test_aio.py +50 -12
  40. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_plan.py +9 -0
  41. clientwright-0.2.1/clientwright/__version__.py +0 -1
  42. {clientwright-0.2.1 → clientwright-0.2.2}/.gitignore +0 -0
  43. {clientwright-0.2.1 → clientwright-0.2.2}/LICENSE +0 -0
  44. {clientwright-0.2.1 → clientwright-0.2.2}/README.md +0 -0
  45. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/__init__.py +0 -0
  46. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/_lazy.py +0 -0
  47. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/_imports.py +0 -0
  48. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/adapter.py +0 -0
  49. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/classify.py +0 -0
  50. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/middleware.py +0 -0
  51. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/options.py +0 -0
  52. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/trace.py +0 -0
  53. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/aiohttp/views.py +0 -0
  54. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/_imports.py +0 -0
  55. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/adapter.py +0 -0
  56. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/capabilities.py +0 -0
  57. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/classify.py +0 -0
  58. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/transport.py +0 -0
  59. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx/views.py +0 -0
  60. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/_imports.py +0 -0
  61. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/adapter.py +0 -0
  62. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/capabilities.py +0 -0
  63. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/classify.py +0 -0
  64. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/transport.py +0 -0
  65. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/httpx2/views.py +0 -0
  66. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/observability/__init__.py +0 -0
  67. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/observability/_metrics/__init__.py +0 -0
  68. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/observability/_metrics/prometheus.py +0 -0
  69. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/observability/_tracing/__init__.py +0 -0
  70. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/observability/_tracing/otel.py +0 -0
  71. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/__init__.py +0 -0
  72. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/_imports.py +0 -0
  73. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/adapter.py +0 -0
  74. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/classify.py +0 -0
  75. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/errors.py +0 -0
  76. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/requests/views.py +0 -0
  77. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/__init__.py +0 -0
  78. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/_imports.py +0 -0
  79. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/adapter.py +0 -0
  80. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/classify.py +0 -0
  81. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/errors.py +0 -0
  82. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/adapters/urllib3/views.py +0 -0
  83. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/contrib/__init__.py +0 -0
  84. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/contrib/deadline.py +0 -0
  85. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/contrib/dishka.py +0 -0
  86. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/__init__.py +0 -0
  87. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/balancer/__init__.py +0 -0
  88. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/balancer/policy.py +0 -0
  89. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/contracts/__init__.py +0 -0
  90. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/contracts/adapter.py +0 -0
  91. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/contracts/context.py +0 -0
  92. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/contracts/observability.py +0 -0
  93. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/contracts/settings.py +0 -0
  94. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/engine/__init__.py +0 -0
  95. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/engine/base.py +0 -0
  96. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/engine/redirects.py +0 -0
  97. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/engine/suppress.py +0 -0
  98. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/native.py +0 -0
  99. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/options.py +0 -0
  100. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/plan.py +0 -0
  101. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/policy/__init__.py +0 -0
  102. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/policy/budget.py +0 -0
  103. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/policy/circuit.py +0 -0
  104. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/policy/concurrency.py +0 -0
  105. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/policy/retry.py +0 -0
  106. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/policy/timeout.py +0 -0
  107. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/registry.py +0 -0
  108. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/telemetry/__init__.py +0 -0
  109. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/telemetry/emitter.py +0 -0
  110. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/telemetry/names.py +0 -0
  111. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/telemetry/null.py +0 -0
  112. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/telemetry/redaction.py +0 -0
  113. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/testing/__init__.py +0 -0
  114. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/core/testing/doubles.py +0 -0
  115. {clientwright-0.2.1 → clientwright-0.2.2}/clientwright/py.typed +0 -0
  116. {clientwright-0.2.1 → clientwright-0.2.2}/pyproject.toml +0 -0
  117. {clientwright-0.2.1 → clientwright-0.2.2}/tests/__init__.py +0 -0
  118. {clientwright-0.2.1 → clientwright-0.2.2}/tests/conftest.py +0 -0
  119. {clientwright-0.2.1 → clientwright-0.2.2}/tests/helpers/__init__.py +0 -0
  120. {clientwright-0.2.1 → clientwright-0.2.2}/tests/helpers/drivers.py +0 -0
  121. {clientwright-0.2.1 → clientwright-0.2.2}/tests/helpers/telemetry.py +0 -0
  122. {clientwright-0.2.1 → clientwright-0.2.2}/tests/helpers/views.py +0 -0
  123. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/__init__.py +0 -0
  124. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/adapters/__init__.py +0 -0
  125. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/adapters/test_httpx2.py +0 -0
  126. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/adapters/test_requests.py +0 -0
  127. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/adapters/test_urllib3.py +0 -0
  128. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/chaos/__init__.py +0 -0
  129. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/chaos/test_faults.py +0 -0
  130. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/conftest.py +0 -0
  131. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/contrib/__init__.py +0 -0
  132. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/contrib/test_deadline.py +0 -0
  133. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/contrib/test_dishka.py +0 -0
  134. {clientwright-0.2.1 → clientwright-0.2.2}/tests/integration/parity/__init__.py +0 -0
  135. {clientwright-0.2.1 → clientwright-0.2.2}/tests/live/__init__.py +0 -0
  136. {clientwright-0.2.1 → clientwright-0.2.2}/tests/live/conftest.py +0 -0
  137. {clientwright-0.2.1 → clientwright-0.2.2}/tests/live/test_public_endpoints.py +0 -0
  138. {clientwright-0.2.1 → clientwright-0.2.2}/tests/live/test_tls_and_dns.py +0 -0
  139. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/__init__.py +0 -0
  140. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/__init__.py +0 -0
  141. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/aiohttp/__init__.py +0 -0
  142. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/aiohttp/test_build.py +0 -0
  143. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/aiohttp/test_normalize.py +0 -0
  144. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/aiohttp/test_trace.py +0 -0
  145. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/conftest.py +0 -0
  146. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/httpx/__init__.py +0 -0
  147. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/httpx2/__init__.py +0 -0
  148. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/httpx2/test_units.py +0 -0
  149. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/observability/__init__.py +0 -0
  150. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/observability/test_exports.py +0 -0
  151. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/observability/test_otel.py +0 -0
  152. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/observability/test_prometheus.py +0 -0
  153. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/requests/__init__.py +0 -0
  154. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/requests/test_adapter.py +0 -0
  155. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/requests/test_normalize.py +0 -0
  156. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/requests/test_units.py +0 -0
  157. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/test_bare_install.py +0 -0
  158. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/test_httpx_family_parity.py +0 -0
  159. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/urllib3/__init__.py +0 -0
  160. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/urllib3/test_adapter.py +0 -0
  161. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/urllib3/test_normalize.py +0 -0
  162. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/adapters/urllib3/test_units.py +0 -0
  163. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/contrib/__init__.py +0 -0
  164. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/contrib/test_deadline.py +0 -0
  165. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/contrib/test_deadline_context.py +0 -0
  166. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/contrib/test_dishka.py +0 -0
  167. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/__init__.py +0 -0
  168. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/engine/__init__.py +0 -0
  169. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/engine/test_base.py +0 -0
  170. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/engine/test_redirects.py +0 -0
  171. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/engine/test_suppress.py +0 -0
  172. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/engine/test_sync.py +0 -0
  173. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/__init__.py +0 -0
  174. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_budget.py +0 -0
  175. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_circuit.py +0 -0
  176. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_circuit_properties.py +0 -0
  177. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_concurrency.py +0 -0
  178. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_retry.py +0 -0
  179. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_retry_properties.py +0 -0
  180. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_timeout.py +0 -0
  181. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/policy/test_timeout_properties.py +0 -0
  182. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/telemetry/__init__.py +0 -0
  183. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/telemetry/test_emitter.py +0 -0
  184. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/telemetry/test_masking.py +0 -0
  185. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/telemetry/test_redaction.py +0 -0
  186. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/telemetry/test_redaction_properties.py +0 -0
  187. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_balancer.py +0 -0
  188. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_capabilities.py +0 -0
  189. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_capabilities_properties.py +0 -0
  190. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_config.py +0 -0
  191. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_enum_coercion.py +0 -0
  192. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_errors.py +0 -0
  193. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_model.py +0 -0
  194. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_options.py +0 -0
  195. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_registry.py +0 -0
  196. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/core/test_settings.py +0 -0
  197. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/test_docs_snippets.py +0 -0
  198. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/test_sdk_imports_are_guarded.py +0 -0
  199. {clientwright-0.2.1 → clientwright-0.2.2}/tests/unit/test_version.py +0 -0
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.2](https://github.com/bedrock-python/clientwright/compare/clientwright-v0.2.1...clientwright-v0.2.2) (2026-09-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * bound the response body by the total on the httpx family ([#28](https://github.com/bedrock-python/clientwright/issues/28)) ([5767b89](https://github.com/bedrock-python/clientwright/commit/5767b891595c7385887ecb40f2bdbf01ee615977)), closes [#25](https://github.com/bedrock-python/clientwright/issues/25)
9
+ * give the attempt ceiling its own retryable kind and adapter error ([#26](https://github.com/bedrock-python/clientwright/issues/26)) ([e36beeb](https://github.com/bedrock-python/clientwright/commit/e36beeba169355cb7b94a1484c032a77a12995b6)), closes [#24](https://github.com/bedrock-python/clientwright/issues/24)
10
+
3
11
  ## [0.2.1](https://github.com/bedrock-python/clientwright/compare/clientwright-v0.2.0...clientwright-v0.2.1) (2026-09-06)
4
12
 
5
13
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: clientwright
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: HTTP client platform: one resilience and observability core with adapters for httpx, aiohttp, requests, urllib3 and friends
5
5
  Project-URL: Repository, https://github.com/bedrock-python/clientwright
6
6
  Project-URL: Documentation, https://bedrock-python.github.io/clientwright/
@@ -60,6 +60,7 @@ from .core.contracts import (
60
60
  from .core.contracts.adapter import default_deps
61
61
  from .core.contracts.settings import client_config_from_settings
62
62
  from .core.errors import (
63
+ AttemptTimeoutError,
63
64
  CallError,
64
65
  CircuitOpenError,
65
66
  ClientwrightError,
@@ -117,6 +118,7 @@ __all__ = [
117
118
  "UNSET",
118
119
  "AdapterCapabilities",
119
120
  "AdapterDeps",
121
+ "AttemptTimeoutError",
120
122
  "CallError",
121
123
  "CallOptions",
122
124
  "CallerOverride",
@@ -0,0 +1 @@
1
+ __version__ = "0.2.2" # x-release-please-version
@@ -13,6 +13,7 @@ one extra and one import path.
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
+ import asyncio
16
17
  import ssl
17
18
  from collections.abc import Callable, Mapping, MutableMapping
18
19
  from typing import Any
@@ -28,10 +29,11 @@ from ..core.config import ClientConfig, RedirectMode, is_set, resolve
28
29
  from ..core.contracts.adapter import AdapterDeps
29
30
  from ..core.engine.aio import AsyncAttemptEngine
30
31
  from ..core.engine.sync import SyncAttemptEngine
31
- from ..core.errors import CallError, CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
32
+ from ..core.errors import AttemptTimeoutError, CallError, CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
32
33
  from ..core.model import IDEMPOTENT_METHODS, ConnMetrics, FailureKind, Outcome, RequestInfo, ResolvedTimeouts, origin_of
33
34
  from ..core.native import accepted_overrides, validate_native
34
35
  from ..core.plan import CallPlan, ClientHandle, ClientRuntime, compile_plan, register_handle
36
+ from ..core.policy.budget import Deadline
35
37
  from ..core.policy.timeout import base_timeouts
36
38
  from ..core.telemetry.emitter import ClientTelemetry
37
39
 
@@ -84,6 +86,7 @@ def capabilities_for(adapter: str) -> AdapterCapabilities:
84
86
  Capability.TIMEOUT_WRITE: Support.NATIVE,
85
87
  Capability.TIMEOUT_POOL: Support.NATIVE,
86
88
  Capability.DEADLINE_HARD: Support.EMULATED,
89
+ Capability.DEADLINE_COVERS_BODY: Support.EMULATED,
87
90
  Capability.POOL_LIMIT_TOTAL: Support.NATIVE,
88
91
  Capability.POOL_LIMIT_PER_HOST: Support.EMULATED,
89
92
  Capability.KEEPALIVE: Support.NATIVE,
@@ -105,6 +108,7 @@ def capabilities_for(adapter: str) -> AdapterCapabilities:
105
108
  FailureKind.READ_TIMEOUT,
106
109
  FailureKind.WRITE_TIMEOUT,
107
110
  FailureKind.POOL_TIMEOUT,
111
+ FailureKind.ATTEMPT_TIMEOUT,
108
112
  FailureKind.TOTAL_TIMEOUT,
109
113
  FailureKind.CONNECT_ERROR,
110
114
  FailureKind.TLS_ERROR,
@@ -120,9 +124,17 @@ def capabilities_for(adapter: str) -> AdapterCapabilities:
120
124
  collapses={FailureKind.DNS_ERROR: FailureKind.CONNECT_ERROR},
121
125
  notes={
122
126
  "deadline_hard": "Hard cancellation on the async client only; the sync client clamps phases (soft).",
127
+ "attempt_timeout": (
128
+ "Emitted by the async client only; the sync client drops the attempt ceiling, so a stall arrives as "
129
+ "read_timeout."
130
+ ),
123
131
  "pool_limit_per_host": "Emulated as a per-origin in-flight semaphore; limits requests, not connections.",
124
132
  "dns_error": f"{adapter} wraps DNS failures into ConnectError; they surface as connect_error.",
125
133
  "proxy_from_env": "Environment proxies are parsed into mounts; NO_PROXY entries match hosts literally.",
134
+ "deadline_covers_body": (
135
+ "The response body is inside the total: the async client bounds every chunk by the remaining budget, "
136
+ "the sync client refuses the next chunk once the budget is gone (soft)."
137
+ ),
126
138
  },
127
139
  )
128
140
 
@@ -140,8 +152,6 @@ def has_ssl_cause(exc: BaseException) -> bool:
140
152
 
141
153
  def classify_family_error(sdk: Any, exc: BaseException) -> FailureKind:
142
154
  """Exception -> FailureKind for any SDK exposing the httpx error names."""
143
- import asyncio # noqa: PLC0415 - stdlib, deferred to keep module import light
144
-
145
155
  if isinstance(exc, asyncio.CancelledError):
146
156
  return FailureKind.CANCELLED
147
157
  if isinstance(exc, sdk.ConnectTimeout):
@@ -173,6 +183,7 @@ def make_error_translator(
173
183
  circuit_cls: type[CircuitOpenError],
174
184
  deadline_cls: type[DeadlineExceededError],
175
185
  redirects_cls: type[TooManyRedirectsError],
186
+ attempt_cls: type[AttemptTimeoutError],
176
187
  ) -> Callable[[CallError], BaseException]:
177
188
  """Build the CallError -> dual-family translator from the bound classes."""
178
189
 
@@ -181,6 +192,8 @@ def make_error_translator(
181
192
  return circuit_cls(error.key, error.retry_after)
182
193
  if isinstance(error, DeadlineExceededError):
183
194
  return deadline_cls(error.total)
195
+ if isinstance(error, AttemptTimeoutError):
196
+ return attempt_cls(error.attempt)
184
197
  if isinstance(error, TooManyRedirectsError):
185
198
  return redirects_cls(error.hops)
186
199
  return error
@@ -344,12 +357,21 @@ class FamilyResponseView:
344
357
 
345
358
 
346
359
  class TimedStreamCore:
347
- """Times body consumption and reports 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
@@ -636,7 +695,7 @@ class FamilyAdapter:
636
695
  applied.add(Capability.HTTP2)
637
696
  if config.proxy is not None:
638
697
  applied.add(Capability.PROXY)
639
- emulated = {Capability.TIMEOUT_TOTAL}
698
+ emulated = {Capability.TIMEOUT_TOTAL, Capability.DEADLINE_COVERS_BODY}
640
699
  dropped: dict[Capability, str] = {}
641
700
  if sync:
642
701
  # The sync engine cannot cancel a blocked socket call: no hard
@@ -13,6 +13,7 @@ from .._lazy import lazy_attribute
13
13
  if TYPE_CHECKING:
14
14
  from .adapter import AiohttpAdapter as AiohttpAdapter
15
15
  from .capabilities import CAPABILITIES as CAPABILITIES
16
+ from .errors import AiohttpAttemptTimeoutError as AiohttpAttemptTimeoutError
16
17
  from .errors import AiohttpCircuitOpenError as AiohttpCircuitOpenError
17
18
  from .errors import AiohttpDeadlineExceededError as AiohttpDeadlineExceededError
18
19
  from .errors import AiohttpTooManyRedirectsError as AiohttpTooManyRedirectsError
@@ -22,6 +23,7 @@ if TYPE_CHECKING:
22
23
  _EXPORTS = {
23
24
  "CAPABILITIES": "capabilities",
24
25
  "AiohttpAdapter": "adapter",
26
+ "AiohttpAttemptTimeoutError": "errors",
25
27
  "AiohttpCircuitOpenError": "errors",
26
28
  "AiohttpDeadlineExceededError": "errors",
27
29
  "AiohttpTooManyRedirectsError": "errors",
@@ -37,6 +39,7 @@ def __getattr__(name: str) -> Any:
37
39
  __all__ = [
38
40
  "CAPABILITIES",
39
41
  "AiohttpAdapter",
42
+ "AiohttpAttemptTimeoutError",
40
43
  "AiohttpCircuitOpenError",
41
44
  "AiohttpDeadlineExceededError",
42
45
  "AiohttpTooManyRedirectsError",
@@ -24,6 +24,7 @@ CAPABILITIES = AdapterCapabilities(
24
24
  Capability.TIMEOUT_WRITE: Support.ABSENT,
25
25
  Capability.TIMEOUT_POOL: Support.ABSENT,
26
26
  Capability.DEADLINE_HARD: Support.EMULATED,
27
+ Capability.DEADLINE_COVERS_BODY: Support.ABSENT,
27
28
  Capability.POOL_LIMIT_TOTAL: Support.NATIVE,
28
29
  Capability.POOL_LIMIT_PER_HOST: Support.NATIVE,
29
30
  Capability.KEEPALIVE: Support.NATIVE,
@@ -43,6 +44,7 @@ CAPABILITIES = AdapterCapabilities(
43
44
  {
44
45
  FailureKind.CONNECT_TIMEOUT,
45
46
  FailureKind.READ_TIMEOUT,
47
+ FailureKind.ATTEMPT_TIMEOUT,
46
48
  FailureKind.TOTAL_TIMEOUT,
47
49
  FailureKind.CONNECT_ERROR,
48
50
  FailureKind.DNS_ERROR,
@@ -74,6 +76,10 @@ CAPABILITIES = AdapterCapabilities(
74
76
  ),
75
77
  "ceil_threshold": "ClientTimeout.ceil_threshold is raised so aiohttp never ceils deadlines to whole seconds.",
76
78
  "body_duration": "The middleware returns at headers; body read is not instrumented (no body_duration metric).",
79
+ "deadline_covers_body": (
80
+ "The body streams outside the seam through aiohttp's StreamReader: the total stops at the headers and "
81
+ "only sock_read bounds a dripping body."
82
+ ),
77
83
  "max_keepalive": "aiohttp has no cap on the NUMBER of keep-alive connections; pool.max_keepalive is ignored.",
78
84
  },
79
85
  )
@@ -6,7 +6,13 @@ keeps working when clientwright raises on its own authority.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from ...core.errors import 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",
@@ -7,7 +7,7 @@ mirror the httpx adapter on purpose.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from ...core.errors import CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
10
+ from ...core.errors import AttemptTimeoutError, CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
11
11
  from .._httpx_shared import make_error_translator
12
12
  from ._imports import httpx2
13
13
 
@@ -20,15 +20,20 @@ class HttpxDeadlineExceededError(DeadlineExceededError, httpx2.TimeoutException)
20
20
  """Total deadline exhausted, catchable as httpx2.TimeoutException."""
21
21
 
22
22
 
23
+ class HttpxAttemptTimeoutError(AttemptTimeoutError, httpx2.TimeoutException):
24
+ """Attempt ceiling exhausted, catchable as httpx2.TimeoutException."""
25
+
26
+
23
27
  class HttpxTooManyRedirectsError(TooManyRedirectsError, httpx2.TooManyRedirects):
24
28
  """Owned redirect limit exceeded, catchable as httpx2.TooManyRedirects."""
25
29
 
26
30
 
27
31
  translate_call_error = make_error_translator(
28
- HttpxCircuitOpenError, HttpxDeadlineExceededError, HttpxTooManyRedirectsError
32
+ HttpxCircuitOpenError, HttpxDeadlineExceededError, HttpxTooManyRedirectsError, HttpxAttemptTimeoutError
29
33
  )
30
34
 
31
35
  __all__ = [
36
+ "HttpxAttemptTimeoutError",
32
37
  "HttpxCircuitOpenError",
33
38
  "HttpxDeadlineExceededError",
34
39
  "HttpxTooManyRedirectsError",
@@ -6,6 +6,7 @@ from collections.abc import Callable
6
6
 
7
7
  from .._httpx_shared import AsyncFamilyNormalizer, AsyncTimedStreamMixin
8
8
  from ._imports import httpx2
9
+ from .errors import translate_call_error
9
10
  from .views import HttpxRequestView
10
11
 
11
12
 
@@ -21,6 +22,7 @@ class AsyncHttpxNormalizer(AsyncFamilyNormalizer):
21
22
  sdk=httpx2,
22
23
  request_view=lambda native: HttpxRequestView(native, default_timeout),
23
24
  timed_stream=_TimedAsyncStream,
25
+ translate=translate_call_error,
24
26
  )
25
27
 
26
28
 
@@ -6,6 +6,7 @@ from collections.abc import Callable
6
6
 
7
7
  from .._httpx_shared import SyncFamilyNormalizer, SyncTimedStreamMixin
8
8
  from ._imports import httpx2
9
+ from .errors import translate_call_error
9
10
  from .views import HttpxRequestView
10
11
 
11
12
 
@@ -21,6 +22,7 @@ class SyncHttpxNormalizer(SyncFamilyNormalizer):
21
22
  sdk=httpx2,
22
23
  request_view=lambda native: HttpxRequestView(native, default_timeout),
23
24
  timed_stream=_TimedSyncStream,
25
+ translate=translate_call_error,
24
26
  )
25
27
 
26
28
 
@@ -24,6 +24,7 @@ CAPABILITIES = AdapterCapabilities(
24
24
  Capability.TIMEOUT_WRITE: Support.ABSENT,
25
25
  Capability.TIMEOUT_POOL: Support.ABSENT,
26
26
  Capability.DEADLINE_HARD: Support.ABSENT,
27
+ Capability.DEADLINE_COVERS_BODY: Support.ABSENT,
27
28
  Capability.POOL_LIMIT_TOTAL: Support.ABSENT,
28
29
  Capability.POOL_LIMIT_PER_HOST: Support.NATIVE,
29
30
  Capability.KEEPALIVE: Support.DEGRADED,
@@ -55,12 +56,17 @@ CAPABILITIES = AdapterCapabilities(
55
56
  }
56
57
  ),
57
58
  collapses={
59
+ FailureKind.ATTEMPT_TIMEOUT: FailureKind.READ_TIMEOUT,
58
60
  FailureKind.POOL_TIMEOUT: FailureKind.CONNECT_TIMEOUT,
59
61
  FailureKind.PROTOCOL_ERROR: FailureKind.DISCONNECTED,
60
62
  FailureKind.WRITE_TIMEOUT: FailureKind.TOTAL_TIMEOUT,
61
63
  },
62
64
  notes={
63
65
  "sync_only": "requests has no async client; build_async raises.",
66
+ "attempt_timeout": (
67
+ "A sync runtime cannot cancel an attempt, so no ceiling ever fires; the stall it would have caught "
68
+ "arrives as the clamped read phase."
69
+ ),
64
70
  "no_session_timeout": (
65
71
  "requests has NO session-level timeout default - a bare session.get() hangs forever. The engine "
66
72
  "closes that hole: every attempt is sent with the planned (connect, read) tuple."
@@ -74,6 +80,10 @@ CAPABILITIES = AdapterCapabilities(
74
80
  "base_url": "requests has no base_url; the build rejects a config that sets one.",
75
81
  "per_call_options": "No request extensions; route/idempotency travel via the call_options() context manager.",
76
82
  "protocol_error": "requests folds protocol violations into ConnectionError; they surface as disconnected.",
83
+ "deadline_covers_body": (
84
+ "Session.send reads the body above the seam: the total stops at the headers and only the read timeout "
85
+ "bounds a dripping body."
86
+ ),
77
87
  },
78
88
  )
79
89
 
@@ -8,6 +8,7 @@ from typing import Any
8
8
  from ...core.contracts.message import RequestView, ResponseView
9
9
  from ...core.engine.base import default_response_outcome
10
10
  from ...core.model import ConnMetrics, FailureKind, Outcome
11
+ from ...core.policy.budget import Deadline
11
12
  from ._imports import requests
12
13
  from .classify import classify_error
13
14
  from .views import RequestsRequestView, RequestsResponseView
@@ -51,7 +52,9 @@ class SyncRequestsNormalizer:
51
52
  except Exception:
52
53
  return None
53
54
 
54
- def wrap_stream(self, response: ResponseView, on_done: Callable[[Outcome, float], None]) -> None:
55
+ def wrap_stream(
56
+ self, response: ResponseView, on_done: Callable[[Outcome, float], None], deadline: Deadline
57
+ ) -> None:
55
58
  # Session.send consumes the body ABOVE this seam (unless stream=True);
56
59
  # body read is not instrumented. Declared in capabilities.
57
60
  return None
@@ -24,6 +24,7 @@ CAPABILITIES = AdapterCapabilities(
24
24
  Capability.TIMEOUT_WRITE: Support.ABSENT,
25
25
  Capability.TIMEOUT_POOL: Support.NATIVE,
26
26
  Capability.DEADLINE_HARD: Support.ABSENT,
27
+ Capability.DEADLINE_COVERS_BODY: Support.ABSENT,
27
28
  Capability.POOL_LIMIT_TOTAL: Support.ABSENT,
28
29
  Capability.POOL_LIMIT_PER_HOST: Support.NATIVE,
29
30
  Capability.KEEPALIVE: Support.DEGRADED,
@@ -56,11 +57,16 @@ CAPABILITIES = AdapterCapabilities(
56
57
  }
57
58
  ),
58
59
  collapses={
60
+ FailureKind.ATTEMPT_TIMEOUT: FailureKind.READ_TIMEOUT,
59
61
  FailureKind.PROTOCOL_ERROR: FailureKind.DISCONNECTED,
60
62
  FailureKind.WRITE_TIMEOUT: FailureKind.TOTAL_TIMEOUT,
61
63
  },
62
64
  notes={
63
65
  "sync_only": "urllib3 has no async client; build_async raises.",
66
+ "attempt_timeout": (
67
+ "A sync runtime cannot cancel an attempt, so no ceiling ever fires; the stall it would have caught "
68
+ "arrives as the clamped read phase."
69
+ ),
64
70
  "seam": (
65
71
  "The engine is injected as an INSTANCE urlopen on a genuine PoolManager (type(client) is "
66
72
  "urllib3.PoolManager); recursive native redirect hops re-enter it and pass straight through."
@@ -80,6 +86,10 @@ CAPABILITIES = AdapterCapabilities(
80
86
  "per_call_options": "No request object; route/idempotency travel via the call_options() context manager.",
81
87
  "base_url": "urllib3 has no base_url; the build rejects a config that sets one.",
82
88
  "proxy": "An explicit proxy builds a genuine urllib3.ProxyManager; env proxies are not read (dropped).",
89
+ "deadline_covers_body": (
90
+ "urlopen preloads or streams the body above the seam: the total stops at the headers and only the read "
91
+ "timeout bounds a dripping body."
92
+ ),
83
93
  },
84
94
  )
85
95