cloudru-qa-http-client 1.0.0__py3-none-any.whl
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.
- cloudru_qa_http_client-1.0.0.dist-info/METADATA +615 -0
- cloudru_qa_http_client-1.0.0.dist-info/RECORD +18 -0
- cloudru_qa_http_client-1.0.0.dist-info/WHEEL +4 -0
- cloudru_qa_http_client-1.0.0.dist-info/licenses/LICENSE +202 -0
- http_client/.agents/skills/http-client-usage/SKILL.md +227 -0
- http_client/.agents/skills/http-client-usage/references/options.md +219 -0
- http_client/.agents/skills/http-client-usage/references/pitfalls.md +53 -0
- http_client/__init__.py +16 -0
- http_client/client.py +603 -0
- http_client/config.py +82 -0
- http_client/event_hooks.py +159 -0
- http_client/options.py +226 -0
- http_client/py.typed +1 -0
- http_client/types.py +141 -0
- http_client/utils/__init__.py +0 -0
- http_client/utils/logutils.py +275 -0
- http_client/utils/sanitizer.py +59 -0
- http_client/utils/traceparent.py +12 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cloudru-qa-http-client
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Базовый HTTP-клиент
|
|
5
|
+
Project-URL: Repository, https://github.com/cloud-ru-tech/qa-libraries
|
|
6
|
+
Project-URL: Issues, https://github.com/cloud-ru-tech/qa-libraries/issues
|
|
7
|
+
Author-email: Алексей Кирютин <ayukiryutin@cloud.ru>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: async,client,http,httpx
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.13
|
|
21
|
+
Requires-Dist: glom>=25.12.0
|
|
22
|
+
Requires-Dist: httpx<0.29,>=0.28.1
|
|
23
|
+
Provides-Extra: truststore
|
|
24
|
+
Requires-Dist: truststore<0.11,>=0.10.4; extra == 'truststore'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# HTTP client
|
|
28
|
+
|
|
29
|
+
[](https://pypi.org/project/cloudru-qa-http-client/)
|
|
30
|
+
[](https://pypi.org/project/cloudru-qa-http-client/)
|
|
31
|
+
[](https://github.com/cloud-ru-tech/qa-libraries/blob/master/LICENSE)
|
|
32
|
+
|
|
33
|
+
English | [Русский](README.ru.md)
|
|
34
|
+
|
|
35
|
+
- [HTTP client](#http-client)
|
|
36
|
+
- [Installation](#installation)
|
|
37
|
+
- [Skills for AI agents](#skills-for-ai-agents)
|
|
38
|
+
- [Usage](#usage)
|
|
39
|
+
- [Example:](#example)
|
|
40
|
+
- [Base client example](#base-client-example)
|
|
41
|
+
- [Custom client example](#custom-client-example)
|
|
42
|
+
- [Asynchronous client example](#asynchronous-client-example)
|
|
43
|
+
- [Configuration](#configuration)
|
|
44
|
+
- [Available options](#available-options)
|
|
45
|
+
- [EventHooks](#eventhooks)
|
|
46
|
+
- [Decorating the send method](#decorating-the-send-method)
|
|
47
|
+
- [Streaming requests](#streaming-requests)
|
|
48
|
+
- [The `stream` method](#the-stream-method)
|
|
49
|
+
- [The `stream_request` method](#the-stream_request-method)
|
|
50
|
+
- [Decorating the stream\_request method](#decorating-the-stream_request-method)
|
|
51
|
+
- [The request method](#the-request-method)
|
|
52
|
+
- [Timeouts](#timeouts)
|
|
53
|
+
- [Authorization](#authorization)
|
|
54
|
+
- [Request identification](#request-identification)
|
|
55
|
+
- [Tracing](#tracing)
|
|
56
|
+
- [Logging](#logging)
|
|
57
|
+
- [Closing connections](#closing-connections)
|
|
58
|
+
- [TLS connection](#tls-connection)
|
|
59
|
+
- [Default headers](#default-headers)
|
|
60
|
+
- [Development](#development)
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
Requires Python 3.13 or later.
|
|
65
|
+
|
|
66
|
+
```shell
|
|
67
|
+
pip install cloudru-qa-http-client
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Import the library as the `http_client` module.
|
|
71
|
+
|
|
72
|
+
## Skills for AI agents
|
|
73
|
+
|
|
74
|
+
The package includes the [`http-client-usage`](https://github.com/cloud-ru-tech/qa-libraries/blob/master/.agents/skills/http-client-usage/SKILL.md) skill
|
|
75
|
+
with examples and usage rules for AI agents. Install the library
|
|
76
|
+
in your project environment, then run this from its root (requires [`uv`](https://docs.astral.sh/uv/)):
|
|
77
|
+
|
|
78
|
+
```shell
|
|
79
|
+
uvx library-skills install --skill http-client-usage --yes
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
For Claude Code, add `--claude`. The instructions are shipped with the library version.
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
The library provides `HttpClient` and `AsyncHttpClient`. Use them directly or subclass them to create custom clients.
|
|
87
|
+
|
|
88
|
+
All options (logging, request identification, tracing, default headers, and others) work the same for both clients. Synchronous event hooks are automatically wrapped as async when used with `AsyncHttpClient`.
|
|
89
|
+
|
|
90
|
+
### Example:
|
|
91
|
+
|
|
92
|
+
#### Base client example
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import logging
|
|
96
|
+
|
|
97
|
+
from http_client import HttpClient, options
|
|
98
|
+
from httpx import BasicAuth
|
|
99
|
+
|
|
100
|
+
with HttpClient(
|
|
101
|
+
"https://api.example.com",
|
|
102
|
+
options.with_auth(BasicAuth("demo", "example-password")),
|
|
103
|
+
options.with_logger(logging.getLogger(__name__)),
|
|
104
|
+
) as client:
|
|
105
|
+
response = client.post("/test_method", json={"test": True})
|
|
106
|
+
response.raise_for_status()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Custom client example
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import logging
|
|
113
|
+
|
|
114
|
+
from http_client import HttpClient, options
|
|
115
|
+
from httpx import Response
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class CustomHttpClient(HttpClient):
|
|
119
|
+
def test_method(self, data: dict[str, object]) -> Response:
|
|
120
|
+
"""Send data to a service method.
|
|
121
|
+
|
|
122
|
+
:param data: JSON request body.
|
|
123
|
+
:return: service response.
|
|
124
|
+
"""
|
|
125
|
+
request = self.client.build_request("POST", "/test_method", json=data)
|
|
126
|
+
return self.send(request)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
with CustomHttpClient(
|
|
130
|
+
"https://api.example.com",
|
|
131
|
+
options.with_logger(logging.getLogger(__name__)),
|
|
132
|
+
options.with_autogenerated_request_id(),
|
|
133
|
+
options.with_autogenerated_traceparent(),
|
|
134
|
+
) as client:
|
|
135
|
+
response = client.test_method({"test": True})
|
|
136
|
+
response.raise_for_status()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### Asynchronous client example
|
|
140
|
+
|
|
141
|
+
All standard options, including synchronous event hooks, work unchanged with `AsyncHttpClient`:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
import logging
|
|
145
|
+
|
|
146
|
+
from http_client import AsyncHttpClient, options
|
|
147
|
+
|
|
148
|
+
async with AsyncHttpClient(
|
|
149
|
+
"https://api.example.com",
|
|
150
|
+
options.with_logger(logging.getLogger(__name__)),
|
|
151
|
+
options.with_autogenerated_request_id(),
|
|
152
|
+
) as client:
|
|
153
|
+
response = await client.post("/test_method", json={"test": True})
|
|
154
|
+
response.raise_for_status()
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Configuration
|
|
158
|
+
Configure the client with `ClientOption` options. Each option receives the actual client instance and runs in its constructor. Register hooks and decorators with `add_request_hook`, `add_response_hook`, `add_send_decorator`, and `add_stream_decorator`:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from http_client import BaseHttpClient, ClientOption
|
|
162
|
+
from httpx import Request
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def with_region(region: str) -> ClientOption:
|
|
166
|
+
"""Add a region to outgoing request headers.
|
|
167
|
+
|
|
168
|
+
:param region: region identifier.
|
|
169
|
+
:return: client option.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def add_region(request: Request) -> None:
|
|
173
|
+
request.headers["X-Region"] = region
|
|
174
|
+
|
|
175
|
+
def option(client: BaseHttpClient) -> None:
|
|
176
|
+
client.add_request_hook(add_region, prepend=True)
|
|
177
|
+
|
|
178
|
+
return option
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Registration is available while constructor options run. After client creation, its transport is already configured.
|
|
182
|
+
|
|
183
|
+
## Available options
|
|
184
|
+
|
|
185
|
+
The `options` module provides options for configuring the HTTP client. Combine them when creating a client instance.
|
|
186
|
+
|
|
187
|
+
### EventHooks
|
|
188
|
+
|
|
189
|
+
Event hook options let you process requests and responses:
|
|
190
|
+
|
|
191
|
+
- `with_request_hook` — adds a function to process requests before sending
|
|
192
|
+
- `with_response_hook` — adds a function to process responses after receiving
|
|
193
|
+
|
|
194
|
+
These options provide a flexible way to handle each client request and/or response.
|
|
195
|
+
|
|
196
|
+
Usage example:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
import logging
|
|
200
|
+
|
|
201
|
+
from httpx import Request, Response
|
|
202
|
+
|
|
203
|
+
from http_client import HttpClient, options
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def my_request_hook(request: Request) -> None:
|
|
207
|
+
# process the request before sending it
|
|
208
|
+
request.headers["X-Custom-Header"] = "Custom Value"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def my_response_hook(response: Response) -> None:
|
|
212
|
+
# process the response after receiving it
|
|
213
|
+
logging.getLogger(__name__).info("Response status code: %s", response.status_code)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
client = HttpClient(
|
|
217
|
+
"http://example.com",
|
|
218
|
+
options.with_request_hook(my_request_hook),
|
|
219
|
+
options.with_response_hook(my_response_hook),
|
|
220
|
+
)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
With `AsyncHttpClient`, synchronous hooks are automatically wrapped as async. You can also pass native async hooks:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from http_client import AsyncHttpClient, options
|
|
227
|
+
from httpx import Request
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def my_async_request_hook(request: Request) -> None:
|
|
231
|
+
request.headers["X-Async"] = "true"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
client = AsyncHttpClient(
|
|
235
|
+
"http://example.com",
|
|
236
|
+
options.with_request_hook(my_async_request_hook),
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
See the [`httpx` documentation](https://www.python-httpx.org/advanced/event-hooks/) for more about hooks.
|
|
241
|
+
|
|
242
|
+
### Decorating the send method
|
|
243
|
+
|
|
244
|
+
The HTTP client supports decorating its `send` method with `with_send_decorator`. This lets you add behavior before sending the HTTP request and after receiving the response.
|
|
245
|
+
|
|
246
|
+
`with_send_decorator` applies the decorator pattern to `send`, letting you:
|
|
247
|
+
- Preprocess the request
|
|
248
|
+
- Postprocess the response
|
|
249
|
+
- Implement retries, logging, caching, monitoring, and other features
|
|
250
|
+
|
|
251
|
+
Example of a request logging decorator:
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from functools import wraps
|
|
255
|
+
from http_client import HttpClient, options
|
|
256
|
+
from http_client.types import SendOptions, SyncSendMethod
|
|
257
|
+
from typing import Unpack
|
|
258
|
+
import logging
|
|
259
|
+
import httpx
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def logging_decorator(func: SyncSendMethod) -> SyncSendMethod:
|
|
263
|
+
@wraps(func)
|
|
264
|
+
def wrapper(request: httpx.Request, **kwargs: Unpack[SendOptions]) -> httpx.Response:
|
|
265
|
+
logging.getLogger(__name__).info("Sending request: %s %s", request.method, request.url)
|
|
266
|
+
response = func(request, **kwargs)
|
|
267
|
+
logging.getLogger(__name__).info("Received response: %s", response.status_code)
|
|
268
|
+
return response
|
|
269
|
+
|
|
270
|
+
return wrapper
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
client = HttpClient("https://example.com", options.with_send_decorator(logging_decorator))
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
For the asynchronous HTTP client, the decorator must be asynchronous:
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
from functools import wraps
|
|
280
|
+
from http_client import AsyncHttpClient, options
|
|
281
|
+
from http_client.types import AsyncSendMethod, SendOptions
|
|
282
|
+
from typing import Unpack
|
|
283
|
+
import logging
|
|
284
|
+
import httpx
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def async_logging_decorator(func: AsyncSendMethod) -> AsyncSendMethod:
|
|
288
|
+
@wraps(func)
|
|
289
|
+
async def wrapper(request: httpx.Request, **kwargs: Unpack[SendOptions]) -> httpx.Response:
|
|
290
|
+
logging.getLogger(__name__).info("Sending request: %s %s", request.method, request.url)
|
|
291
|
+
response = await func(request, **kwargs)
|
|
292
|
+
logging.getLogger(__name__).info("Received response: %s", response.status_code)
|
|
293
|
+
return response
|
|
294
|
+
|
|
295
|
+
return wrapper
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
client = AsyncHttpClient("https://example.com", options.with_send_decorator(async_logging_decorator))
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
You can apply several decorators. Each new decorator wraps the previous one, so the last one added is outermost and runs first:
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
client = HttpClient(
|
|
305
|
+
"https://example.com",
|
|
306
|
+
options.with_send_decorator(decorator1), # applied first, becomes innermost
|
|
307
|
+
options.with_send_decorator(decorator2), # applied second, becomes outermost
|
|
308
|
+
)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Request calls run in this order: `decorator2` → `decorator1` → `send`. The response passes through them in reverse: `decorator1` finishes first, then `decorator2`.
|
|
312
|
+
|
|
313
|
+
### Streaming requests
|
|
314
|
+
|
|
315
|
+
The HTTP client supports streaming requests in two ways:
|
|
316
|
+
|
|
317
|
+
#### The `stream` method
|
|
318
|
+
|
|
319
|
+
`stream(method, url, **kwargs)` is fully compatible with the `httpx` API. It takes an HTTP method and URL, builds a request, and returns a context manager:
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
from http_client import HttpClient, options
|
|
323
|
+
|
|
324
|
+
client = HttpClient(
|
|
325
|
+
"https://example.com",
|
|
326
|
+
options.with_auth(auth),
|
|
327
|
+
options.with_logger(logger),
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Streaming download (httpx contract)
|
|
331
|
+
with client.stream("GET", "/large-file.zip") as response:
|
|
332
|
+
response.raise_for_status()
|
|
333
|
+
|
|
334
|
+
with open("file.zip", "wb") as f:
|
|
335
|
+
for chunk in response.iter_bytes(chunk_size=8192):
|
|
336
|
+
f.write(chunk)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Asynchronous client example:
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
from http_client import AsyncHttpClient, options
|
|
343
|
+
|
|
344
|
+
client = AsyncHttpClient(
|
|
345
|
+
"https://example.com",
|
|
346
|
+
options.with_auth(auth),
|
|
347
|
+
options.with_logger(logger),
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
async with client.stream("GET", "/stream-data") as response:
|
|
351
|
+
response.raise_for_status()
|
|
352
|
+
|
|
353
|
+
async for line in response.aiter_lines():
|
|
354
|
+
process_line(line)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
#### The `stream_request` method
|
|
358
|
+
|
|
359
|
+
`stream_request(request, **kwargs)` takes a ready-made `httpx.Request` and supports all client options, including decorators:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
request = client.client.build_request("GET", "/large-file.zip")
|
|
363
|
+
|
|
364
|
+
with client.stream_request(request) as response:
|
|
365
|
+
for chunk in response.iter_bytes(chunk_size=8192):
|
|
366
|
+
f.write(chunk)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
#### Decorating the stream_request method
|
|
370
|
+
|
|
371
|
+
Like `send`, `stream_request` can be decorated with `with_stream_decorator`:
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
from functools import wraps
|
|
375
|
+
from contextlib import contextmanager
|
|
376
|
+
from http_client import HttpClient, options
|
|
377
|
+
from http_client.types import StreamOptions, SyncStreamRequestMethod
|
|
378
|
+
from collections.abc import Iterator
|
|
379
|
+
from typing import Unpack
|
|
380
|
+
import httpx
|
|
381
|
+
import logging
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def stream_logging_decorator(func: SyncStreamRequestMethod) -> SyncStreamRequestMethod:
|
|
385
|
+
@wraps(func)
|
|
386
|
+
@contextmanager
|
|
387
|
+
def wrapper(request: httpx.Request, **kwargs: Unpack[StreamOptions]) -> Iterator[httpx.Response]:
|
|
388
|
+
logging.getLogger(__name__).info("Starting stream: %s %s", request.method, request.url)
|
|
389
|
+
with func(request, **kwargs) as response:
|
|
390
|
+
logging.getLogger(__name__).info("Stream status: %s", response.status_code)
|
|
391
|
+
yield response
|
|
392
|
+
logging.getLogger(__name__).info("Stream finished")
|
|
393
|
+
|
|
394
|
+
return wrapper
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
client = HttpClient("https://example.com", options.with_stream_decorator(stream_logging_decorator))
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Important:** When logging streaming requests, the response body appears as `<streamed content>` because it has not yet been read when the response hook runs.
|
|
401
|
+
|
|
402
|
+
### The request method
|
|
403
|
+
|
|
404
|
+
`request(method, url, **kwargs)` sends HTTP requests with any method:
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
response = client.request("PATCH", "/api/resource", json={"field": "value"})
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Timeouts
|
|
411
|
+
|
|
412
|
+
`with_timeout` sets different types of HTTP request timeouts:
|
|
413
|
+
|
|
414
|
+
```python
|
|
415
|
+
from httpx import Timeout
|
|
416
|
+
from http_client import HttpClient, options
|
|
417
|
+
|
|
418
|
+
# Set different timeout types
|
|
419
|
+
timeout = Timeout(
|
|
420
|
+
connect=5.0, # time to establish a connection
|
|
421
|
+
read=10.0, # time to read the response
|
|
422
|
+
write=5.0, # time to write the request
|
|
423
|
+
pool=1.0, # time to wait for a free connection in the pool
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
client = HttpClient("https://example.com", options.with_timeout(timeout))
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Authorization
|
|
430
|
+
|
|
431
|
+
`with_auth` configures HTTP request authorization. All `httpx` authorization mechanisms are supported. You can also implement your own; see the [`httpx` documentation](https://www.python-httpx.org/advanced/authentication/#custom-authentication-schemes).
|
|
432
|
+
|
|
433
|
+
```python
|
|
434
|
+
from httpx import BasicAuth, DigestAuth
|
|
435
|
+
from http_client import HttpClient, options
|
|
436
|
+
|
|
437
|
+
# Basic authorization
|
|
438
|
+
client = HttpClient("https://example.com", options.with_auth(BasicAuth(username="user", password="password")))
|
|
439
|
+
|
|
440
|
+
# Digest authorization
|
|
441
|
+
client = HttpClient("https://example.com", options.with_auth(DigestAuth(username="user", password="password")))
|
|
442
|
+
|
|
443
|
+
# Custom Bearer token authorization
|
|
444
|
+
client = HttpClient(
|
|
445
|
+
"https://example.com", options.with_auth(lambda r: r.headers.update({"Authorization": f"Bearer {token}"}))
|
|
446
|
+
)
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Request identification
|
|
450
|
+
|
|
451
|
+
`with_autogenerated_request_id` automatically adds a unique identifier to each request's `X-Request-ID` header:
|
|
452
|
+
|
|
453
|
+
```python
|
|
454
|
+
from http_client import HttpClient, options
|
|
455
|
+
|
|
456
|
+
# Automatically generate X-Request-ID
|
|
457
|
+
client = HttpClient("https://example.com", options.with_autogenerated_request_id())
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
This is useful for tracing requests across microservices and for debugging.
|
|
461
|
+
|
|
462
|
+
### Tracing
|
|
463
|
+
|
|
464
|
+
`with_autogenerated_traceparent` automatically adds a `traceparent` tracing header compatible with W3C Trace Context:
|
|
465
|
+
|
|
466
|
+
```python
|
|
467
|
+
from http_client import HttpClient, options
|
|
468
|
+
|
|
469
|
+
# Add a traceparent header to every request
|
|
470
|
+
client = HttpClient("https://example.com", options.with_autogenerated_traceparent())
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
This lets you track requests across multiple services using different monitoring systems.
|
|
474
|
+
|
|
475
|
+
### Logging
|
|
476
|
+
|
|
477
|
+
`with_logger` configures HTTP request and response logging. Set shared defaults with a `LoggingConfig` object:
|
|
478
|
+
|
|
479
|
+
```python
|
|
480
|
+
from http_client import configure_logging
|
|
481
|
+
from http_client.config import LoggingConfig
|
|
482
|
+
|
|
483
|
+
configure_logging(options=LoggingConfig(json_indent=2, use_line_breaks=True))
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Pass client-specific settings to `with_logger`:
|
|
487
|
+
|
|
488
|
+
```python
|
|
489
|
+
import logging
|
|
490
|
+
from http_client import HttpClient, options
|
|
491
|
+
|
|
492
|
+
# Configure a custom logger
|
|
493
|
+
logger = logging.getLogger("http_client")
|
|
494
|
+
|
|
495
|
+
client = HttpClient(
|
|
496
|
+
"https://example.com",
|
|
497
|
+
options.with_logger(
|
|
498
|
+
logger,
|
|
499
|
+
sanitize_keys=["authorization", "password"], # hide secret data
|
|
500
|
+
),
|
|
501
|
+
)
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
`sanitize_keys` hides sensitive information in logs, such as authorization tokens and passwords.
|
|
505
|
+
|
|
506
|
+
Common secrets are masked **by default**, even without `sanitize_keys`: `authorization`, `proxy-authorization`, `cookie`, `set-cookie`, `x-api-key`, `api-key`, `api_key`, `access_token`, `refresh_token`, `password`, `secret`, `token`. Keys from `sanitize_keys` extend this list.
|
|
507
|
+
|
|
508
|
+
Disable the default protection with an explicit flag, for example when debugging in a private environment:
|
|
509
|
+
|
|
510
|
+
```python
|
|
511
|
+
options.with_logger(logger, use_default_sanitize_keys=False)
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
⚠️ Masking applies only to access log records written by this library. `httpx` exceptions may contain the full URL, including query parameters, so do not pass secrets in URLs.
|
|
515
|
+
|
|
516
|
+
### Closing connections
|
|
517
|
+
|
|
518
|
+
The connection pool is created in the client constructor, so close the client. A context manager is the simplest way:
|
|
519
|
+
|
|
520
|
+
```python
|
|
521
|
+
with HttpClient("https://example.com") as client:
|
|
522
|
+
client.get("/api/resource")
|
|
523
|
+
|
|
524
|
+
async with AsyncHttpClient("https://example.com") as client:
|
|
525
|
+
await client.get("/api/resource")
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
For a long-lived client created once per application, use explicit methods:
|
|
529
|
+
|
|
530
|
+
```python
|
|
531
|
+
client.close() # HttpClient
|
|
532
|
+
await client.aclose() # AsyncHttpClient
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
A closed client cannot be reopened: like `httpx`, this raises `RuntimeError`.
|
|
536
|
+
|
|
537
|
+
### TLS connection
|
|
538
|
+
|
|
539
|
+
The library provides two options to configure TLS connections. Both work the same with `HttpClient` and `AsyncHttpClient`.
|
|
540
|
+
|
|
541
|
+
1. `with_verify_context` — sets the TLS verification context:
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
import ssl
|
|
545
|
+
from http_client import HttpClient, options
|
|
546
|
+
|
|
547
|
+
# Create an SSL context
|
|
548
|
+
ssl_context = ssl.create_default_context()
|
|
549
|
+
ssl_context.load_verify_locations("/path/to/ca-certificates.crt")
|
|
550
|
+
|
|
551
|
+
# Set the SSL context
|
|
552
|
+
client = HttpClient("https://example.com", options.with_verify_context(verify=ssl_context))
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
2. `use_truststore` — uses the system certificate store for verification:
|
|
556
|
+
|
|
557
|
+
```python
|
|
558
|
+
from http_client import HttpClient, options
|
|
559
|
+
|
|
560
|
+
# Use the system certificate store
|
|
561
|
+
client = HttpClient("https://example.com", options.use_truststore())
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
To use `use_truststore`, install the extra dependency:
|
|
565
|
+
```
|
|
566
|
+
pip install "cloudru-qa-http-client[truststore]"
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### Default headers
|
|
570
|
+
|
|
571
|
+
`with_default_headers` sets default headers sent with all client requests.
|
|
572
|
+
|
|
573
|
+
```python
|
|
574
|
+
from http_client import HttpClient, options
|
|
575
|
+
|
|
576
|
+
headers = {
|
|
577
|
+
"X-My-Custom-Header": "Hello, world!",
|
|
578
|
+
"X-Another-Header": "Another value",
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
client = HttpClient(
|
|
582
|
+
"https://example.com",
|
|
583
|
+
options.with_default_headers(headers),
|
|
584
|
+
)
|
|
585
|
+
```
|
|
586
|
+
## Development
|
|
587
|
+
|
|
588
|
+
The library is developed in the [qa-libraries](https://github.com/cloud-ru-tech/qa-libraries) monorepo
|
|
589
|
+
alongside the other `cloudru-qa-*` libraries. You need [`uv`](https://docs.astral.sh/uv/) 0.11.14
|
|
590
|
+
or later, GNU Make, and [`checkmake`](https://github.com/checkmake/checkmake). Run commands from the repository root:
|
|
591
|
+
|
|
592
|
+
```shell
|
|
593
|
+
make sync # install dependencies for all libraries from uv.lock
|
|
594
|
+
make test-http-client # tests for this library
|
|
595
|
+
make typecheck-http-client # mypy for this library
|
|
596
|
+
make check # formatting, ruff, mypy, and tests for the entire workspace
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
⚠️ **Keep** the library lightweight and general-purpose. Discuss new dependencies
|
|
600
|
+
only when you are sure all library users need them.
|
|
601
|
+
|
|
602
|
+
Tests run on Python 3.13 and 3.14. Create a separate environment for another version:
|
|
603
|
+
`make sync test-http-client PYTHON_VERSION=3.14`.
|
|
604
|
+
|
|
605
|
+
Demo scenarios with real requests to local [go-httpbin](https://github.com/mccutchen/go-httpbin)
|
|
606
|
+
are in `examples/httpbin`. Docker must be running; run commands from `packages/http-client`:
|
|
607
|
+
|
|
608
|
+
```shell
|
|
609
|
+
docker compose up -d
|
|
610
|
+
uv run --no-sync python -m examples.httpbin
|
|
611
|
+
docker compose down
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
Publish a release with the git tag `http-client/v<version>`. The tag version must match the version
|
|
615
|
+
in `packages/http-client/pyproject.toml`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
http_client/__init__.py,sha256=Lk6JzOwSSOWbM-zS9zVUj_iYKfLyA6cpJnohQ4orins,353
|
|
2
|
+
http_client/client.py,sha256=4hLs10MVNUcMABALr7GfzXqSJLEjMwciNTYhzRIMs68,22808
|
|
3
|
+
http_client/config.py,sha256=WzRLmXicM0skU3g0mxhxhMSm9d9epDS6XhjVkIWbPv0,3229
|
|
4
|
+
http_client/event_hooks.py,sha256=vuLCdI8jDDHMsL_2noUPGTA1xa-48cn1domoqd1hJGk,4836
|
|
5
|
+
http_client/options.py,sha256=CmD7sFqGAemlmT-MTUkpvy_IUOw1bAFUDwQtqdfCt78,7473
|
|
6
|
+
http_client/py.typed,sha256=2V-BXHpFdhiPP7ymH5gngkCEyIkBSKRBl5jxiG1YHEM,81
|
|
7
|
+
http_client/types.py,sha256=Xv14tsiQ7AGVTceJMQFdhH23yrhutDidb6q8Z-P-EbE,4010
|
|
8
|
+
http_client/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
http_client/utils/logutils.py,sha256=89Rs7tSbfyiub9oN-pwQ_xBdlfEn6wYAj8QBF6KlQuU,8736
|
|
10
|
+
http_client/utils/sanitizer.py,sha256=R-Nc_ymZkmZMaVCdZOnVthYa5AIAPyoygoJaJZEaCEA,2025
|
|
11
|
+
http_client/utils/traceparent.py,sha256=oZXVJ8eLYzNLbAbA0oPmUgfQGf_tVH241TbIG1DHz8E,329
|
|
12
|
+
http_client/.agents/skills/http-client-usage/SKILL.md,sha256=06XX2L-b2xmdKeWFvo5T0auc-8WquqKE87CcOjXqjtY,10007
|
|
13
|
+
http_client/.agents/skills/http-client-usage/references/options.md,sha256=wDtsM4X-iqp1A8Jj-FQVcfRNqH_nQVBo2H6hhI04Mz0,10384
|
|
14
|
+
http_client/.agents/skills/http-client-usage/references/pitfalls.md,sha256=9kRbecUGZbTmHJF-pXPCM5kiAeTPiDnBHTNVsTXY_g4,2733
|
|
15
|
+
cloudru_qa_http_client-1.0.0.dist-info/METADATA,sha256=a8ye2349RUpWmnUxZOWmJ79iLqPGbXvSI1GgS1HSamo,19893
|
|
16
|
+
cloudru_qa_http_client-1.0.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
17
|
+
cloudru_qa_http_client-1.0.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
18
|
+
cloudru_qa_http_client-1.0.0.dist-info/RECORD,,
|