xtr-http-client 4.0.4__tar.gz → 4.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/PKG-INFO +25 -15
  2. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/README.md +24 -9
  3. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/pyproject.toml +2 -4
  4. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/pyproject.toml.orig +3 -10
  5. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/.agents/skills/xtr-http-client/SKILL.md +9 -3
  6. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/bundle/_clients.py +1 -1
  7. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/bundle/client_options_config.py +1 -1
  8. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/bundle/http_client_bundle.py +1 -1
  9. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/exception/__init__.py +5 -1
  10. xtr_http_client-4.1.0/src/xtr_http_client/exception/logic_error.py +16 -0
  11. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/no_private_network_http_client.py +33 -6
  12. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/LICENSE +0 -0
  13. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/__init__.py +0 -0
  14. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/bundle/__init__.py +0 -0
  15. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/bundle/http_client_config.py +0 -0
  16. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/bundle/retry_failed_config.py +0 -0
  17. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/bundle/scoped_client_config.py +0 -0
  18. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/exception/_http_error_message.py +0 -0
  19. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/http_client.py +0 -0
  20. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/http_client_mixin.py +0 -0
  21. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/httpx_http_client.py +0 -0
  22. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/internal/__init__.py +0 -0
  23. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/internal/debug_type.py +0 -0
  24. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/internal/httpx_body.py +0 -0
  25. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/internal/httpx_clients.py +0 -0
  26. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/internal/httpx_errors.py +0 -0
  27. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/internal/httpx_redirect.py +0 -0
  28. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/internal/httpx_request.py +0 -0
  29. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/internal/mock_head.py +0 -0
  30. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/internal/mock_info.py +0 -0
  31. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/internal/option_merge.py +0 -0
  32. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/internal/response_body.py +0 -0
  33. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/internal/retry_count_response.py +0 -0
  34. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/internal/retry_delay.py +0 -0
  35. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/internal/retry_options.py +0 -0
  36. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/mock_http_client.py +0 -0
  37. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/py.typed +0 -0
  38. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/recipe/__init__.py +0 -0
  39. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/recipe/manifest.toml +0 -0
  40. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/response/__init__.py +0 -0
  41. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/response/http_response.py +0 -0
  42. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/response/httpx_response.py +0 -0
  43. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/response/json_mock_response.py +0 -0
  44. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/response/mock_response.py +0 -0
  45. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/retry/__init__.py +0 -0
  46. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/retry/generic_retry_strategy.py +0 -0
  47. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/retry/retry_context.py +0 -0
  48. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/retry/retry_strategy_interface.py +0 -0
  49. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/retryable_http_client.py +0 -0
  50. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/scoping_http_client.py +0 -0
  51. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/throttling_http_client.py +0 -0
  52. {xtr_http_client-4.0.4 → xtr_http_client-4.1.0}/src/xtr_http_client/traceable_http_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xtr-http-client
3
- Version: 4.0.4
3
+ Version: 4.1.0
4
4
  Summary: An async HTTP client behind one interface: typed request options, bodies read on demand, scoped clients, a private-network guard and a mock client for tests.
5
5
  Keywords: http,http-client,request,response,mock,asyncio
6
6
  Author: Xterr
@@ -20,18 +20,13 @@ Requires-Dist: typing-extensions>=4.4
20
20
  Requires-Dist: anyio>=4.0
21
21
  Requires-Dist: httpx>=0.27
22
22
  Requires-Dist: xtr-http-client-contracts>=4.0,<5
23
- Requires-Dist: xtr-http-foundation>=4.0,<5
24
23
  Requires-Dist: xtr-http-message>=4.0,<5
25
24
  Requires-Dist: xtr-http-message-contracts>=4.0,<5
26
25
  Requires-Dist: xtr-logging-contracts>=4.0,<5
27
26
  Requires-Dist: xtr-service-contracts>=4.0,<5
28
- Requires-Dist: xtr-dependency-injection>=4.0,<5 ; extra == 'di'
29
27
  Requires-Dist: httpx[http2]>=0.27 ; extra == 'http2'
