uhttp-client 2.2.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.
- uhttp_client-2.2.0/LICENSE +21 -0
- uhttp_client-2.2.0/PKG-INFO +615 -0
- uhttp_client-2.2.0/README.md +598 -0
- uhttp_client-2.2.0/pyproject.toml +27 -0
- uhttp_client-2.2.0/setup.cfg +4 -0
- uhttp_client-2.2.0/tests/test_async.py +186 -0
- uhttp_client-2.2.0/tests/test_auth.py +255 -0
- uhttp_client-2.2.0/tests/test_basic.py +254 -0
- uhttp_client-2.2.0/tests/test_cookies.py +137 -0
- uhttp_client-2.2.0/tests/test_errors.py +187 -0
- uhttp_client-2.2.0/tests/test_keepalive.py +109 -0
- uhttp_client-2.2.0/tests/test_unit.py +540 -0
- uhttp_client-2.2.0/uhttp/client.py +847 -0
- uhttp_client-2.2.0/uhttp_client.egg-info/PKG-INFO +615 -0
- uhttp_client-2.2.0/uhttp_client.egg-info/SOURCES.txt +15 -0
- uhttp_client-2.2.0/uhttp_client.egg-info/dependency_links.txt +1 -0
- uhttp_client-2.2.0/uhttp_client.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pavel Revak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uhttp-client
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: Micro HTTP client for Python and MicroPython
|
|
5
|
+
Author-email: Pavel Revak <pavelrevak@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pavelrevak/uhttp
|
|
8
|
+
Project-URL: Repository, https://github.com/pavelrevak/uhttp
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: Implementation :: MicroPython
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.7
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# uHTTP Client: micro HTTP client
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- MicroPython and CPython compatible
|
|
24
|
+
- Select-based async (no async/await, no threading)
|
|
25
|
+
- Keep-alive connections with automatic reuse
|
|
26
|
+
- Fluent API: `response = client.get('/path').wait()`
|
|
27
|
+
- URL parsing with automatic SSL detection
|
|
28
|
+
- Base path support for API versioning
|
|
29
|
+
- JSON support (auto-encode request, lazy decode response)
|
|
30
|
+
- Binary data support
|
|
31
|
+
- Cookies persistence
|
|
32
|
+
- HTTP Basic and Digest authentication
|
|
33
|
+
- SSL/TLS support for HTTPS
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### URL-based initialization (recommended)
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import uhttp.client
|
|
42
|
+
|
|
43
|
+
# HTTPS with automatic SSL context
|
|
44
|
+
client = uhttp.client.HttpClient('https://api.example.com')
|
|
45
|
+
response = client.get('/users').wait()
|
|
46
|
+
client.close()
|
|
47
|
+
|
|
48
|
+
# With base path for API versioning
|
|
49
|
+
client = uhttp.client.HttpClient('https://api.example.com/v1')
|
|
50
|
+
response = client.get('/users').wait() # requests /v1/users
|
|
51
|
+
client.close()
|
|
52
|
+
|
|
53
|
+
# HTTP
|
|
54
|
+
client = uhttp.client.HttpClient('http://localhost:8080')
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Traditional initialization
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import uhttp.client
|
|
61
|
+
|
|
62
|
+
client = uhttp.client.HttpClient('httpbin.org', port=80)
|
|
63
|
+
response = client.get('/get').wait()
|
|
64
|
+
client.close()
|
|
65
|
+
|
|
66
|
+
# With explicit SSL context
|
|
67
|
+
import ssl
|
|
68
|
+
ctx = ssl.create_default_context()
|
|
69
|
+
client = uhttp.client.HttpClient('api.example.com', port=443, ssl_context=ctx)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Context manager
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import uhttp.client
|
|
76
|
+
|
|
77
|
+
with uhttp.client.HttpClient('https://httpbin.org') as client:
|
|
78
|
+
response = client.get('/get').wait()
|
|
79
|
+
print(response.status)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### JSON API
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
client = uhttp.client.HttpClient('https://api.example.com/v1')
|
|
86
|
+
|
|
87
|
+
# GET with query parameters
|
|
88
|
+
response = client.get('/users', query={'page': 1, 'limit': 10}).wait()
|
|
89
|
+
|
|
90
|
+
# POST with JSON body
|
|
91
|
+
response = client.post('/users', json={'name': 'John'}).wait()
|
|
92
|
+
|
|
93
|
+
# PUT
|
|
94
|
+
response = client.put('/users/1', json={'name': 'Jane'}).wait()
|
|
95
|
+
|
|
96
|
+
# DELETE
|
|
97
|
+
response = client.delete('/users/1').wait()
|
|
98
|
+
|
|
99
|
+
client.close()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Custom headers
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
response = client.get('/protected', headers={
|
|
106
|
+
'Authorization': 'Bearer token123',
|
|
107
|
+
'X-Custom-Header': 'value'
|
|
108
|
+
}).wait()
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Binary data
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# Send binary
|
|
115
|
+
response = client.post('/upload', data=b'\x00\x01\x02\xff').wait()
|
|
116
|
+
|
|
117
|
+
# Receive binary
|
|
118
|
+
response = client.get('/image.png').wait()
|
|
119
|
+
image_bytes = response.data
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## HTTPS
|
|
124
|
+
|
|
125
|
+
### Automatic (with URL)
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
import uhttp.client
|
|
129
|
+
|
|
130
|
+
# SSL context created automatically for https:// URLs
|
|
131
|
+
client = uhttp.client.HttpClient('https://api.example.com')
|
|
132
|
+
response = client.get('/secure').wait()
|
|
133
|
+
client.close()
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Manual SSL context
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
import ssl
|
|
140
|
+
import uhttp.client
|
|
141
|
+
|
|
142
|
+
ctx = ssl.create_default_context()
|
|
143
|
+
client = uhttp.client.HttpClient('api.example.com', port=443, ssl_context=ctx)
|
|
144
|
+
response = client.get('/secure').wait()
|
|
145
|
+
client.close()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### MicroPython HTTPS
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
import ssl
|
|
152
|
+
import uhttp.client
|
|
153
|
+
|
|
154
|
+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
155
|
+
client = uhttp.client.HttpClient('api.example.com', port=443, ssl_context=ctx)
|
|
156
|
+
response = client.get('/secure').wait()
|
|
157
|
+
client.close()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
## Async (non-blocking) mode
|
|
162
|
+
|
|
163
|
+
Default mode is async. Use with external select loop:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
import select
|
|
167
|
+
import uhttp.client
|
|
168
|
+
|
|
169
|
+
client = uhttp.client.HttpClient('http://httpbin.org')
|
|
170
|
+
|
|
171
|
+
# Start request (non-blocking)
|
|
172
|
+
client.get('/delay/2')
|
|
173
|
+
|
|
174
|
+
# Manual select loop
|
|
175
|
+
while True:
|
|
176
|
+
r, w, _ = select.select(
|
|
177
|
+
client.read_sockets,
|
|
178
|
+
client.write_sockets,
|
|
179
|
+
[], 10.0
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
response = client.process_events(r, w)
|
|
183
|
+
if response:
|
|
184
|
+
print(response.status)
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
client.close()
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Parallel requests
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
import select
|
|
194
|
+
import uhttp.client
|
|
195
|
+
|
|
196
|
+
clients = [
|
|
197
|
+
uhttp.client.HttpClient('http://httpbin.org'),
|
|
198
|
+
uhttp.client.HttpClient('http://httpbin.org'),
|
|
199
|
+
uhttp.client.HttpClient('http://httpbin.org'),
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
# Start all requests
|
|
203
|
+
for i, client in enumerate(clients):
|
|
204
|
+
client.get('/delay/1', query={'n': i})
|
|
205
|
+
|
|
206
|
+
# Wait for all
|
|
207
|
+
results = {}
|
|
208
|
+
while len(results) < len(clients):
|
|
209
|
+
read_socks = []
|
|
210
|
+
write_socks = []
|
|
211
|
+
for c in clients:
|
|
212
|
+
read_socks.extend(c.read_sockets)
|
|
213
|
+
write_socks.extend(c.write_sockets)
|
|
214
|
+
|
|
215
|
+
r, w, _ = select.select(read_socks, write_socks, [], 10.0)
|
|
216
|
+
|
|
217
|
+
for i, client in enumerate(clients):
|
|
218
|
+
if i not in results:
|
|
219
|
+
resp = client.process_events(r, w)
|
|
220
|
+
if resp:
|
|
221
|
+
results[i] = resp
|
|
222
|
+
|
|
223
|
+
for client in clients:
|
|
224
|
+
client.close()
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Combined with HttpServer
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
import select
|
|
231
|
+
import uhttp.server
|
|
232
|
+
import uhttp.client
|
|
233
|
+
|
|
234
|
+
server = uhttp.server.HttpServer(port=8080)
|
|
235
|
+
backend = uhttp.client.HttpClient('http://api.example.com')
|
|
236
|
+
|
|
237
|
+
while True:
|
|
238
|
+
r, w, _ = select.select(
|
|
239
|
+
server.read_sockets + backend.read_sockets,
|
|
240
|
+
server.write_sockets + backend.write_sockets,
|
|
241
|
+
[], 1.0
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Handle incoming requests
|
|
245
|
+
incoming = server.process_events(r, w)
|
|
246
|
+
if incoming:
|
|
247
|
+
backend.get('/data', query=incoming.query)
|
|
248
|
+
|
|
249
|
+
# Handle backend response
|
|
250
|
+
response = backend.process_events(r, w)
|
|
251
|
+
if response:
|
|
252
|
+
incoming.respond(data=response.data)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
## API
|
|
257
|
+
|
|
258
|
+
### Function `parse_url`
|
|
259
|
+
|
|
260
|
+
**`uhttp.client.parse_url(url)`**
|
|
261
|
+
|
|
262
|
+
Parse URL into components. Returns `(host, port, path, ssl, auth)` tuple.
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
import uhttp.client
|
|
266
|
+
|
|
267
|
+
uhttp.client.parse_url('https://api.example.com/v1/users')
|
|
268
|
+
# → ('api.example.com', 443, '/v1/users', True, None)
|
|
269
|
+
|
|
270
|
+
uhttp.client.parse_url('http://localhost:8080/api')
|
|
271
|
+
# → ('localhost', 8080, '/api', False, None)
|
|
272
|
+
|
|
273
|
+
uhttp.client.parse_url('https://user:pass@api.example.com')
|
|
274
|
+
# → ('api.example.com', 443, '', True, ('user', 'pass'))
|
|
275
|
+
|
|
276
|
+
uhttp.client.parse_url('example.com')
|
|
277
|
+
# → ('example.com', 80, '', False, None)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
### Class `HttpClient`
|
|
282
|
+
|
|
283
|
+
**`uhttp.client.HttpClient(url_or_host, port=None, ssl_context=None, auth=None, connect_timeout=10, timeout=30, max_response_length=1MB)`**
|
|
284
|
+
|
|
285
|
+
Can be initialized with URL or host/port:
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
import uhttp.client
|
|
289
|
+
|
|
290
|
+
# URL-based (recommended)
|
|
291
|
+
uhttp.client.HttpClient('https://api.example.com/v1')
|
|
292
|
+
|
|
293
|
+
# With auth in URL
|
|
294
|
+
uhttp.client.HttpClient('https://user:pass@api.example.com/v1')
|
|
295
|
+
|
|
296
|
+
# Traditional
|
|
297
|
+
uhttp.client.HttpClient('api.example.com', port=443, ssl_context=ctx)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Parameters:
|
|
301
|
+
- `url_or_host` - Full URL (http://... or https://...) or hostname
|
|
302
|
+
- `port` - Server port (auto-detected from URL: 80 for http, 443 for https)
|
|
303
|
+
- `ssl_context` - Optional `ssl.SSLContext` (auto-created for https:// URLs)
|
|
304
|
+
- `auth` - Optional (username, password) tuple for HTTP authentication
|
|
305
|
+
- `connect_timeout` - Connection timeout in seconds (default: 10)
|
|
306
|
+
- `timeout` - Response timeout in seconds (default: 30)
|
|
307
|
+
- `max_response_length` - Maximum response size (default: 1MB)
|
|
308
|
+
|
|
309
|
+
#### Properties
|
|
310
|
+
|
|
311
|
+
- `host` - Server hostname
|
|
312
|
+
- `port` - Server port
|
|
313
|
+
- `base_path` - Base path from URL (prepended to all request paths)
|
|
314
|
+
- `is_connected` - True if socket is connected
|
|
315
|
+
- `state` - Current state (STATE_IDLE, STATE_SENDING, etc.)
|
|
316
|
+
- `auth` - Authentication credentials tuple (username, password) or None
|
|
317
|
+
- `cookies` - Cookies dict (persistent across requests)
|
|
318
|
+
- `read_sockets` - Sockets to monitor for reading (for select)
|
|
319
|
+
- `write_sockets` - Sockets to monitor for writing (for select)
|
|
320
|
+
|
|
321
|
+
#### Methods
|
|
322
|
+
|
|
323
|
+
**`request(method, path, headers=None, data=None, query=None, json=None, auth=None, timeout=None)`**
|
|
324
|
+
|
|
325
|
+
Start HTTP request (async). Returns `self` for chaining.
|
|
326
|
+
|
|
327
|
+
- `method` - HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
328
|
+
- `path` - Request path (base_path is prepended automatically)
|
|
329
|
+
- `headers` - Optional headers dict
|
|
330
|
+
- `data` - Request body (bytes, str, or dict/list for JSON)
|
|
331
|
+
- `query` - Optional query parameters dict
|
|
332
|
+
- `json` - Shortcut for data with JSON encoding
|
|
333
|
+
- `auth` - Optional (username, password) tuple, overrides client's default auth
|
|
334
|
+
- `timeout` - Optional timeout in seconds, overrides client's default timeout
|
|
335
|
+
|
|
336
|
+
**`get(path, **kwargs)`** - Send GET request
|
|
337
|
+
|
|
338
|
+
**`post(path, **kwargs)`** - Send POST request
|
|
339
|
+
|
|
340
|
+
**`put(path, **kwargs)`** - Send PUT request
|
|
341
|
+
|
|
342
|
+
**`delete(path, **kwargs)`** - Send DELETE request
|
|
343
|
+
|
|
344
|
+
**`head(path, **kwargs)`** - Send HEAD request
|
|
345
|
+
|
|
346
|
+
**`patch(path, **kwargs)`** - Send PATCH request
|
|
347
|
+
|
|
348
|
+
**`wait(timeout=None)`**
|
|
349
|
+
|
|
350
|
+
Wait for response (blocking). Returns `HttpResponse` when complete.
|
|
351
|
+
|
|
352
|
+
- `timeout` - Max time to spend in wait() call. If `None`, uses request timeout.
|
|
353
|
+
- Returns `None` if wait timeout expires (connection stays open, can call again).
|
|
354
|
+
- Raises `HttpTimeoutError` if request timeout expires (connection closed).
|
|
355
|
+
|
|
356
|
+
**`process_events(read_sockets, write_sockets)`**
|
|
357
|
+
|
|
358
|
+
Process select events. Returns `HttpResponse` when complete, `None` otherwise.
|
|
359
|
+
|
|
360
|
+
- First processes any ready data, then checks request timeout.
|
|
361
|
+
- Raises `HttpTimeoutError` if request timeout has expired and no complete response.
|
|
362
|
+
|
|
363
|
+
**`close()`**
|
|
364
|
+
|
|
365
|
+
Close connection.
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
### Class `HttpResponse`
|
|
369
|
+
|
|
370
|
+
#### Properties
|
|
371
|
+
|
|
372
|
+
- `status` - HTTP status code (int)
|
|
373
|
+
- `status_message` - HTTP status message (str)
|
|
374
|
+
- `headers` - Response headers dict (keys are lowercase)
|
|
375
|
+
- `data` - Response body as bytes
|
|
376
|
+
- `content_type` - Content-Type header value
|
|
377
|
+
- `content_length` - Content-Length header value
|
|
378
|
+
|
|
379
|
+
#### Methods
|
|
380
|
+
|
|
381
|
+
**`json()`**
|
|
382
|
+
|
|
383
|
+
Parse response body as JSON. Lazy evaluation, cached.
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
## Authentication
|
|
387
|
+
|
|
388
|
+
### Basic Auth
|
|
389
|
+
|
|
390
|
+
HTTP Basic authentication via URL or `auth` parameter:
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
import uhttp.client
|
|
394
|
+
|
|
395
|
+
# Via URL
|
|
396
|
+
client = uhttp.client.HttpClient('https://user:password@api.example.com')
|
|
397
|
+
response = client.get('/protected').wait()
|
|
398
|
+
|
|
399
|
+
# Via parameter
|
|
400
|
+
client = uhttp.client.HttpClient('https://api.example.com', auth=('user', 'password'))
|
|
401
|
+
response = client.get('/protected').wait()
|
|
402
|
+
|
|
403
|
+
# Change auth at runtime
|
|
404
|
+
client.auth = ('new_user', 'new_password')
|
|
405
|
+
|
|
406
|
+
# Per-request auth (overrides client's default)
|
|
407
|
+
client = uhttp.client.HttpClient('https://api.example.com')
|
|
408
|
+
response = client.get('/admin', auth=('admin', 'secret')).wait()
|
|
409
|
+
response = client.get('/public').wait() # no auth
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Digest Auth
|
|
413
|
+
|
|
414
|
+
HTTP Digest authentication is handled automatically. On 401 response with
|
|
415
|
+
`WWW-Authenticate: Digest` header, the client retries with digest credentials:
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
import uhttp.client
|
|
419
|
+
|
|
420
|
+
# Same API as Basic auth - digest is automatic
|
|
421
|
+
client = uhttp.client.HttpClient('https://api.example.com', auth=('user', 'password'))
|
|
422
|
+
|
|
423
|
+
# First request gets 401, client automatically retries with digest auth
|
|
424
|
+
response = client.get('/protected').wait()
|
|
425
|
+
print(response.status) # 200 (after automatic retry)
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Supported digest features:
|
|
429
|
+
- MD5 and MD5-sess algorithms
|
|
430
|
+
- qop (quality of protection) with auth mode
|
|
431
|
+
- Nonce counting for multiple requests
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
## Cookies
|
|
435
|
+
|
|
436
|
+
Cookies are automatically:
|
|
437
|
+
- Stored from `Set-Cookie` response headers
|
|
438
|
+
- Sent with subsequent requests
|
|
439
|
+
|
|
440
|
+
```python
|
|
441
|
+
import uhttp.client
|
|
442
|
+
|
|
443
|
+
client = uhttp.client.HttpClient('https://example.com')
|
|
444
|
+
|
|
445
|
+
# Login - server sets session cookie
|
|
446
|
+
client.post('/login', json={'user': 'admin', 'pass': 'secret'}).wait()
|
|
447
|
+
|
|
448
|
+
# Subsequent requests include the cookie automatically
|
|
449
|
+
response = client.get('/dashboard').wait()
|
|
450
|
+
|
|
451
|
+
# Access cookies
|
|
452
|
+
print(client.cookies) # {'session': 'abc123'}
|
|
453
|
+
|
|
454
|
+
client.close()
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
## Keep-Alive
|
|
459
|
+
|
|
460
|
+
Connections are reused automatically (HTTP/1.1 keep-alive).
|
|
461
|
+
|
|
462
|
+
```python
|
|
463
|
+
import uhttp.client
|
|
464
|
+
|
|
465
|
+
client = uhttp.client.HttpClient('https://httpbin.org')
|
|
466
|
+
|
|
467
|
+
# All requests use the same connection
|
|
468
|
+
for i in range(10):
|
|
469
|
+
response = client.get('/get', query={'n': i}).wait()
|
|
470
|
+
print(f"Request {i}: {response.status}")
|
|
471
|
+
|
|
472
|
+
client.close()
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
## Timeouts
|
|
477
|
+
|
|
478
|
+
Two types of timeouts:
|
|
479
|
+
|
|
480
|
+
### Request timeout
|
|
481
|
+
|
|
482
|
+
Total time allowed for the request. Set via `timeout` parameter on client or per-request.
|
|
483
|
+
When expired, raises `HttpTimeoutError` and closes connection.
|
|
484
|
+
|
|
485
|
+
```python
|
|
486
|
+
import uhttp.client
|
|
487
|
+
|
|
488
|
+
# Client-level timeout (default for all requests)
|
|
489
|
+
client = uhttp.client.HttpClient('https://example.com', timeout=30)
|
|
490
|
+
|
|
491
|
+
# Per-request timeout (overrides client default)
|
|
492
|
+
response = client.get('/slow', timeout=60).wait()
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Wait timeout
|
|
496
|
+
|
|
497
|
+
Time to spend in `wait()` call. When expired, returns `None` but keeps connection open.
|
|
498
|
+
Useful for polling or interleaving with other work.
|
|
499
|
+
|
|
500
|
+
```python
|
|
501
|
+
import uhttp.client
|
|
502
|
+
|
|
503
|
+
client = uhttp.client.HttpClient('https://example.com', timeout=60) # request timeout
|
|
504
|
+
client.get('/slow')
|
|
505
|
+
|
|
506
|
+
# Try for 5 seconds, then do something else
|
|
507
|
+
response = client.wait(timeout=5)
|
|
508
|
+
if response is None:
|
|
509
|
+
print("Still waiting, doing other work...")
|
|
510
|
+
# Can call wait() again
|
|
511
|
+
response = client.wait(timeout=10)
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
## Error handling
|
|
516
|
+
|
|
517
|
+
```python
|
|
518
|
+
import uhttp.client
|
|
519
|
+
|
|
520
|
+
client = uhttp.client.HttpClient('https://example.com')
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
response = client.get('/api').wait()
|
|
524
|
+
except uhttp.client.HttpConnectionError as e:
|
|
525
|
+
print(f"Connection failed: {e}")
|
|
526
|
+
except uhttp.client.HttpTimeoutError as e:
|
|
527
|
+
print(f"Timeout: {e}")
|
|
528
|
+
except uhttp.client.HttpResponseError as e:
|
|
529
|
+
print(f"Invalid response: {e}")
|
|
530
|
+
except uhttp.client.HttpClientError as e:
|
|
531
|
+
print(f"Client error: {e}")
|
|
532
|
+
finally:
|
|
533
|
+
client.close()
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
## Configuration constants
|
|
538
|
+
|
|
539
|
+
```python
|
|
540
|
+
CONNECT_TIMEOUT = 10 # seconds
|
|
541
|
+
TIMEOUT = 30 # seconds
|
|
542
|
+
MAX_RESPONSE_HEADERS_LENGTH = 4KB
|
|
543
|
+
MAX_RESPONSE_LENGTH = 1MB
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
## Examples
|
|
548
|
+
|
|
549
|
+
See [examples/](../examples/) directory:
|
|
550
|
+
- `client_basic.py` - Basic blocking examples
|
|
551
|
+
- `client_https.py` - HTTPS examples
|
|
552
|
+
- `client_async.py` - Async select loop examples
|
|
553
|
+
- `client_with_server.py` - Combined server + client examples
|
|
554
|
+
|
|
555
|
+
Run examples from project root:
|
|
556
|
+
```bash
|
|
557
|
+
PYTHONPATH=./server:./client python examples/client_basic.py
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
## CLI Tool
|
|
562
|
+
|
|
563
|
+
Simple curl-like CLI tool using uhttp.client:
|
|
564
|
+
|
|
565
|
+
```bash
|
|
566
|
+
# GET request
|
|
567
|
+
python -m tools.httpcl http://httpbin.org/get
|
|
568
|
+
|
|
569
|
+
# POST with JSON
|
|
570
|
+
python -m tools.httpcl http://httpbin.org/post -j '{"key": "value"}'
|
|
571
|
+
|
|
572
|
+
# Verbose mode
|
|
573
|
+
python -m tools.httpcl -v https://httpbin.org/get
|
|
574
|
+
|
|
575
|
+
# Save to file
|
|
576
|
+
python -m tools.httpcl https://httpbin.org/image/png -o image.png
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
See `python -m tools.httpcl --help` for all options.
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
## IPv6 Support
|
|
583
|
+
|
|
584
|
+
Client supports both IPv4 and IPv6:
|
|
585
|
+
- Automatically tries all addresses returned by `getaddrinfo()` (IPv4 and IPv6)
|
|
586
|
+
- Works with hostnames like `localhost` on all systems
|
|
587
|
+
|
|
588
|
+
```python
|
|
589
|
+
import uhttp.client
|
|
590
|
+
|
|
591
|
+
# Works on all systems (IPv4 or IPv6)
|
|
592
|
+
client = uhttp.client.HttpClient('http://localhost:8080')
|
|
593
|
+
|
|
594
|
+
# Explicit IPv4
|
|
595
|
+
client = uhttp.client.HttpClient('http://127.0.0.1:8080')
|
|
596
|
+
|
|
597
|
+
# Explicit IPv6
|
|
598
|
+
client = uhttp.client.HttpClient('http://[::1]:8080')
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
## Development
|
|
603
|
+
|
|
604
|
+
### Running tests
|
|
605
|
+
|
|
606
|
+
```bash
|
|
607
|
+
../.venv/bin/pip install -e .
|
|
608
|
+
../.venv/bin/python -m unittest discover -v tests/
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
For running tests from meta-repo, see [uhttp README](https://github.com/pavelrevak/uhttp#testing).
|
|
612
|
+
|
|
613
|
+
### CI
|
|
614
|
+
|
|
615
|
+
Tests run automatically on push/PR via GitHub Actions (Ubuntu + Windows, Python 3.10 + 3.14).
|