python-engineio 4.8.2__tar.gz → 4.9.1__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 (75) hide show
  1. {python-engineio-4.8.2/src/python_engineio.egg-info → python_engineio-4.9.1}/PKG-INFO +1 -1
  2. {python-engineio-4.8.2 → python_engineio-4.9.1}/pyproject.toml +1 -1
  3. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/async_client.py +12 -8
  4. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/async_drivers/asgi.py +17 -10
  5. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/async_server.py +4 -3
  6. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/client.py +20 -10
  7. {python-engineio-4.8.2 → python_engineio-4.9.1/src/python_engineio.egg-info}/PKG-INFO +1 -1
  8. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/async/test_asgi.py +27 -0
  9. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/async/test_client.py +2 -1
  10. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/common/test_client.py +2 -1
  11. {python-engineio-4.8.2 → python_engineio-4.9.1}/LICENSE +0 -0
  12. {python-engineio-4.8.2 → python_engineio-4.9.1}/MANIFEST.in +0 -0
  13. {python-engineio-4.8.2 → python_engineio-4.9.1}/README.md +0 -0
  14. {python-engineio-4.8.2 → python_engineio-4.9.1}/docs/Makefile +0 -0
  15. {python-engineio-4.8.2 → python_engineio-4.9.1}/docs/_static/README.md +0 -0
  16. {python-engineio-4.8.2 → python_engineio-4.9.1}/docs/api.rst +0 -0
  17. {python-engineio-4.8.2 → python_engineio-4.9.1}/docs/client.rst +0 -0
  18. {python-engineio-4.8.2 → python_engineio-4.9.1}/docs/conf.py +0 -0
  19. {python-engineio-4.8.2 → python_engineio-4.9.1}/docs/index.rst +0 -0
  20. {python-engineio-4.8.2 → python_engineio-4.9.1}/docs/intro.rst +0 -0
  21. {python-engineio-4.8.2 → python_engineio-4.9.1}/docs/make.bat +0 -0
  22. {python-engineio-4.8.2 → python_engineio-4.9.1}/docs/server.rst +0 -0
  23. {python-engineio-4.8.2 → python_engineio-4.9.1}/setup.cfg +0 -0
  24. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/__init__.py +0 -0
  25. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/async_drivers/__init__.py +0 -0
  26. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/async_drivers/_websocket_wsgi.py +0 -0
  27. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/async_drivers/aiohttp.py +0 -0
  28. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/async_drivers/eventlet.py +0 -0
  29. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/async_drivers/gevent.py +0 -0
  30. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/async_drivers/gevent_uwsgi.py +0 -0
  31. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/async_drivers/sanic.py +0 -0
  32. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/async_drivers/threading.py +0 -0
  33. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/async_drivers/tornado.py +0 -0
  34. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/async_socket.py +0 -0
  35. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/base_client.py +0 -0
  36. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/base_server.py +0 -0
  37. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/base_socket.py +0 -0
  38. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/exceptions.py +0 -0
  39. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/json.py +0 -0
  40. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/middleware.py +0 -0
  41. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/packet.py +0 -0
  42. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/payload.py +0 -0
  43. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/server.py +0 -0
  44. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/socket.py +0 -0
  45. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/engineio/static_files.py +0 -0
  46. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/python_engineio.egg-info/SOURCES.txt +0 -0
  47. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/python_engineio.egg-info/dependency_links.txt +0 -0
  48. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/python_engineio.egg-info/not-zip-safe +0 -0
  49. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/python_engineio.egg-info/requires.txt +0 -0
  50. {python-engineio-4.8.2 → python_engineio-4.9.1}/src/python_engineio.egg-info/top_level.txt +0 -0
  51. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/__init__.py +0 -0
  52. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/async/__init__.py +0 -0
  53. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/async/index.html +0 -0
  54. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/async/test_aiohttp.py +0 -0
  55. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/async/test_sanic.py +0 -0
  56. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/async/test_server.py +0 -0
  57. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/async/test_socket.py +0 -0
  58. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/async/test_tornado.py +0 -0
  59. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/common/__init__.py +0 -0
  60. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/common/index.html +0 -0
  61. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/common/test_async_eventlet.py +0 -0
  62. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/common/test_middleware.py +0 -0
  63. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/common/test_packet.py +0 -0
  64. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/common/test_payload.py +0 -0
  65. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/common/test_server.py +0 -0
  66. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/common/test_socket.py +0 -0
  67. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/performance/README.md +0 -0
  68. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/performance/binary_b64_packet.py +0 -0
  69. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/performance/binary_packet.py +0 -0
  70. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/performance/json_packet.py +0 -0
  71. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/performance/payload.py +0 -0
  72. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/performance/run.sh +0 -0
  73. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/performance/server_receive.py +0 -0
  74. {python-engineio-4.8.2 → python_engineio-4.9.1}/tests/performance/text_packet.py +0 -0
  75. {python-engineio-4.8.2 → python_engineio-4.9.1}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-engineio