30
- Requires-Dist: xtr-rate-limiter>=4.0,<5 ; extra == 'throttling'
31
28
  Requires-Python: >=3.11
32
- Provides-Extra: di
33
29
  Provides-Extra: http2
34
- Provides-Extra: throttling
35
30
  Description-Content-Type: text/markdown
36
31
 
37
32
  <div align="center">
@@ -64,12 +59,18 @@ calls out sees one type however much is layered under it.
64
59
 
65
60
  ```sh
66
61
  uv add xtr-http-client
67
- uv add "xtr-http-client[di]" # + the bundle
68
- uv add "xtr-http-client[throttling]" # + the limiter the throttling client paces by
69
- uv add "xtr-http-client[http2]" # + the framing a request asking for version "2" needs
62
+ uv add "xtr-http-client[http2]" # + the framing a request asking for version "2" needs
63
+ uv add xtr-http-client xtr-dependency-injection # + the container, for the HttpClientBundle
64
+ uv add xtr-rate-limiter # beside it: the limiter the throttling client paces by
65
+ uv add xtr-http-foundation # beside it: the networks the private-network guard refuses
70
66
  ```
71
67
 
72
- Requires Python 3.11+.
68
+ Requires Python 3.11+. `http2` is the only extra, and the container is not a dependency: the
69
+ bundle is used when [xtr-dependency-injection](../xtr-dependency-injection) is installed beside the
70
+ package, never as an extra, and every client works without it, built by hand as below. Installed
71
+ beside it too, [xtr-rate-limiter](../xtr-rate-limiter) supplies the limiter `ThrottlingHttpClient`
72
+ paces by, and [xtr-http-foundation](../xtr-http-foundation) the networks
73
+ `NoPrivateNetworkHttpClient` refuses.
73
74
 
74
75
  ## Quick start
75
76
 
@@ -250,9 +251,9 @@ from xtr_http_client import ThrottlingHttpClient
250
251
  client = ThrottlingHttpClient(inner, limiter)
251
252
  ```
252
253
 
253
- `limiter` is an [xtr-rate-limiter](../xtr-rate-limiter) `LimiterInterface`, from the
254
- `throttling` extra. A token is booked before each request and the request waits for the moment
255
- that booking is due; booking first and waiting after is what makes the pace hold under
254
+ `limiter` is an [xtr-rate-limiter](../xtr-rate-limiter) `LimiterInterface`, from that package
255
+ installed beside this one. A token is booked before each request and the request waits for the
256
+ moment that booking is due; booking first and waiting after is what makes the pace hold under
256
257
  concurrency, since requests then leave in the order they were asked for rather than all at
257
258
  once. Which requests share a pace is decided by which of them are given the same limiter — one
258
259
  per host, one per account, one for the whole application — and a copy made by `with_options()`
@@ -308,6 +309,10 @@ alike. `subnets` replaces that set and `allow_list` opens one receiver without o
308
309
  network it sits in; both take addresses or CIDR networks, never host names. What a host
309
310
  resolved to is remembered for the client's life, and `reset()` forgets it.
310
311
 
312
+ xtr-http-foundation is not a dependency: install it beside this package (`uv add
313
+ xtr-http-foundation`) to use the guard. Without it, building `NoPrivateNetworkHttpClient`
314
+ raises `LogicError`, so nothing goes out unchecked.
315
+
311
316
  ### `MockHttpClient` — answer from a script
312
317
 
313
318
  The same client as any other here — the options it refuses, the defaults it starts from and the
@@ -370,8 +375,9 @@ Everything adding this package to an application on
370
375
  [xtr-dependency-injection](../xtr-dependency-injection) takes — and, read backwards, what
371
376
  removing it undoes.
372
377
 
373
- - **Install** — `uv add "xtr-http-client[di]"`; add `throttling` to pace a client with an
374
- [xtr-rate-limiter](../xtr-rate-limiter) limiter, `http2` to ask for HTTP/2.
378
+ - **Install** — `uv add xtr-http-client xtr-dependency-injection`: the bundle needs the container
379
+ installed beside it, never as an extra. Add `http2` to ask for HTTP/2, and install
380
+ [xtr-rate-limiter](../xtr-rate-limiter) beside it to pace a client with one of its limiters.
375
381
  - **Recipe** — `uv run xtr-recipes recipes:sync` does the *Activate* step below: it lists
376
382
  `HttpClientBundle`. There is no config file, environment or ignore line to write; it prints the
377
383
  step to name the services the application calls in `config/http_client.py`, which a recipe
@@ -392,6 +398,10 @@ removing it undoes.
392
398
 
393
399
  ## Kernel / bundle
394
400
 
401
+ With [xtr-dependency-injection](../xtr-dependency-injection) installed beside the package — it is
402
+ not a dependency, and nothing outside `xtr_http_client.bundle` needs it — list the bundle, then
403
+ configure the clients:
404
+
395
405
  ```python
