uhttp-workers 1.6.0__tar.gz → 1.7.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 (24) hide show
  1. {uhttp_workers-1.6.0/uhttp_workers.egg-info → uhttp_workers-1.7.0}/PKG-INFO +13 -10
  2. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/README.md +12 -9
  3. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/test_dispatcher.py +30 -9
  4. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/test_request_response.py +11 -0
  5. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/uhttp/workers.py +11 -3
  6. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0/uhttp_workers.egg-info}/PKG-INFO +13 -10
  7. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/.github/workflows/publish.yml +0 -0
  8. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/.github/workflows/tests.yml +0 -0
  9. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/.gitignore +0 -0
  10. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/examples/simple_workers.py +0 -0
  11. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/examples/sse_workers.py +0 -0
  12. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/examples/static/index.html +0 -0
  13. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/pyproject.toml +0 -0
  14. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/setup.cfg +0 -0
  15. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/__init__.py +0 -0
  16. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/test_api_handler.py +0 -0
  17. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/test_decorators.py +0 -0
  18. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/test_pattern_matching.py +0 -0
  19. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/test_worker.py +0 -0
  20. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/test_worker_pool.py +0 -0
  21. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/uhttp_workers.egg-info/SOURCES.txt +0 -0
  22. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/uhttp_workers.egg-info/dependency_links.txt +0 -0
  23. {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/uhttp_workers.egg-info/requires.txt +0 -0
  24. {uhttp_workers-1.6.0 → uhttp_workers-1.7.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.6.0
3
+ Version: 1.7.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
@@ -277,15 +277,18 @@ Workers send heartbeats automatically via the shared response queue. When a work
277
277
  ```python
278
278
  @_workers.api('/process/{id:int}', 'POST')
279
279
  def process(self, request):
280
- # request.request_id — internal ID for dispatcher pairing
281
- # request.method — 'POST'
282
- # request.path — '/process/42'
283
- # request.path_params — {'id': 42}
284
- # request.query — {'page': '1'} or None
285
- # request.data — dict (JSON), bytes (binary), or None
286
- # request.headers — dict
287
- # request.cookies — dict (lazy-parsed from Cookie header)
288
- # request.content_type — 'application/json'
280
+ # request.request_id — internal ID for dispatcher pairing
281
+ # request.method — 'POST'
282
+ # request.path — '/process/42'
283
+ # request.path_params — {'id': 42}
284
+ # request.query — {'page': '1'} or None
285
+ # request.data — dict (JSON), bytes (binary), or None
286
+ # request.headers — dict
287
+ # request.cookies — dict (lazy-parsed from Cookie header)
288
+ # request.content_type — 'application/json'
289
+ # request.remote_address — 'host:port' string (honors X-Forwarded-For
290
+ # from trusted proxies; configured on the
291
+ # HTTP server side)
289
292
 
290
293
  # return data (status 200)
291
294
  return {'result': 'ok'}
@@ -263,15 +263,18 @@ Workers send heartbeats automatically via the shared response queue. When a work
263
263
  ```python
264
264
  @_workers.api('/process/{id:int}', 'POST')
265
265
  def process(self, request):
266
- # request.request_id — internal ID for dispatcher pairing
267
- # request.method — 'POST'
268
- # request.path — '/process/42'
269
- # request.path_params — {'id': 42}
270
- # request.query — {'page': '1'} or None
271
- # request.data — dict (JSON), bytes (binary), or None
272
- # request.headers — dict
273
- # request.cookies — dict (lazy-parsed from Cookie header)
274
- # request.content_type — 'application/json'
266
+ # request.request_id — internal ID for dispatcher pairing
267
+ # request.method — 'POST'
268
+ # request.path — '/process/42'
269
+ # request.path_params — {'id': 42}
270
+ # request.query — {'page': '1'} or None
271
+ # request.data — dict (JSON), bytes (binary), or None
272
+ # request.headers — dict
273
+ # request.cookies — dict (lazy-parsed from Cookie header)
274
+ # request.content_type — 'application/json'
275
+ # request.remote_address — 'host:port' string (honors X-Forwarded-For
276
+ # from trusted proxies; configured on the
277
+ # HTTP server side)
275
278
 
276
279
  # return data (status 200)
277
280
  return {'result': 'ok'}
@@ -30,7 +30,7 @@ class MockClient:
30
30
 
31
31
  def __init__(self, method='GET', path='/', query=None, data=None,
32
32
  headers=None, content_type=None, body=None,
33
- address='127.0.0.1'):
33
+ remote_address='127.0.0.1:0'):
34
34
  self.method = method
35
35
  self.path = path
36
36
  self.query = query
@@ -38,7 +38,7 @@ class MockClient:
38
38
  self.headers = headers or {}
39
39
  self.content_type = content_type
40
40
  self.body = body
41
- self.address = address
41
+ self.remote_address = remote_address
42
42
  self.responded = False
43
43
  self.response_data = None
44
44
  self.response_status = None
@@ -364,6 +364,26 @@ class TestDispatcherPoolRouting(unittest.TestCase):
364
364
  self.assertEqual(
365
365
  client.response_data['error'], 'No workers available')
366
366
 
367
+ def test_dispatch_forwards_remote_address(self):
368
+ """Request enqueued to worker carries client.remote_address."""
369
+ # fake an alive worker so dispatch reaches enqueue
370
+ self.pool_default.workers = [
371
+ type('W', (), {'is_alive': lambda self: True})()]
372
+ d = Dispatcher.__new__(Dispatcher)
373
+ d._sync_routes = []
374
+ d._static_routes = {}
375
+ d._pools = [self.pool_default]
376
+ d._pending = {}
377
+ d._max_pending = 1000
378
+ d._next_request_id = 0
379
+
380
+ client = MockClient(
381
+ 'GET', '/test', remote_address='198.51.100.7:33421')
382
+ d._dispatch_to_pool(client)
383
+ # request was enqueued
384
+ req = self.pool_default.request_queue.get(timeout=1)
385
+ self.assertEqual(req.remote_address, '198.51.100.7:33421')
386
+
367
387
 
368
388
  class TestDispatcherProcessResponse(unittest.TestCase):
369
389
 
@@ -851,7 +871,8 @@ class TestDispatcherWorkerDied(unittest.TestCase):
851
871
 
852
872
  d, pool = self._make_dispatcher(RecordingDispatcher)
853
873
  client = MockClient(
854
- 'POST', '/api/scan', body=b'\x00\x01bad', address='10.0.0.7')
874
+ 'POST', '/api/scan', body=b'\x00\x01bad',
875
+ remote_address='10.0.0.7:42')
855
876
  pending = _PendingRequest(client, pool)