3
- Version: 4.8.2
3
+ Version: 4.9.1
4
4
  Summary: Engine.IO server and client for Python
5
5
  Author-email: Miguel Grinberg <miguel.grinberg@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/miguelgrinberg/python-engineio
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-engineio"
3
- version = "4.8.2"
3
+ version = "4.9.1"
4
4
  authors = [{name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com"}]
5
5
  description = "Engine.IO server and client for Python"
6
6
  classifiers = [
@@ -493,7 +493,7 @@ class AsyncClient(base_client.BaseClient):
493
493
 
494
494
  async def _read_loop_polling(self):
495
495
  """Read packets by polling the Engine.IO server."""
496
- while self.state == 'connected':
496
+ while self.state == 'connected' and self.write_loop_task:
497
497
  self.logger.info(
498
498
  'Sending polling GET request to ' + self.base_url)
499
499
  r = await self._send_request(
@@ -520,8 +520,9 @@ class AsyncClient(base_client.BaseClient):
520
520
  for pkt in p.packets:
521
521
  await self._receive_packet(pkt)
522
522
 
523
- self.logger.info('Waiting for write loop task to end')
524
- await self.write_loop_task
523
+ if self.write_loop_task: # pragma: no branch
524
+ self.logger.info('Waiting for write loop task to end')
525
+ await self.write_loop_task
525
526
  if self.state == 'connected':
526
527
  await self._trigger_event('disconnect', run_async=False)
527
528
  try:
@@ -541,8 +542,10 @@ class AsyncClient(base_client.BaseClient):
541
542
  timeout=self.ping_interval + self.ping_timeout)
542
543
  if not isinstance(p.data, (str, bytes)): # pragma: no cover
543
544
  self.logger.warning(
544
- 'Server sent unexpected packet %s data %s, aborting',
545
- str(p.type), str(p.data))
545
+ 'Server sent %s packet data %s, aborting',
546
+ 'close' if p.type in [aiohttp.WSMsgType.CLOSE,
547
+ aiohttp.WSMsgType.CLOSING]
548
+ else str(p.type), str(p.data))
546
549
  await self.queue.put(None)
547
550
  break # the connection is broken
548
551
  p = p.data
@@ -571,8 +574,9 @@ class AsyncClient(base_client.BaseClient):
571
574
  break
572
575
  await self._receive_packet(pkt)
573
576
 
574
- self.logger.info('Waiting for write loop task to end')
575
- await self.write_loop_task
577
+ if self.write_loop_task: # pragma: no branch
578
+ self.logger.info('Waiting for write loop task to end')
579
+ await self.write_loop_task
576
580
  if self.state == 'connected':
577
581
  await self._trigger_event('disconnect', run_async=False)
578
582
  try:
@@ -630,7 +634,7 @@ class AsyncClient(base_client.BaseClient):
630
634
  if r.status < 200 or r.status >= 300:
631
635
  self.logger.warning('Unexpected status code %s in server '
632
636
  'response, aborting', r.status)
633
- await self._reset()
637
+ self.write_loop_task = None
634
638
  break
635
639
  else:
636
640
  # websocket
@@ -19,7 +19,12 @@ class ASGIApp:
19
19
  :param other_asgi_app: A separate ASGI app that receives all other traffic.
20
20
  :param engineio_path: The endpoint where the Engine.IO application should
21
21
  be installed. The default value is appropriate for
22
- most cases.
22
+ most cases. With a value of ``None``, all incoming
23
+ traffic is directed to the Engine.IO server, with the
24
+ assumption that routing, if necessary, is handled by
25
+ a different layer. When this option is set to
26
+ ``None``, ``static_files`` and ``other_asgi_app`` are
27
+ ignored.
23
28
  :param on_startup: function to be called on application startup; can be
24
29
  coroutine
25
30
  :param on_shutdown: function to be called on application shutdown; can be
@@ -44,24 +49,26 @@ class ASGIApp:
44
49
  self.engineio_server = engineio_server
45
50
  self.other_asgi_app = other_asgi_app
46
51
  self.engineio_path = engineio_path
47
- if not self.engineio_path.startswith('/'):
48
- self.engineio_path = '/' + self.engineio_path
49
- if not self.engineio_path.endswith('/'):
50
- self.engineio_path += '/'
52
+ if self.engineio_path is not None:
53
+ if not self.engineio_path.startswith('/'):
54
+ self.engineio_path = '/' + self.engineio_path
55
+ if not self.engineio_path.endswith('/'):
56
+ self.engineio_path += '/'
51
57
  self.static_files = static_files or {}
52
58
  self.on_startup = on_startup
53
59
  self.on_shutdown = on_shutdown
54
60
 
55
61
  async def __call__(self, scope, receive, send):
56
- if scope['type'] in ['http', 'websocket'] and \
57
- scope['path'].startswith(self.engineio_path):
62
+ if scope['type'] == 'lifespan':
63
+ await self.lifespan(scope, receive, send)
64
+ elif scope['type'] in ['http', 'websocket'] and (
65
+ self.engineio_path is None
66
+ or scope['path'].startswith(self.engineio_path)):
58
67
  await self.engineio_server.handle_request(scope, receive, send)
59
68
  else:
60
69
  static_file = get_static_file(scope['path'], self.static_files) \
61
70
  if scope['type'] == 'http' and self.static_files else None
62
- if scope['type'] == 'lifespan':
63
- await self.lifespan(scope, receive, send)
64
- elif static_file and os.path.exists(static_file['filename']):
71
+ if static_file and os.path.exists(static_file['filename']):
65
72
  await self.serve_static_file(static_file, receive, send)
66
73
  elif self.other_asgi_app is not None:
67
74
  await self.other_asgi_app(scope, receive, send)
@@ -524,9 +524,9 @@ class AsyncServer(base_server.BaseServer):
524
524
  try:
525
525
  await asyncio.wait_for(self.service_task_event.wait(),
526
526
  timeout=self.ping_timeout)
527
- except asyncio.TimeoutError:
528
527
  break
529
- continue
528
+ except asyncio.TimeoutError:
529
+ continue
530
530
 
531
531
  # go through the entire client list in a ping interval cycle
532
532
  sleep_interval = self.ping_timeout / len(self.sockets)
@@ -546,8 +546,9 @@ class AsyncServer(base_server.BaseServer):
546
546
  try:
547
547
  await asyncio.wait_for(self.service_task_event.wait(),
548
548
  timeout=sleep_interval)
549
- except asyncio.TimeoutError:
550
549
  raise KeyboardInterrupt()
550
+ except asyncio.TimeoutError:
551
+ continue
551
552
  except (
552
553
  SystemExit,
553
554
  KeyboardInterrupt,
@@ -450,7 +450,7 @@ class Client(base_client.BaseClient):
450
450
 
451
451
  def _read_loop_polling(self):
452
452
  """Read packets by polling the Engine.IO server."""
453
- while self.state == 'connected':
453
+ while self.state == 'connected' and self.write_loop_task:
454
454
  self.logger.info(
455
455
  'Sending polling GET request to ' + self.base_url)
456
456
  r = self._send_request(
@@ -476,8 +476,9 @@ class Client(base_client.BaseClient):
476
476
  for pkt in p.packets:
477
477
  self._receive_packet(pkt)
478
478
 
479
- self.logger.info('Waiting for write loop task to end')
480
- self.write_loop_task.join()
479
+ if self.write_loop_task: # pragma: no branch
480
+ self.logger.info('Waiting for write loop task to end')
481
+ self.write_loop_task.join()
481
482
  if self.state == 'connected':
482
483
  self._trigger_event('disconnect', run_async=False)
483
484
  try:
@@ -493,6 +494,9 @@ class Client(base_client.BaseClient):
493
494
  p = None
494
495
  try:
495
496
  p = self.ws.recv()
497
+ if len(p) == 0 and not self.ws.connected: # pragma: no cover
498
+ # websocket client can return an empty string after close
499
+ raise websocket.WebSocketConnectionClosedException()
496
500
  except websocket.WebSocketTimeoutException:
497
501
  self.logger.warning(
498
502
  'Server has stopped communicating, aborting')
@@ -503,10 +507,15 @@ class Client(base_client.BaseClient):
503
507
  'WebSocket connection was closed, aborting')
504
508
  self.queue.put(None)
505
509
  break
506
- except Exception as e:
507
- self.logger.info(
508
- 'Unexpected error receiving packet: "%s", aborting',
509
- str(e))
510
+ except Exception as e: # pragma: no cover
511
+ if type(e) is OSError and e.errno == 9:
512
+ self.logger.info(
513
+ 'WebSocket connection is closing, aborting',
514
+ str(e))
515
+ else:
516
+ self.logger.info(
517
+ 'Unexpected error receiving packet: "%s", aborting',
518
+ str(e))
510
519
  self.queue.put(None)
511
520
  break
512
521
  try:
@@ -518,8 +527,9 @@ class Client(base_client.BaseClient):
518
527
  break
519
528
  self._receive_packet(pkt)
520
529
 
521
- self.logger.info('Waiting for write loop task to end')
522
- self.write_loop_task.join()
530
+ if self.write_loop_task: # pragma: no branch
531
+ self.logger.info('Waiting for write loop task to end')
532
+ self.write_loop_task.join()
523
533
  if self.state == 'connected':
524
534
  self._trigger_event('disconnect', run_async=False)
525
535
  try:
@@ -575,7 +585,7 @@ class Client(base_client.BaseClient):
575
585
  if r.status_code < 200 or r.status_code >= 300:
576
586
  self.logger.warning('Unexpected status code %s in server '
577
587
  'response, aborting', r.status_code)
578
- self._reset()
588
+ self.write_loop_task = None
579
589
  break
580
590
  else:
581
591
  # websocket
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-engineio
3
- Version: 4.8.2
3
+ Version: 4.9.1
4
4
  Summary: Engine.IO server and client for Python
5
5
  Author-email: Miguel Grinberg <miguel.grinberg@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/miguelgrinberg/python-engineio
@@ -514,3 +514,30 @@ class AsgiTests(unittest.TestCase):
514
514
  environ['asgi.send'].mock.assert_called_with(
515
515
  {'type': 'websocket.close'}
516
516
  )
517
+
518
+ def test_sub_app_routing(self):
519
+
520
+ class ASGIDispatcher:
521
+ def __init__(self, routes):
522
+ self.routes = routes
523
+
524
+ async def __call__(self, scope, receive, send):
525
+ path = scope['path']
526
+ for prefix, app in self.routes.items():
527
+ if path.startswith(prefix):
528
+ await app(scope, receive, send)
529
+ return
530
+ assert False, 'No route found'
531
+
532
+ other_app = AsyncMock()
533
+ mock_server = mock.MagicMock()
534
+ mock_server.handle_request = AsyncMock()
535
+ eio_app = async_asgi.ASGIApp(mock_server, engineio_path=None)
536
+ root_app = ASGIDispatcher({'/foo': other_app, '/eio': eio_app})
537
+ scope = {'type': 'http', 'path': '/foo/bar'}
538
+ _run(root_app(scope, 'receive', 'send'))
539
+ other_app.mock.assert_called_once_with(scope, 'receive', 'send')
540
+ scope = {'type': 'http', 'path': '/eio/'}
541
+ _run(root_app(scope, 'receive', 'send'))
542
+ eio_app.engineio_server.handle_request.mock.assert_called_once_with(
543
+ scope, 'receive', 'send')
@@ -1378,7 +1378,8 @@ class TestAsyncClient(unittest.TestCase):
1378
1378
  headers={'Content-Type': 'text/plain'},
1379
1379
  timeout=5,
1380
1380
  )
1381
- assert c.state == 'disconnected'
1381
+ assert c.state == 'connected'
1382
+ assert c.write_loop_task is None
1382
1383
 
1383
1384
  def test_write_loop_websocket_one_packet(self):
1384
1385
  c = async_client.AsyncClient()
@@ -1631,7 +1631,8 @@ class TestClient(unittest.TestCase):
1631
1631
  headers={'Content-Type': 'text/plain'},
1632
1632
  timeout=5,
1633
1633
  )
1634
- assert c.state == 'disconnected'
1634
+ assert c.state == 'connected'
1635
+ assert c.write_loop_task is None
1635
1636
 
1636
1637
  def test_write_loop_websocket_one_packet(self):
1637
1638
  c = client.Client()
File without changes
File without changes