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.
@@ -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).