396
406
  # app/bundles.py
397
407
  from xtr_http_client.bundle import HttpClientBundle
@@ -28,12 +28,18 @@ calls out sees one type however much is layered under it.
28
28
 
29
29
  ```sh
30
30
  uv add xtr-http-client
31
- uv add "xtr-http-client[di]" # + the bundle
32
- uv add "xtr-http-client[throttling]" # + the limiter the throttling client paces by
33
- uv add "xtr-http-client[http2]" # + the framing a request asking for version "2" needs
31
+ uv add "xtr-http-client[http2]" # + the framing a request asking for version "2" needs
32
+ uv add xtr-http-client xtr-dependency-injection # + the container, for the HttpClientBundle
33
+ uv add xtr-rate-limiter # beside it: the limiter the throttling client paces by
34
+ uv add xtr-http-foundation # beside it: the networks the private-network guard refuses
34
35
  ```
35
36
 
36
- Requires Python 3.11+.
37
+ Requires Python 3.11+. `http2` is the only extra, and the container is not a dependency: the
38
+ bundle is used when [xtr-dependency-injection](../xtr-dependency-injection) is installed beside the
39
+ package, never as an extra, and every client works without it, built by hand as below. Installed
40
+ beside it too, [xtr-rate-limiter](../xtr-rate-limiter) supplies the limiter `ThrottlingHttpClient`
41
+ paces by, and [xtr-http-foundation](../xtr-http-foundation) the networks
42
+ `NoPrivateNetworkHttpClient` refuses.
37
43
 
38
44
  ## Quick start
39
45
 
@@ -214,9 +220,9 @@ from xtr_http_client import ThrottlingHttpClient
214
220
  client = ThrottlingHttpClient(inner, limiter)
