clientwright 0.3.0__tar.gz → 0.4.1__tar.gz

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