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.
Files changed (25) hide show
  1. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/PKG-INFO +27 -2
  2. uhttp_workers-1.4.0/uhttp_workers.egg-info/PKG-INFO → uhttp_workers-1.5.0/README.md +25 -14
  3. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/pyproject.toml +1 -1
  4. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/test_dispatcher.py +43 -1
  5. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/test_worker.py +33 -1
  6. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/uhttp/workers.py +27 -0
  7. uhttp_workers-1.4.0/README.md → uhttp_workers-1.5.0/uhttp_workers.egg-info/PKG-INFO +39 -0
  8. uhttp_workers-1.5.0/uhttp_workers.egg-info/requires.txt +1 -0
  9. uhttp_workers-1.4.0/uhttp_workers.egg-info/requires.txt +0 -1
  10. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/.github/workflows/publish.yml +0 -0
  11. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/.github/workflows/tests.yml +0 -0
  12. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/.gitignore +0 -0
  13. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/examples/simple_workers.py +0 -0
  14. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/examples/sse_workers.py +0 -0
  15. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/examples/static/index.html +0 -0
  16. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/setup.cfg +0 -0
  17. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/__init__.py +0 -0
  18. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/test_api_handler.py +0 -0
  19. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/test_decorators.py +0 -0
  20. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/test_pattern_matching.py +0 -0
  21. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/test_request_response.py +0 -0
  22. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/tests/test_worker_pool.py +0 -0
  23. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/uhttp_workers.egg-info/SOURCES.txt +0 -0
  24. {uhttp_workers-1.4.0 → uhttp_workers-1.5.0}/uhttp_workers.egg-info/dependency_links.txt +0 -0
  25. {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.4.0
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:
@@ -13,7 +13,7 @@ authors = [
13
13
  license = "MIT"
14
14
  requires-python = ">=3.10"
15
15
  dependencies = [
16
- "uhttp-server",
16
+ "uhttp-server>=2.5.2",
17
17
  ]
18
18
  classifiers = [
19
19
  "Programming Language :: Python :: 3",
@@ -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