215
221
  ```
216
222
 
217
- `limiter` is an [xtr-rate-limiter](../xtr-rate-limiter) `LimiterInterface`, from the
218
- `throttling` extra. A token is booked before each request and the request waits for the moment
219
- that booking is due; booking first and waiting after is what makes the pace hold under
223
+ `limiter` is an [xtr-rate-limiter](../xtr-rate-limiter) `LimiterInterface`, from that package
224
+ installed beside this one. A token is booked before each request and the request waits for the
225
+ moment that booking is due; booking first and waiting after is what makes the pace hold under
220
226
  concurrency, since requests then leave in the order they were asked for rather than all at
221
227
  once. Which requests share a pace is decided by which of them are given the same limiter — one
222
228
  per host, one per account, one for the whole application — and a copy made by `with_options()`
@@ -272,6 +278,10 @@ alike. `subnets` replaces that set and `allow_list` opens one receiver without o
272
278
  network it sits in; both take addresses or CIDR networks, never host names. What a host
273
279
  resolved to is remembered for the client's life, and `reset()` forgets it.
274
280
 
281
+ xtr-http-foundation is not a dependency: install it beside this package (`uv add
282
+ xtr-http-foundation`) to use the guard. Without it, building `NoPrivateNetworkHttpClient`
283
+ raises `LogicError`, so nothing goes out unchecked.
284
+
275
285
  ### `MockHttpClient` — answer from a script
276
286
 
277
287
  The same client as any other here — the options it refuses, the defaults it starts from and the
@@ -334,8 +344,9 @@ Everything adding this package to an application on
334
344
  [xtr-dependency-injection](../xtr-dependency-injection) takes — and, read backwards, what
335
345
  removing it undoes.
336
346
 
337
- - **Install** — `uv add "xtr-http-client[di]"`; add `throttling` to pace a client with an
338
- [xtr-rate-limiter](../xtr-rate-limiter) limiter, `http2` to ask for HTTP/2.
347
+ - **Install** — `uv add xtr-http-client xtr-dependency-injection`: the bundle needs the container
348
+ installed beside it, never as an extra. Add `http2` to ask for HTTP/2, and install
349
+ [xtr-rate-limiter](../xtr-rate-limiter) beside it to pace a client with one of its limiters.
339
350
  - **Recipe** — `uv run xtr-recipes recipes:sync` does the *Activate* step below: it lists
340
351
  `HttpClientBundle`. There is no config file, environment or ignore line to write; it prints the
341
352
  step to name the services the application calls in `config/http_client.py`, which a recipe
@@ -356,6 +367,10 @@ removing it undoes.
356
367
 
357
368
  ## Kernel / bundle
358
369
 
370
+ With [xtr-dependency-injection](../xtr-dependency-injection) installed beside the package — it is
371
+ not a dependency, and nothing outside `xtr_http_client.bundle` needs it — list the bundle, then
372
+ configure the clients:
373
+
359
374
  ```python
360
375
  # app/bundles.py