856
877
  pending.worker_id = 0
857
878
  d._pending[42] = pending
@@ -869,9 +890,9 @@ class TestDispatcherWorkerDied(unittest.TestCase):
869
890
 
870
891
  def test_multiple_victims_all_handled(self):
871
892
  d, pool = self._make_dispatcher()
872
- c1 = MockClient('GET', '/api/a', address='1.1.1.1')
873
- c2 = MockClient('GET', '/api/b', address='2.2.2.2')
874
- c3 = MockClient('GET', '/api/c', address='3.3.3.3')
893
+ c1 = MockClient('GET', '/api/a', remote_address='1.1.1.1:42')
894
+ c2 = MockClient('GET', '/api/b', remote_address='2.2.2.2:42')
895
+ c3 = MockClient('GET', '/api/c', remote_address='3.3.3.3:42')
875
896
  for rid, c in [(1, c1), (2, c2), (3, c3)]:
876
897
  p = _PendingRequest(c, pool)
877
898
  p.worker_id = 0
@@ -978,7 +999,7 @@ class TestDispatcherWorkerDied(unittest.TestCase):
978
999
  for rid, pending in victims:
979
1000
  captured.append({
980
1001
  'rid': rid,
981
- 'address': pending.client.address,
1002
+ 'remote_address': pending.client.remote_address,
982
1003
  'body': pending.client.body,
983
1004
  'reason': reason,
984
1005
  'exitcode': exitcode})
@@ -988,14 +1009,14 @@ class TestDispatcherWorkerDied(unittest.TestCase):
988
1009
  d, pool = self._make_dispatcher(ForensicDispatcher)
989
1010
  client = MockClient(
990
1011
  'POST', '/api/process',
991
- body=b'\xff\xfecorrupted', address='9.9.9.9')
1012
+ body=b'\xff\xfecorrupted', remote_address='9.9.9.9:42')
992
1013
  pending = _PendingRequest(client, pool)
993
1014
  pending.worker_id = 0
994
1015
  d._pending[7] = pending
995
1016
  pool._fake_restarted = [(0, 'died exit=-11', -11)]
996
1017
  d._check_all_workers()
997
1018
  self.assertEqual(len(captured), 1)
998
- self.assertEqual(captured[0]['address'], '9.9.9.9')
1019
+ self.assertEqual(captured[0]['remote_address'], '9.9.9.9:42')
999
1020
  self.assertEqual(captured[0]['body'], b'\xff\xfecorrupted')
1000
1021
  self.assertEqual(captured[0]['exitcode'], -11)
1001
1022
  # super() still ran
