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.
- {uhttp_workers-1.6.0/uhttp_workers.egg-info → uhttp_workers-1.7.0}/PKG-INFO +13 -10
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/README.md +12 -9
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/test_dispatcher.py +30 -9
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/test_request_response.py +11 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/uhttp/workers.py +11 -3
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0/uhttp_workers.egg-info}/PKG-INFO +13 -10
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/.github/workflows/publish.yml +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/.github/workflows/tests.yml +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/.gitignore +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/examples/simple_workers.py +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/examples/sse_workers.py +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/examples/static/index.html +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/pyproject.toml +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/setup.cfg +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/__init__.py +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/test_api_handler.py +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/test_decorators.py +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/test_pattern_matching.py +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/test_worker.py +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/tests/test_worker_pool.py +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/uhttp_workers.egg-info/SOURCES.txt +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/uhttp_workers.egg-info/dependency_links.txt +0 -0
- {uhttp_workers-1.6.0 → uhttp_workers-1.7.0}/uhttp_workers.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
281
|
-
# request.method
|
|
282
|
-
# request.path
|
|
283
|
-
# request.path_params
|
|
284
|
-
# request.query
|
|
285
|
-
# request.data
|
|
286
|
-
# request.headers
|
|
287
|
-
# request.cookies
|
|
288
|
-
# request.content_type
|
|
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
|
|
267
|
-
# request.method
|
|
268
|
-
# request.path
|
|
269
|
-
# request.path_params
|
|
270
|
-
# request.query
|
|
271
|
-
# request.data
|
|
272
|
-
# request.headers
|
|
273
|
-
# request.cookies
|
|
274
|
-
# request.content_type
|
|
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
|
-
|
|
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.
|
|
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',
|
|
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',
|
|
873
|
-
c2 = MockClient('GET', '/api/b',
|
|
874
|
-
c3 = MockClient('GET', '/api/c',
|
|
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
|
-
'
|
|
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',
|
|
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]['
|
|
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.
|
|
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.
|
|
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
|
|
281
|
-
# request.method
|
|
282
|
-
# request.path
|
|
283
|
-
# request.path_params
|
|
284
|
-
# request.query
|
|
285
|
-
# request.data
|
|
286
|
-
# request.headers
|
|
287
|
-
# request.cookies
|
|
288
|
-
# request.content_type
|
|
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
|
|
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
|