361
376
  from xtr_http_client.bundle import HttpClientBundle
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "xtr-http-client"
3
- version = "4.0.4"
3
+ version = "4.1.0"
4
4
  description = "An async HTTP client behind one interface: typed request options, bodies read on demand, scoped clients, a private-network guard and a mock client for tests."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -30,7 +30,6 @@ dependencies = [
30
30
  "anyio>=4.0",
31
31
  "httpx>=0.27",
32
32
  "xtr-http-client-contracts>=4.0,<5",
33
- "xtr-http-foundation>=4.0,<5",
34
33
  "xtr-http-message>=4.0,<5",
35
34
  "xtr-http-message-contracts>=4.0,<5",
36
35
  "xtr-logging-contracts>=4.0,<5",
@@ -42,9 +41,7 @@ name = "Xterr"
42
41
  email = "me@xterr.dev"
43
42
 
44
43
  [project.optional-dependencies]
45
- di = ["xtr-dependency-injection>=4.0,<5"]
46
44
  http2 = ["httpx[http2]>=0.27"]
47
- throttling = ["xtr-rate-limiter>=4.0,<5"]
48
45
 
49
46
  [project.entry-points."xtr_dependency_injection.bundles"]
50
47
  http_client = "xtr_http_client.bundle:HttpClientBundle"
@@ -63,6 +60,7 @@ dev = [
63
60
  "ty>=0.0.83",
64
61
  "xtr-clock>=4.0,<5",
65
62
  "xtr-dependency-injection>=4.0,<5",
63
+ "xtr-http-foundation>=4.0,<5",
66
64
  "xtr-rate-limiter>=4.0,<5",
67
65
  ]
68
66
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "xtr-http-client"
3
- version = "4.0.4"
3
+ version = "4.1.0"
4
4
  description = "An async HTTP client behind one interface: typed request options, bodies read on demand, scoped clients, a private-network guard and a mock client for tests."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -28,7 +28,6 @@ dependencies = [
28
28
  "anyio>=4.0",
29
29
  "httpx>=0.27",
30
30
  "xtr-http-client-contracts>=4.0,<5",
31
- "xtr-http-foundation>=4.0,<5",
32
31
  "xtr-http-message>=4.0,<5",
33
32
  "xtr-http-message-contracts>=4.0,<5",
34
33
  "xtr-logging-contracts>=4.0,<5",
@@ -36,18 +35,10 @@ dependencies = [
36
35
  ]
37
36
 
38
37
  [project.optional-dependencies]
39
- di = [
40
- "xtr-dependency-injection>=4.0,<5",
41
- ]
42
38
  # The HTTP/2 framing, which only a request asking for version "2" needs.
43
39
  http2 = [
44
40
  "httpx[http2]>=0.27",
45
41
  ]
46
- # The limiter the throttling client paces requests by; nothing else needs it,
47
- # so the package imports and every other client works without it.
48
- throttling = [
49
- "xtr-rate-limiter>=4.0,<5",
50
- ]
51
42
 
52
43
  # Advertises the bundle so debug:bundles can name it when installed but not listed.
53
44
  # Advertising activates nothing: an application lists the bundles it wants.
@@ -69,6 +60,8 @@ dev = [
69
60
  "ty>=0.0.83",
70
61
  "xtr-clock>=4.0,<5",
71
62
  "xtr-dependency-injection>=4.0,<5",
63
+ # The networks NoPrivateNetworkHttpClient refuses; optional for an application.
64
+ "xtr-http-foundation>=4.0,<5",
72
65
  "xtr-rate-limiter>=4.0,<5",
73
66
  ]
74
67
 
@@ -28,6 +28,9 @@ client, so the calling code sees one type however deep the stack is.
28
28
  `MockHttpClient`.
29
29
  - Options are the contract's `RequestOptions`. An option it does not know is refused, never
30
30
  ignored.
31
+ - In an application, with xtr-dependency-injection installed beside the package (it is not a
32
+ dependency): activate `HttpClientBundle` and inject `HttpClientInterface`. Without the
33
+ container, build the clients yourself; only `xtr_http_client.bundle` needs it.
31
34
 
32
35
  ## Make a request
33
36
 
@@ -109,7 +112,7 @@ client = RetryableHttpClient(inner, GenericRetryStrategy([500, 502, 503]), max_r
109
112
  ```python
110
113
  from xtr_http_client import NoPrivateNetworkHttpClient, ThrottlingHttpClient, TraceableHttpClient
111
114
 
112
- paced = ThrottlingHttpClient(inner, limiter) # xtr-rate-limiter LimiterInterface, [throttling]
115
+ paced = ThrottlingHttpClient(inner, limiter) # a LimiterInterface from xtr-rate-limiter
113
116
  traced = TraceableHttpClient(inner)
114
117
  guarded = NoPrivateNetworkHttpClient(inner, allow_list=("10.1.2.3",))
115
118
  ```
@@ -125,6 +128,8 @@ guarded = NoPrivateNetworkHttpClient(inner, allow_list=("10.1.2.3",))
125
128
  Refusal is `TransportError`: `Host "<host>" is blocked for "<url>".`. Without `subnets` it
126
129
  refuses `IpUtils.PRIVATE_SUBNETS`; `allow_list` opens one receiver. Addresses and CIDR
127
130
  networks only — never host names.
131
+ It needs xtr-http-foundation installed beside this package (`uv add xtr-http-foundation`);
132
+ without it, building the guard raises `LogicError`.
128
133
 
129
134
  ## Use the transport directly
130
135
 
@@ -187,8 +192,9 @@ async def test_it_reads_the_created_order() -> None:
187
192
  `HttpClientBundle`. There is no config file, environment or ignore line to write; it prints the
188
193
  step to name the services the application calls, which a recipe cannot make for you.
189
194
 
190
- 1. **Install** — `uv add "xtr-http-client[di]"`; add `throttling` to pace a client with an
191
- xtr-rate-limiter limiter, `http2` to ask for HTTP/2.
195
+ 1. **Install** — `uv add xtr-http-client xtr-dependency-injection`: the bundle needs the container
196
+ installed beside it, never as an extra. Add `http2` to ask for HTTP/2, and install
197
+ xtr-rate-limiter beside it to pace a client with one of its limiters.
192
198
  2. **Activate** — `HttpClientBundle: {"all": True}` in `BUNDLES` in `<app>/bundles.py`
193
199
  (`from xtr_http_client.bundle import HttpClientBundle`).
194
200
  3. **Brings along** — the rate limiter bundle when xtr-rate-limiter is installed.
@@ -124,7 +124,7 @@ def _mock_transport() -> MockHttpClient:
124
124
 
125
125
  def _limiters(rate_limiter: str) -> object:
126
126
  """Return the annotation asking for the limiter factory ``rate_limiter`` names."""
127
- # The throttling extra is optional: a client is only paced while the rate
127
+ # xtr-rate-limiter is optional: a client is only paced while the rate
128
128
  # limiter bundle is active, which proves it installed.
129
129
  from xtr_rate_limiter import RateLimiterFactoryInterface # noqa: PLC0415
130
130
 
@@ -44,7 +44,7 @@ class ClientOptionsConfig:
44
44
  extra: Additional options a client may ignore.
45
45
  retry_failed: Whether and how a failed request is made again.
46
46
  rate_limiter: The name of the xtr-rate-limiter limiter every request
47
- waits its turn from; needs the ``throttling`` extra.
47
+ waits its turn from; needs xtr-rate-limiter installed.
48
48
  """
49
49
 
50
50
  _NOT_OPTIONS: ClassVar[frozenset[str]] = frozenset({"retry_failed", "rate_limiter"})
@@ -81,5 +81,5 @@ def _check_pace(owner: str, rate_limiter: str | None, pacing: bool) -> None:
81
81
  if rate_limiter is not None and not pacing:
82
82
  raise InvalidArgumentError(
83
83
  f'{owner} is paced by the "{rate_limiter}" rate limiter, but the rate limiter '
84
- f'bundle is not active: install "xtr-http-client[throttling]".'
84
+ "bundle is not active: install xtr-rate-limiter."
85
85
  )
@@ -8,7 +8,8 @@ at the root, :class:`TransportError` for "there is no answer" (with
8
8
  :class:`ClientError`, :class:`ServerError`), :class:`DecodingError` for an
9
9
  answer that is not what it was read as (with :class:`JsonError` for a body that
10
10
  is not the JSON it was read as), and :class:`InvalidArgumentError` for a request
11
- this client refuses to make.
11
+ this client refuses to make. :class:`LogicError` is this package's own: a client
12
+ built in a way that cannot work.
12
13
  """
13
14
 
14
15
  from __future__ import annotations
@@ -26,6 +27,8 @@ from xtr_http_client_contracts import (
26
27
  TransportTimeoutError,
27
28
  )
28
29
 
30
+ from .logic_error import LogicError
31
+
29
32
  __all__ = [
30
33
  "ClientError",
31
34
  "DecodingError",
@@ -33,6 +36,7 @@ __all__ = [
33
36
  "HttpError",
34
37
  "InvalidArgumentError",
35
38
  "JsonError",
39
+ "LogicError",
36
40
  "RedirectionError",
37
41
  "ServerError",
38
42
  "TransportError",
@@ -0,0 +1,16 @@
1
+ """This library was used in a way that cannot work."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from xtr_http_client_contracts import HttpClientError
6
+
7
+ __all__ = ["LogicError"]
8
+
9
+
10
+ class LogicError(HttpClientError, RuntimeError):
11
+ """A client was built in a way that cannot work, whatever it is asked to send.
12
+
13
+ Building the private-network guard without xtr-http-foundation installed,
14
+ which holds the networks it refuses, is a mistake in the application's
15
+ setup rather than in a request, so retrying it changes nothing.
16
+ """
@@ -12,11 +12,10 @@ from urllib.parse import urljoin
12
12
  import anyio
13
13
  from typing_extensions import Unpack, override
14
14
  from xtr_http_client_contracts import HttpClientInterface
15
- from xtr_http_foundation import IpUtils
16
15
  from xtr_http_message import Request, Stream, Uri
17
16
  from xtr_service_contracts import ResetInterface
18
17
 
19
- from .exception import InvalidArgumentError, TransportError
18
+ from .exception import InvalidArgumentError, LogicError, TransportError
20
19
  from .internal.option_merge import as_options
21
20
 
22
21
  if TYPE_CHECKING:
@@ -24,6 +23,7 @@ if TYPE_CHECKING:
24
23
  from typing import Self
25
24
 
26
25
  from xtr_http_client_contracts import HttpResponseInterface, RequestOptions
26
+ from xtr_http_foundation import IpUtils
27
27
  from xtr_http_message_contracts import RequestInterface
28
28
 
29
29
  __all__ = ["NoPrivateNetworkHttpClient"]
@@ -64,9 +64,12 @@ class NoPrivateNetworkHttpClient(HttpClientInterface, ResetInterface):
64
64
  What a host resolves to is remembered for as long as this client lives,
65
65
  so a chain of hops back to the same host asks the resolver once;
66
66
  :meth:`reset` forgets it.
67
+
68
+ The networks it refuses come from xtr-http-foundation, which is not a
69
+ dependency of this package: install it beside it to use this client.
67
70
  """
68
71
 
69
- __slots__ = ("_allow_list", "_client", "_dns_cache", "_resolver", "_subnets")
72
+ __slots__ = ("_allow_list", "_client", "_dns_cache", "_ip_utils", "_resolver", "_subnets")
70
73
 
71
74
  def __init__(
72
75
  self,
@@ -89,9 +92,14 @@ class NoPrivateNetworkHttpClient(HttpClientInterface, ResetInterface):
89
92
  have to be resolved, and could point anywhere by then.
90
93
  resolver: What every address of a host and port is asked of.
91
94
  ``None`` asks the platform's own resolver.
95
+
96
+ Raises:
97
+ LogicError: When xtr-http-foundation is not installed, since there
98
+ is then no way to tell which addresses to refuse.
92
99
  """
100
+ self._ip_utils = _ip_utils()
93
101
  self._client = client
94
- self._subnets = _entries(subnets) if subnets is not None else IpUtils.PRIVATE_SUBNETS
102
+ self._subnets = _entries(subnets) if subnets is not None else self._ip_utils.PRIVATE_SUBNETS
95
103
  self._allow_list = _entries(allow_list)
96
104
  self._resolver = resolver if resolver is not None else _getaddrinfo
97
105
  self._dns_cache: dict[str, Sequence[str]] = {}
@@ -258,10 +266,29 @@ class NoPrivateNetworkHttpClient(HttpClientInterface, ResetInterface):
258
266
  The allow list is read first, so one internal receiver is opened
259
267
  without opening the network it sits in.
260
268
  """
261
- if self._allow_list and IpUtils.check_ip(address, self._allow_list):
269
+ if self._allow_list and self._ip_utils.check_ip(address, self._allow_list):
262
270
  return False
263
271
 
264
- return IpUtils.check_ip(address, self._subnets)
272
+ return self._ip_utils.check_ip(address, self._subnets)
273
+
274
+
275
+ def _ip_utils() -> type[IpUtils]:
276
+ """The address checks, from xtr-http-foundation when it is installed.
277
+
278
+ Raises:
279
+ LogicError: When it is not, so the guard fails where it is built
280
+ rather than letting through a request it could not check.
281
+ """
282
+ try:
283
+ # Optional package, so imported only when a guard is built (hence PLC0415).
284
+ from xtr_http_foundation import IpUtils # noqa: PLC0415
285
+ except ImportError as error:
286
+ raise LogicError(
287
+ 'You cannot use "NoPrivateNetworkHttpClient" if xtr-http-foundation is not '
288
+ 'installed. Try running "uv add xtr-http-foundation".'
289
+ ) from error
290
+
291
+ return IpUtils
265
292
 
266
293
 
267
294
  async def _getaddrinfo(host: str, port: int) -> Sequence[str]:
File without changes