@@ -18,6 +18,17 @@ class TestRequest(unittest.TestCase):
18
18
  self.assertEqual(req.headers, {})
19
19
  self.assertIsNone(req.content_type)
20
20
  self.assertEqual(req.path_params, {})
21
+ self.assertIsNone(req.remote_address)
22
+
23
+ def test_remote_address(self):
24
+ req = Request(1, 'GET', '/', remote_address='10.0.0.1:54321')
25
+ self.assertEqual(req.remote_address, '10.0.0.1:54321')
26
+
27
+ def test_pickle_remote_address(self):
28
+ req = Request(
29
+ 1, 'GET', '/', remote_address='2001:db8::1:54321')
30
+ restored = pickle.loads(pickle.dumps(req))
31
+ self.assertEqual(restored.remote_address, '2001:db8::1:54321')
21
32
 
22
33
  def test_full_creation(self):
23
34
  req = Request(
@@ -215,16 +215,22 @@ class Request:
215
215
  headers: Request headers dict.
216
216
  content_type: Content-Type header value, or None.
217
217
  path_params: Path parameters filled by worker router.
218
+ remote_address: Client address as "host:port" string. Honors
219
+ X-Forwarded-For when the connection comes from a trusted
220
+ proxy (uhttp-server's trusted_proxies setting). None if the
221
+ dispatcher could not resolve the address (e.g., in tests).
218
222
  """
219
223
 
220
224
  __slots__ = (
221
225
  'request_id', 'method', 'path', 'query',
222
226
  'data', 'headers', 'content_type', 'path_params',
227
+ 'remote_address',
223
228
  '_cookies', '_response_queue')
224
229
 
225
230
  def __init__(
226
231
  self, request_id, method, path, query=None,
227
- data=None, headers=None, content_type=None):
232
+ data=None, headers=None, content_type=None,
233
+ remote_address=None):
228
234
  self.request_id = request_id
229
235
  self.method = method
230
236
  self.path = path
@@ -233,6 +239,7 @@ class Request:
233
239
  self.headers = headers or {}
234
240
  self.content_type = content_type
235
241
  self.path_params = {}
242
+ self.remote_address = remote_address
236
243
  self._cookies = None
237
244
  self._response_queue = None
238
245
 
@@ -1235,7 +1242,8 @@ class Dispatcher:
1235
1242
  query=client.query,
1236
1243
  data=client.data,
1237
1244
  headers=dict(client.headers),
1238
- content_type=client.content_type))
1245
+ content_type=client.content_type,
1246
+ remote_address=client.remote_address))
1239
1247
 
1240
1248
  def _http_request(self, client):
1241
1249
  """Process incoming HTTP request."""
@@ -1453,7 +1461,7 @@ class Dispatcher:
1453
1461
  body_len = len(c.body) if c.body is not None else 0
1454
1462
  self.on_log(
1455
1463
  pool.name, LOG_ERROR,
1456
- f" victim rid={request_id} from={c.address} "
1464
+ f" victim rid={request_id} from={c.remote_address} "
1457
1465
  f"{c.method} {c.path} body={body_len}B")
1458
1466
  del self._pending[request_id]
1459
1467
  if pending.streaming:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uhttp-workers
3
- Version: 1.6.0
3
+ Version: 1.7.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
@@ -277,15 +277,18 @@ Workers send heartbeats automatically via the shared response queue. When a work
277
277
  ```python
278
278
  @_workers.api('/process/{id:int}', 'POST')
279
279
  def process(self, request):
280
- # request.request_id — internal ID for dispatcher pairing
281
- # request.method — 'POST'
282
- # request.path — '/process/42'
283
- # request.path_params — {'id': 42}
284
- # request.query — {'page': '1'} or None
285
- # request.data — dict (JSON), bytes (binary), or None
286
- # request.headers — dict
287
- # request.cookies — dict (lazy-parsed from Cookie header)
288
- # request.content_type — 'application/json'
280
+ # request.request_id — internal ID for dispatcher pairing
281
+ # request.method — 'POST'
282
+ # request.path — '/process/42'
283
+ # request.path_params — {'id': 42}
284
+ # request.query — {'page': '1'} or None
285
+ # request.data — dict (JSON), bytes (binary), or None
286
+ # request.headers — dict
287
+ # request.cookies — dict (lazy-parsed from Cookie header)
288
+ # request.content_type — 'application/json'
289
+ # request.remote_address — 'host:port' string (honors X-Forwarded-For
290
+ # from trusted proxies; configured on the
291
+ # HTTP server side)
289
292
 
290
293
  # return data (status 200)
291
294
  return {'result': 'ok'}
File without changes
File without changes