uhttp-workers 1.4.0__tar.gz → 1.5.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_workers-1.4.0 → uhttp_workers-1.5.0}/PKG-INFO +27 -2
- uhttp_workers-1.4.0/uhttp_workers.egg-info/PKG-INFO → uhttp_workers-1.5.0/README.md +25 -14
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/pyproject.toml +1 -1
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/test_dispatcher.py +43 -1
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/test_worker.py +33 -1
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/uhttp/workers.py +27 -0
- uhttp_workers-1.4.0/README.md → uhttp_workers-1.5.0/uhttp_workers.egg-info/PKG-INFO +39 -0
- uhttp_workers-1.5.0/uhttp_workers.egg-info/requires.txt +1 -0
- uhttp_workers-1.4.0/uhttp_workers.egg-info/requires.txt +0 -1
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/.github/workflows/publish.yml +0 -0
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/.github/workflows/tests.yml +0 -0
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/.gitignore +0 -0
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/examples/simple_workers.py +0 -0
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/examples/sse_workers.py +0 -0
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/examples/static/index.html +0 -0
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/setup.cfg +0 -0
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/__init__.py +0 -0
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/test_api_handler.py +0 -0
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/test_decorators.py +0 -0
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/test_pattern_matching.py +0 -0
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/test_request_response.py +0 -0
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/test_worker_pool.py +0 -0
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/uhttp_workers.egg-info/SOURCES.txt +0 -0
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/uhttp_workers.egg-info/dependency_links.txt +0 -0
- {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/uhttp_workers.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uhttp-workers
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Multi-process worker dispatcher built on uhttp-server
|
|
5
5
|
Author-email: Pavel Revak <pavelrevak@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
10
10
|
Classifier: Operating System :: POSIX
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
|
-
Requires-Dist: uhttp-server
|
|
13
|
+
Requires-Dist: uhttp-server>=2.5.2
|
|
14
14
|
|
|
15
15
|
# uhttp-workers
|
|
16
16
|
|
|
@@ -384,6 +384,31 @@ Available streaming methods on `Request`:
|
|
|
384
384
|
|
|
385
385
|
Streaming requests are excluded from dispatcher timeout expiration. When the client disconnects, the dispatcher notifies the worker via `on_disconnect(request_id)`.
|
|
386
386
|
|
|
387
|
+
### NDJSON Streaming
|
|
388
|
+
|
|
389
|
+
Stream JSON objects line-by-line (`application/x-ndjson`) — one JSON value per line, terminated by `\n`. Useful for incremental APIs that aren't event-shaped (long lists, log tails, progress updates):
|
|
390
|
+
|
|
391
|
+
```python
|
|
392
|
+
class MyWorker(_workers.Worker):
|
|
393
|
+
@_workers.api('/devices/scan', 'GET')
|
|
394
|
+
def scan(self, request):
|
|
395
|
+
request.response_ndjson()
|
|
396
|
+
for device in self.discover_devices():
|
|
397
|
+
request.send_ndjson({'id': device.id, 'name': device.name})
|
|
398
|
+
request.response_stream_end()
|
|
399
|
+
return _workers.DEFERRED
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
NDJSON methods on `Request`:
|
|
403
|
+
|
|
404
|
+
| Method | Description |
|
|
405
|
+
|--------|-------------|
|
|
406
|
+
| `response_ndjson(headers, cookies)` | Start NDJSON stream (wrapper over `response_stream` with `application/x-ndjson`) |
|
|
407
|
+
| `send_ndjson(obj)` | Send one JSON-serializable value as a line |
|
|
408
|
+
| `response_stream_end()` | End stream and close connection (shared with SSE) |
|
|
409
|
+
|
|
410
|
+
Same lifecycle as SSE: excluded from timeout expiration, client disconnect triggers `on_disconnect(request_id)`.
|
|
411
|
+
|
|
387
412
|
### Flow Control
|
|
388
413
|
|
|
389
414
|
Workers can stop accepting new requests when busy. Requests stay in the shared pool queue for other workers to pick up:
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: uhttp-workers
|
|
3
|
-
Version: 1.4.0
|
|
4
|
-
Summary: Multi-process worker dispatcher built on uhttp-server
|
|
5
|
-
Author-email: Pavel Revak <pavelrevak@gmail.com>
|
|
6
|
-
License-Expression: 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: Operating System :: POSIX
|
|
11
|
-
Requires-Python: >=3.10
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
Requires-Dist: uhttp-server
|
|
14
|
-
|
|
15
1
|
# uhttp-workers
|
|
16
2
|
|
|
17
3
|
Multi-process worker dispatcher built on [uhttp-server](https://github.com/cortexm/uhttp-server).
|
|
@@ -384,6 +370,31 @@ Available streaming methods on `Request`:
|
|
|
384
370
|
|
|
385
371
|
Streaming requests are excluded from dispatcher timeout expiration. When the client disconnects, the dispatcher notifies the worker via `on_disconnect(request_id)`.
|
|
386
372
|
|
|
373
|
+
### NDJSON Streaming
|
|
374
|
+
|
|
375
|
+
Stream JSON objects line-by-line (`application/x-ndjson`) — one JSON value per line, terminated by `\n`. Useful for incremental APIs that aren't event-shaped (long lists, log tails, progress updates):
|
|
376
|
+
|
|
377
|
+
```python
|
|
378
|
+
class MyWorker(_workers.Worker):
|
|
379
|
+
@_workers.api('/devices/scan', 'GET')
|
|
380
|
+
def scan(self, request):
|
|
381
|
+
request.response_ndjson()
|
|
382
|
+
for device in self.discover_devices():
|
|
383
|
+
request.send_ndjson({'id': device.id, 'name': device.name})
|
|
384
|
+
request.response_stream_end()
|
|
385
|
+
return _workers.DEFERRED
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
NDJSON methods on `Request`:
|
|
389
|
+
|
|
390
|
+
| Method | Description |
|
|
391
|
+
|--------|-------------|
|
|
392
|
+
| `response_ndjson(headers, cookies)` | Start NDJSON stream (wrapper over `response_stream` with `application/x-ndjson`) |
|
|
393
|
+
| `send_ndjson(obj)` | Send one JSON-serializable value as a line |
|
|
394
|
+
| `response_stream_end()` | End stream and close connection (shared with SSE) |
|
|
395
|
+
|
|
396
|
+
Same lifecycle as SSE: excluded from timeout expiration, client disconnect triggers `on_disconnect(request_id)`.
|
|
397
|
+
|
|
387
398
|
### Flow Control
|
|
388
399
|
|
|
389
400
|
Workers can stop accepting new requests when busy. Requests stay in the shared pool queue for other workers to pick up:
|
|
@@ -10,7 +10,7 @@ from uhttp.workers import (
|
|
|
10
10
|
Dispatcher, Worker, WorkerPool, Request, Response,
|
|
11
11
|
api, sync, RejectRequest,
|
|
12
12
|
MSG_RESPONSE, MSG_HEARTBEAT,
|
|
13
|
-
MSG_SSE_OPEN, MSG_SSE_EVENT, MSG_SSE_CLOSE,
|
|
13
|
+
MSG_SSE_OPEN, MSG_SSE_EVENT, MSG_SSE_CLOSE, MSG_NDJSON,
|
|
14
14
|
CTL_DISCONNECT,
|
|
15
15
|
PENDING_COMPLETED, PENDING_TIMEOUT, PENDING_DISCONNECTED,
|
|
16
16
|
PENDING_STREAM_CLOSED, PENDING_SHUTDOWN,
|
|
@@ -78,6 +78,12 @@ class MockClient:
|
|
|
78
78
|
self._chunks.append(data)
|
|
79
79
|
return getattr(self, '_connected', True)
|
|
80
80
|
|
|
81
|
+
def send_ndjson(self, obj):
|
|
82
|
+
if not hasattr(self, '_ndjson'):
|
|
83
|
+
self._ndjson = []
|
|
84
|
+
self._ndjson.append(obj)
|
|
85
|
+
return getattr(self, '_connected', True)
|
|
86
|
+
|
|
81
87
|
def response_stream_end(self):
|
|
82
88
|
self.stream_ended = True
|
|
83
89
|
|
|
@@ -547,6 +553,42 @@ class TestDispatcherSSE(unittest.TestCase):
|
|
|
547
553
|
# non-streaming should be expired
|
|
548
554
|
self.assertNotIn(1, d._pending)
|
|
549
555
|
|
|
556
|
+
def test_ndjson_send(self):
|
|
557
|
+
d, pool = self._make_dispatcher()
|
|
558
|
+
client = MockClient('GET', '/api/stream')
|
|
559
|
+
pending = _PendingRequest(client, pool)
|
|
560
|
+
pending.streaming = True
|
|
561
|
+
d._pending[1] = pending
|
|
562
|
+
d._process_response(
|
|
563
|
+
(MSG_NDJSON, 1, {'devices': [1, 2, 3]}))
|
|
564
|
+
self.assertEqual(len(client._ndjson), 1)
|
|
565
|
+
self.assertEqual(client._ndjson[0], {'devices': [1, 2, 3]})
|
|
566
|
+
# still pending — stream open
|
|
567
|
+
self.assertIn(1, d._pending)
|
|
568
|
+
|
|
569
|
+
def test_ndjson_client_disconnect(self):
|
|
570
|
+
d, pool = self._make_dispatcher()
|
|
571
|
+
pool.start(d._response_queue)
|
|
572
|
+
client = MockClient('GET', '/api/stream')
|
|
573
|
+
client._connected = False
|
|
574
|
+
pending = _PendingRequest(client, pool)
|
|
575
|
+
pending.streaming = True
|
|
576
|
+
pending.worker_id = 0
|
|
577
|
+
d._pending[1] = pending
|
|
578
|
+
d._process_response((MSG_NDJSON, 1, {'x': 1}))
|
|
579
|
+
# removed from pending
|
|
580
|
+
self.assertNotIn(1, d._pending)
|
|
581
|
+
# CTL_DISCONNECT sent to worker's control queue
|
|
582
|
+
msg = pool._control_queues[0].get(timeout=1)
|
|
583
|
+
self.assertEqual(msg, (CTL_DISCONNECT, 1))
|
|
584
|
+
pool.shutdown(timeout=2)
|
|
585
|
+
|
|
586
|
+
def test_ndjson_ignored_after_close(self):
|
|
587
|
+
d, pool = self._make_dispatcher()
|
|
588
|
+
# no pending request with id 99
|
|
589
|
+
d._process_response((MSG_NDJSON, 99, {'x': 1}))
|
|
590
|
+
# should not raise, just ignore
|
|
591
|
+
|
|
550
592
|
def test_sse_event_ignored_after_close(self):
|
|
551
593
|
d, pool = self._make_dispatcher()
|
|
552
594
|
# no pending request with id 99
|
|
@@ -8,7 +8,7 @@ from uhttp.workers import (
|
|
|
8
8
|
Worker, Request, Response, api, RejectRequest, DEFERRED,
|
|
9
9
|
Logger, LOG_DEBUG, LOG_INFO, LOG_WARNING, LOG_ERROR,
|
|
10
10
|
MSG_RESPONSE, MSG_HEARTBEAT,
|
|
11
|
-
MSG_SSE_OPEN, MSG_SSE_EVENT, MSG_SSE_CLOSE,
|
|
11
|
+
MSG_SSE_OPEN, MSG_SSE_EVENT, MSG_SSE_CLOSE, MSG_NDJSON,
|
|
12
12
|
CTL_DISCONNECT)
|
|
13
13
|
|
|
14
14
|
|
|
@@ -520,6 +520,38 @@ class TestSSERequest(unittest.TestCase):
|
|
|
520
520
|
self.assertIsNone(msg[4])
|
|
521
521
|
self.assertIsNone(msg[5])
|
|
522
522
|
|
|
523
|
+
def test_response_ndjson(self):
|
|
524
|
+
self.req.response_ndjson(
|
|
525
|
+
headers={'X-Custom': '1'},
|
|
526
|
+
cookies={'sid': 'abc'})
|
|
527
|
+
msg = self.queue.get(timeout=1)
|
|
528
|
+
self.assertEqual(msg[0], MSG_SSE_OPEN)
|
|
529
|
+
self.assertEqual(msg[1], 10)
|
|
530
|
+
self.assertEqual(msg[2], 'application/x-ndjson')
|
|
531
|
+
self.assertEqual(msg[3], {'X-Custom': '1'})
|
|
532
|
+
self.assertEqual(msg[4], {'sid': 'abc'})
|
|
533
|
+
|
|
534
|
+
def test_response_ndjson_defaults(self):
|
|
535
|
+
self.req.response_ndjson()
|
|
536
|
+
msg = self.queue.get(timeout=1)
|
|
537
|
+
self.assertEqual(msg[0], MSG_SSE_OPEN)
|
|
538
|
+
self.assertEqual(msg[2], 'application/x-ndjson')
|
|
539
|
+
self.assertIsNone(msg[3])
|
|
540
|
+
self.assertIsNone(msg[4])
|
|
541
|
+
|
|
542
|
+
def test_send_ndjson(self):
|
|
543
|
+
self.req.send_ndjson({'devices': [1, 2, 3]})
|
|
544
|
+
msg = self.queue.get(timeout=1)
|
|
545
|
+
self.assertEqual(msg[0], MSG_NDJSON)
|
|
546
|
+
self.assertEqual(msg[1], 10)
|
|
547
|
+
self.assertEqual(msg[2], {'devices': [1, 2, 3]})
|
|
548
|
+
|
|
549
|
+
def test_send_ndjson_keepalive(self):
|
|
550
|
+
self.req.send_ndjson({})
|
|
551
|
+
msg = self.queue.get(timeout=1)
|
|
552
|
+
self.assertEqual(msg[0], MSG_NDJSON)
|
|
553
|
+
self.assertEqual(msg[2], {})
|
|
554
|
+
|
|
523
555
|
def test_response_stream_end(self):
|
|
524
556
|
self.req.response_stream_end()
|
|
525
557
|
msg = self.queue.get(timeout=1)
|
|
@@ -23,6 +23,7 @@ MSG_LOG = 'LOG'
|
|
|
23
23
|
MSG_SSE_OPEN = 'SSE_OPEN'
|
|
24
24
|
MSG_SSE_EVENT = 'SSE_EVENT'
|
|
25
25
|
MSG_SSE_CLOSE = 'SSE_CLOSE'
|
|
26
|
+
MSG_NDJSON = 'NDJSON'
|
|
26
27
|
|
|
27
28
|
# Worker control messages
|
|
28
29
|
CTL_STOP = 'STOP'
|
|
@@ -282,6 +283,25 @@ class Request:
|
|
|
282
283
|
(MSG_SSE_EVENT, self.request_id,
|
|
283
284
|
data, event, event_id, retry))
|
|
284
285
|
|
|
286
|
+
def response_ndjson(self, headers=None, cookies=None):
|
|
287
|
+
"""Start NDJSON streaming response (application/x-ndjson).
|
|
288
|
+
|
|
289
|
+
Thin wrapper over response_stream(). Use with DEFERRED — call from
|
|
290
|
+
handler, then send_ndjson() later. Call response_stream_end() to finish.
|
|
291
|
+
"""
|
|
292
|
+
self._response_queue.put(
|
|
293
|
+
(MSG_SSE_OPEN, self.request_id,
|
|
294
|
+
'application/x-ndjson', headers, cookies))
|
|
295
|
+
|
|
296
|
+
def send_ndjson(self, obj):
|
|
297
|
+
"""Send one JSON-serializable object as an NDJSON line.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
obj: any JSON-serializable value (dict/list/str/int/float/bool/None)
|
|
301
|
+
"""
|
|
302
|
+
self._response_queue.put(
|
|
303
|
+
(MSG_NDJSON, self.request_id, obj))
|
|
304
|
+
|
|
285
305
|
def response_stream_end(self):
|
|
286
306
|
"""End streaming response and close connection."""
|
|
287
307
|
self._response_queue.put(
|
|
@@ -1253,6 +1273,13 @@ class Dispatcher:
|
|
|
1253
1273
|
event_id=event_id, retry=retry)
|
|
1254
1274
|
if not ok:
|
|
1255
1275
|
self._stream_disconnected(request_id, pending)
|
|
1276
|
+
elif msg_type == MSG_NDJSON:
|
|
1277
|
+
_, request_id, obj = msg
|
|
1278
|
+
pending = self._pending.get(request_id)
|
|
1279
|
+
if pending is not None:
|
|
1280
|
+
ok = pending.client.send_ndjson(obj)
|
|
1281
|
+
if not ok:
|
|
1282
|
+
self._stream_disconnected(request_id, pending)
|
|
1256
1283
|
elif msg_type == MSG_SSE_CLOSE:
|
|
1257
1284
|
_, request_id = msg
|
|
1258
1285
|
pending = self._pending.pop(request_id, None)
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uhttp-workers
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: Multi-process worker dispatcher built on uhttp-server
|
|
5
|
+
Author-email: Pavel Revak <pavelrevak@gmail.com>
|
|
6
|
+
License-Expression: 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: Operating System :: POSIX
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: uhttp-server>=2.5.2
|
|
14
|
+
|
|
1
15
|
# uhttp-workers
|
|
2
16
|
|
|
3
17
|
Multi-process worker dispatcher built on [uhttp-server](https://github.com/cortexm/uhttp-server).
|
|
@@ -370,6 +384,31 @@ Available streaming methods on `Request`:
|
|
|
370
384
|
|
|
371
385
|
Streaming requests are excluded from dispatcher timeout expiration. When the client disconnects, the dispatcher notifies the worker via `on_disconnect(request_id)`.
|
|
372
386
|
|
|
387
|
+
### NDJSON Streaming
|
|
388
|
+
|
|
389
|
+
Stream JSON objects line-by-line (`application/x-ndjson`) — one JSON value per line, terminated by `\n`. Useful for incremental APIs that aren't event-shaped (long lists, log tails, progress updates):
|
|
390
|
+
|
|
391
|
+
```python
|
|
392
|
+
class MyWorker(_workers.Worker):
|
|
393
|
+
@_workers.api('/devices/scan', 'GET')
|
|
394
|
+
def scan(self, request):
|
|
395
|
+
request.response_ndjson()
|
|
396
|
+
for device in self.discover_devices():
|
|
397
|
+
request.send_ndjson({'id': device.id, 'name': device.name})
|
|
398
|
+
request.response_stream_end()
|
|
399
|
+
return _workers.DEFERRED
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
NDJSON methods on `Request`:
|
|
403
|
+
|
|
404
|
+
| Method | Description |
|
|
405
|
+
|--------|-------------|
|
|
406
|
+
| `response_ndjson(headers, cookies)` | Start NDJSON stream (wrapper over `response_stream` with `application/x-ndjson`) |
|
|
407
|
+
| `send_ndjson(obj)` | Send one JSON-serializable value as a line |
|
|
408
|
+
| `response_stream_end()` | End stream and close connection (shared with SSE) |
|
|
409
|
+
|
|
410
|
+
Same lifecycle as SSE: excluded from timeout expiration, client disconnect triggers `on_disconnect(request_id)`.
|
|
411
|
+
|
|
373
412
|
### Flow Control
|
|
374
413
|
|
|
375
414
|
Workers can stop accepting new requests when busy. Requests stay in the shared pool queue for other workers to pick up:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uhttp-server>=2.5.2
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
uhttp-server
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|