From 06e7cece74b01707247d28537537eafd73d96f02 Mon Sep 17 00:00:00 2001 From: Jacopo Farina Date: Wed, 6 Dec 2017 18:27:02 +0100 Subject: [PATCH 001/571] aiohttp instead of flask in example HTML (#91) * aiohttp instead of flask in example HTML * page title is now python-socketio --- examples/aiohttp/app.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/aiohttp/app.html b/examples/aiohttp/app.html index 1012beab..d71ebd29 100755 --- a/examples/aiohttp/app.html +++ b/examples/aiohttp/app.html @@ -1,7 +1,7 @@ - Flask-SocketIO Test + python-socketio test -

Flask-SocketIO Test

+

python-socketio test

Send:

From 8d7059a1a22e2d5e3092623251ab357046595a33 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Fri, 8 Dec 2017 09:07:58 -0800 Subject: [PATCH 002/571] Properly handle callbacks in multi-host configurations Fixes #150 --- socketio/asyncio_manager.py | 7 +++---- socketio/pubsub_manager.py | 6 ++++-- tests/test_pubsub_manager.py | 15 ++++++++------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/socketio/asyncio_manager.py b/socketio/asyncio_manager.py index 5322aa93..08b02d72 100644 --- a/socketio/asyncio_manager.py +++ b/socketio/asyncio_manager.py @@ -48,10 +48,9 @@ async def trigger_callback(self, sid, namespace, id, data): else: del self.callbacks[sid][namespace][id] if callback is not None: - if asyncio.iscoroutinefunction(callback) is True: + ret = callback(*data) + if asyncio.iscoroutine(ret): try: - await callback(*data) + await ret except asyncio.CancelledError: # pragma: no cover pass - else: - callback(*data) diff --git a/socketio/pubsub_manager.py b/socketio/pubsub_manager.py index 4810728b..afbe2766 100644 --- a/socketio/pubsub_manager.py +++ b/socketio/pubsub_manager.py @@ -63,7 +63,8 @@ def emit(self, event, data, namespace=None, room=None, skip_sid=None, callback = None self._publish({'method': 'emit', 'event': event, 'data': data, 'namespace': namespace, 'room': room, - 'skip_sid': skip_sid, 'callback': callback}) + 'skip_sid': skip_sid, 'callback': callback, + 'host_id': self.host_id}) def close_room(self, room, namespace=None): self._publish({'method': 'close_room', 'room': room, @@ -93,8 +94,9 @@ def _handle_emit(self, message): # Here in the receiving end we set up a local callback that preserves # the callback host and id from the sender remote_callback = message.get('callback') + remote_host_id = message.get('host_id') if remote_callback is not None and len(remote_callback) == 3: - callback = partial(self._return_callback, self.host_id, + callback = partial(self._return_callback, remote_host_id, *remote_callback) else: callback = None diff --git a/tests/test_pubsub_manager.py b/tests/test_pubsub_manager.py index 684dedb8..04304618 100644 --- a/tests/test_pubsub_manager.py +++ b/tests/test_pubsub_manager.py @@ -17,11 +17,11 @@ def setUp(self): self.pm = pubsub_manager.PubSubManager() self.pm._publish = mock.MagicMock() self.pm.set_server(mock_server) + self.pm.host_id = '123456' self.pm.initialize() def test_default_init(self): self.assertEqual(self.pm.channel, 'socketio') - self.assertEqual(len(self.pm.host_id), 32) self.pm.server.start_background_task.assert_called_once_with( self.pm._thread) @@ -44,28 +44,28 @@ def test_emit(self): self.pm._publish.assert_called_once_with( {'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': None, 'skip_sid': None, - 'callback': None}) + 'callback': None, 'host_id': '123456'}) def test_emit_with_namespace(self): self.pm.emit('foo', 'bar', namespace='/baz') self.pm._publish.assert_called_once_with( {'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/baz', 'room': None, 'skip_sid': None, - 'callback': None}) + 'callback': None, 'host_id': '123456'}) def test_emit_with_room(self): self.pm.emit('foo', 'bar', room='baz') self.pm._publish.assert_called_once_with( {'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': 'baz', 'skip_sid': None, - 'callback': None}) + 'callback': None, 'host_id': '123456'}) def test_emit_with_skip_sid(self): self.pm.emit('foo', 'bar', skip_sid='baz') self.pm._publish.assert_called_once_with( {'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': None, 'skip_sid': 'baz', - 'callback': None}) + 'callback': None, 'host_id': '123456'}) def test_emit_with_callback(self): with mock.patch.object(self.pm, '_generate_ack_id', @@ -74,7 +74,7 @@ def test_emit_with_callback(self): self.pm._publish.assert_called_once_with( {'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': 'baz', 'skip_sid': None, - 'callback': ('baz', '/', '123')}) + 'callback': ('baz', '/', '123'), 'host_id': '123456'}) def test_emit_with_callback_without_server(self): standalone_pm = pubsub_manager.PubSubManager() @@ -141,7 +141,8 @@ def test_handle_emit_with_callback(self): with mock.patch.object(base_manager.BaseManager, 'emit') as super_emit: self.pm._handle_emit({'event': 'foo', 'data': 'bar', 'namespace': '/baz', - 'callback': ('sid', '/baz', 123)}) + 'callback': ('sid', '/baz', 123), + 'host_id': host_id}) self.assertEqual(super_emit.call_count, 1) self.assertEqual(super_emit.call_args[0], ('foo', 'bar')) self.assertEqual(super_emit.call_args[1]['namespace'], '/baz') From b3fc842c76d53c2632192c5fda3db42a0bb764cd Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 9 Dec 2017 10:32:42 -0800 Subject: [PATCH 003/571] properly handle callbacks in multi-host configurations for asyncio --- socketio/asyncio_pubsub_manager.py | 6 ++++-- tests/test_asyncio_pubsub_manager.py | 15 ++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/socketio/asyncio_pubsub_manager.py b/socketio/asyncio_pubsub_manager.py index 8442cd17..578e734c 100644 --- a/socketio/asyncio_pubsub_manager.py +++ b/socketio/asyncio_pubsub_manager.py @@ -65,7 +65,8 @@ async def emit(self, event, data, namespace=None, room=None, skip_sid=None, callback = None await self._publish({'method': 'emit', 'event': event, 'data': data, 'namespace': namespace, 'room': room, - 'skip_sid': skip_sid, 'callback': callback}) + 'skip_sid': skip_sid, 'callback': callback, + 'host_id': self.host_id}) async def close_room(self, room, namespace=None): await self._publish({'method': 'close_room', 'room': room, @@ -95,8 +96,9 @@ async def _handle_emit(self, message): # Here in the receiving end we set up a local callback that preserves # the callback host and id from the sender remote_callback = message.get('callback') + remote_host_id = message.get('host_id') if remote_callback is not None and len(remote_callback) == 3: - callback = partial(self._return_callback, self.host_id, + callback = partial(self._return_callback, remote_host_id, *remote_callback) else: callback = None diff --git a/tests/test_asyncio_pubsub_manager.py b/tests/test_asyncio_pubsub_manager.py index 2f556e67..b18e51d6 100644 --- a/tests/test_asyncio_pubsub_manager.py +++ b/tests/test_asyncio_pubsub_manager.py @@ -44,11 +44,11 @@ def setUp(self): self.pm = asyncio_pubsub_manager.AsyncPubSubManager() self.pm._publish = AsyncMock() self.pm.set_server(mock_server) + self.pm.host_id = '123456' self.pm.initialize() def test_default_init(self): self.assertEqual(self.pm.channel, 'socketio') - self.assertEqual(len(self.pm.host_id), 32) self.pm.server.start_background_task.assert_called_once_with( self.pm._thread) @@ -71,28 +71,28 @@ def test_emit(self): self.pm._publish.mock.assert_called_once_with( {'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': None, 'skip_sid': None, - 'callback': None}) + 'callback': None, 'host_id': '123456'}) def test_emit_with_namespace(self): _run(self.pm.emit('foo', 'bar', namespace='/baz')) self.pm._publish.mock.assert_called_once_with( {'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/baz', 'room': None, 'skip_sid': None, - 'callback': None}) + 'callback': None, 'host_id': '123456'}) def test_emit_with_room(self): _run(self.pm.emit('foo', 'bar', room='baz')) self.pm._publish.mock.assert_called_once_with( {'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': 'baz', 'skip_sid': None, - 'callback': None}) + 'callback': None, 'host_id': '123456'}) def test_emit_with_skip_sid(self): _run(self.pm.emit('foo', 'bar', skip_sid='baz')) self.pm._publish.mock.assert_called_once_with( {'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': None, 'skip_sid': 'baz', - 'callback': None}) + 'callback': None, 'host_id': '123456'}) def test_emit_with_callback(self): with mock.patch.object(self.pm, '_generate_ack_id', @@ -101,7 +101,7 @@ def test_emit_with_callback(self): self.pm._publish.mock.assert_called_once_with( {'method': 'emit', 'event': 'foo', 'data': 'bar', 'namespace': '/', 'room': 'baz', 'skip_sid': None, - 'callback': ('baz', '/', '123')}) + 'callback': ('baz', '/', '123'), 'host_id': '123456'}) def test_emit_with_callback_without_server(self): standalone_pm = asyncio_pubsub_manager.AsyncPubSubManager() @@ -173,7 +173,8 @@ def test_handle_emit_with_callback(self): new=AsyncMock()) as super_emit: _run(self.pm._handle_emit({'event': 'foo', 'data': 'bar', 'namespace': '/baz', - 'callback': ('sid', '/baz', 123)})) + 'callback': ('sid', '/baz', 123), + 'host_id': '123456'})) self.assertEqual(super_emit.mock.call_count, 1) self.assertEqual(super_emit.mock.call_args[0], (self.pm, 'foo', 'bar')) From 96d4e5f257f0ec9bb68ce4b83f348e2899aa7f40 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Mon, 11 Dec 2017 13:30:35 -0800 Subject: [PATCH 004/571] Release 1.8.4 --- socketio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketio/__init__.py b/socketio/__init__.py index 472b902c..7ee5381a 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -19,7 +19,7 @@ AsyncNamespace = None AsyncRedisManager = None -__version__ = '1.8.3' +__version__ = '1.8.4' __all__ = ['__version__', 'Middleware', 'Server', 'BaseManager', 'PubSubManager', 'KombuManager', 'RedisManager', 'ZmqManager', From af13ef067c0f5b357e80d0e7f718bb725ed55fd6 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Mon, 15 Jan 2018 12:32:45 -0800 Subject: [PATCH 005/571] Recoonect to redis when connection is lost Fixes #143 --- socketio/asyncio_redis_manager.py | 48 ++++++++++++++++++++++++------- socketio/redis_manager.py | 47 +++++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 15 deletions(-) diff --git a/socketio/asyncio_redis_manager.py b/socketio/asyncio_redis_manager.py index 7ecc34c7..7cb2a53e 100644 --- a/socketio/asyncio_redis_manager.py +++ b/socketio/asyncio_redis_manager.py @@ -1,3 +1,5 @@ +import asyncio +import logging import pickle from urllib.parse import urlparse @@ -8,6 +10,8 @@ from .asyncio_pubsub_manager import AsyncPubSubManager +logger = logging.getLogger('socketio') + def _parse_redis_url(url): p = urlparse(url) @@ -59,17 +63,39 @@ def __init__(self, url='redis://localhost:6379/0', channel='socketio', super().__init__(channel=channel, write_only=write_only) async def _publish(self, data): - if self.pub is None: - self.pub = await aioredis.create_redis((self.host, self.port), - db=self.db, - password=self.password) - return await self.pub.publish(self.channel, pickle.dumps(data)) + retry = True + while True: + try: + if self.pub is None: + self.pub = await aioredis.create_redis( + (self.host, self.port), db=self.db, + password=self.password) + return await self.pub.publish(self.channel, + pickle.dumps(data)) + except (aioredis.RedisError, OSError): + if retry: + logger.error('Cannot publish to redis... retrying') + self.pub = None + retry = False + else: + logger.error('Cannot publish to redis... giving up') + break async def _listen(self): - if self.sub is None: - self.sub = await aioredis.create_redis((self.host, self.port), - db=self.db, - password=self.password) - self.ch = (await self.sub.subscribe(self.channel))[0] + retry_sleep = 1 while True: - return await self.ch.get() + try: + if self.sub is None: + self.sub = await aioredis.create_redis( + (self.host, self.port), db=self.db, + password=self.password) + self.ch = (await self.sub.subscribe(self.channel))[0] + return await self.ch.get() + except (aioredis.RedisError, OSError): + logger.error('Cannot receive from redis... ' + 'retrying in {} secs'.format(retry_sleep)) + self.sub = None + await asyncio.sleep(retry_sleep) + retry_sleep *= 2 + if retry_sleep > 60: + retry_sleep = 60 diff --git a/socketio/redis_manager.py b/socketio/redis_manager.py index 30f7978c..9a6f499f 100644 --- a/socketio/redis_manager.py +++ b/socketio/redis_manager.py @@ -1,4 +1,6 @@ +import logging import pickle +import time try: import redis @@ -7,6 +9,8 @@ from .pubsub_manager import PubSubManager +logger = logging.getLogger('socketio') + class RedisManager(PubSubManager): # pragma: no cover """Redis based client manager. @@ -38,8 +42,8 @@ def __init__(self, url='redis://localhost:6379/0', channel='socketio', raise RuntimeError('Redis package is not installed ' '(Run "pip install redis" in your ' 'virtualenv).') - self.redis = redis.Redis.from_url(url) - self.pubsub = self.redis.pubsub() + self.redis_url = url + self._redis_connect() super(RedisManager, self).__init__(channel=channel, write_only=write_only) @@ -58,13 +62,48 @@ def initialize(self): 'Redis requires a monkey patched socket library to work ' 'with ' + self.server.async_mode) + def _redis_connect(self): + self.redis = redis.Redis.from_url(self.redis_url) + self.pubsub = self.redis.pubsub() + def _publish(self, data): - return self.redis.publish(self.channel, pickle.dumps(data)) + retry = True + while True: + try: + if not retry: + self._redis_connect() + return self.redis.publish(self.channel, pickle.dumps(data)) + except redis.exceptions.ConnectionError: + if retry: + logger.error('Cannot publish to redis... retrying') + retry = False + else: + logger.error('Cannot publish to redis... giving up') + break + + def _redis_listen_with_retries(self): + retry_sleep = 1 + connect = False + while True: + try: + if connect: + self._redis_connect() + self.pubsub.subscribe(self.channel) + for message in self.pubsub.listen(): + yield message + except redis.exceptions.ConnectionError: + logger.error('Cannot receive from redis... ' + 'retrying in {} secs'.format(retry_sleep)) + connect = True + time.sleep(retry_sleep) + retry_sleep *= 2 + if retry_sleep > 60: + retry_sleep = 60 def _listen(self): channel = self.channel.encode('utf-8') self.pubsub.subscribe(self.channel) - for message in self.pubsub.listen(): + for message in self._redis_listen_with_retries(): if message['channel'] == channel and \ message['type'] == 'message' and 'data' in message: yield message['data'] From 598568c7152dc970dc740de040697e97cf374ec1 Mon Sep 17 00:00:00 2001 From: rettier Date: Mon, 29 Jan 2018 18:43:47 +0100 Subject: [PATCH 006/571] assigning local ret variable to none if the asyncio task is canceled (#164) --- socketio/asyncio_namespace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketio/asyncio_namespace.py b/socketio/asyncio_namespace.py index ba22e4e5..893c5e01 100644 --- a/socketio/asyncio_namespace.py +++ b/socketio/asyncio_namespace.py @@ -36,7 +36,7 @@ async def trigger_event(self, event, *args): try: ret = await handler(*args) except asyncio.CancelledError: # pragma: no cover - pass + ret = None else: ret = handler(*args) return ret From 1ec2e10efcefce0d23040b7ee36e3167f758054b Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 7 Mar 2018 14:59:48 -0800 Subject: [PATCH 007/571] Release 1.9.0 --- socketio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketio/__init__.py b/socketio/__init__.py index 7ee5381a..9f7f7114 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -19,7 +19,7 @@ AsyncNamespace = None AsyncRedisManager = None -__version__ = '1.8.4' +__version__ = '1.9.0' __all__ = ['__version__', 'Middleware', 'Server', 'BaseManager', 'PubSubManager', 'KombuManager', 'RedisManager', 'ZmqManager', From ad37d0db930edf773221cfad40582618c821cb48 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 22 Apr 2018 19:56:38 -0700 Subject: [PATCH 008/571] add pypy3 target to travis builds --- .travis.yml | 2 ++ tox.ini | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cd3a253b..8ac5681b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,8 @@ matrix: env: TOXENV=py36 - python: pypy env: TOXENV=pypy + - python: pypy3 + env: TOXENV=pypy3 - python: 3.6 env: TOXENV=docs install: diff --git a/tox.ini b/tox.ini index c326cd0f..84924a79 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=flake8,py27,py34,py35,py36,pypy,docs,coverage +envlist=flake8,py27,py34,py35,py36,pypy,pypy3,docs,coverage skip_missing_interpreters=True [testenv] @@ -16,6 +16,7 @@ basepython = py35: python3.5 py36: python3.6 pypy: pypy + pypy3: pypy3 [testenv:flake8] basepython=python3.6 From c24c91a72521eeb19eb5ee60e06006da5e8c7578 Mon Sep 17 00:00:00 2001 From: Toon Knapen Date: Mon, 23 Apr 2018 18:54:58 +0200 Subject: [PATCH 009/571] typo fix (#180) --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 4e4d6a7f..ee560b30 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -166,7 +166,7 @@ coroutine friendly server:: # attach server to application sio.attach(app) -Event handlers for servers are register using the :func:`socketio.Server.on` +Event handlers for servers are registered using the :func:`socketio.Server.on` method:: @sio.on('my custom event') From 725afcbc14cd854974db50b57023d94e2c7c6dfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20M=C3=A9zino?= Date: Sat, 28 Apr 2018 16:47:30 +0200 Subject: [PATCH 010/571] Add a copy of redis manager and name it kafka manager --- socketio/kafka_manager.py | 110 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 socketio/kafka_manager.py diff --git a/socketio/kafka_manager.py b/socketio/kafka_manager.py new file mode 100644 index 00000000..c47c23bd --- /dev/null +++ b/socketio/kafka_manager.py @@ -0,0 +1,110 @@ +import logging +import pickle +import time + +try: + import redis +except ImportError: + redis = None + +from .pubsub_manager import PubSubManager + +logger = logging.getLogger('socketio') + + +class KafkaManager(PubSubManager): # pragma: no cover + """Redis based client manager. + + This class implements a Redis backend for event sharing across multiple + processes. Only kept here as one more example of how to build a custom + backend, since the kombu backend is perfectly adequate to support a Redis + message queue. + + To use a Redis backend, initialize the :class:`Server` instance as + follows:: + + url = 'redis://hostname:port/0' + server = socketio.Server(client_manager=socketio.RedisManager(url)) + + :param url: The connection URL for the Redis server. For a default Redis + store running on the same host, use ``redis://``. + :param channel: The channel name on which the server sends and receives + notifications. Must be the same in all the servers. + :param write_only: If set ot ``True``, only initialize to emit events. The + default of ``False`` initializes the class for emitting + and receiving. + """ + name = 'redis' + + def __init__(self, url='redis://localhost:6379/0', channel='socketio', + write_only=False): + if redis is None: + raise RuntimeError('Redis package is not installed ' + '(Run "pip install redis" in your ' + 'virtualenv).') + self.redis_url = url + self._redis_connect() + super(RedisManager, self).__init__(channel=channel, + write_only=write_only) + + def initialize(self): + super(RedisManager, self).initialize() + + monkey_patched = True + if self.server.async_mode == 'eventlet': + from eventlet.patcher import is_monkey_patched + monkey_patched = is_monkey_patched('socket') + elif 'gevent' in self.server.async_mode: + from gevent.monkey import is_module_patched + monkey_patched = is_module_patched('socket') + if not monkey_patched: + raise RuntimeError( + 'Redis requires a monkey patched socket library to work ' + 'with ' + self.server.async_mode) + + def _redis_connect(self): + self.redis = redis.Redis.from_url(self.redis_url) + self.pubsub = self.redis.pubsub() + + def _publish(self, data): + retry = True + while True: + try: + if not retry: + self._redis_connect() + return self.redis.publish(self.channel, pickle.dumps(data)) + except redis.exceptions.ConnectionError: + if retry: + logger.error('Cannot publish to redis... retrying') + retry = False + else: + logger.error('Cannot publish to redis... giving up') + break + + def _redis_listen_with_retries(self): + retry_sleep = 1 + connect = False + while True: + try: + if connect: + self._redis_connect() + self.pubsub.subscribe(self.channel) + for message in self.pubsub.listen(): + yield message + except redis.exceptions.ConnectionError: + logger.error('Cannot receive from redis... ' + 'retrying in {} secs'.format(retry_sleep)) + connect = True + time.sleep(retry_sleep) + retry_sleep *= 2 + if retry_sleep > 60: + retry_sleep = 60 + + def _listen(self): + channel = self.channel.encode('utf-8') + self.pubsub.subscribe(self.channel) + for message in self._redis_listen_with_retries(): + if message['channel'] == channel and \ + message['type'] == 'message' and 'data' in message: + yield message['data'] + self.pubsub.unsubscribe(self.channel) From 2568c80a5afefdbf55a1a04fa6fccd9124d09596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20M=C3=A9zino?= Date: Sat, 28 Apr 2018 17:10:31 +0200 Subject: [PATCH 011/571] Add KafkaManager to __init__.py --- socketio/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/socketio/__init__.py b/socketio/__init__.py index 9f7f7114..859b53b1 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -5,6 +5,7 @@ from .pubsub_manager import PubSubManager from .kombu_manager import KombuManager from .redis_manager import RedisManager +from .kafka_manager import KafkaManager from .zmq_manager import ZmqManager from .server import Server from .namespace import Namespace @@ -22,8 +23,8 @@ __version__ = '1.9.0' __all__ = ['__version__', 'Middleware', 'Server', 'BaseManager', - 'PubSubManager', 'KombuManager', 'RedisManager', 'ZmqManager', - 'Namespace'] + 'PubSubManager', 'KombuManager', 'RedisManager', 'KafkaManager', + 'ZmqManager', 'Namespace'] if AsyncServer is not None: # pragma: no cover __all__ += ['AsyncServer', 'AsyncNamespace', 'AsyncManager', 'AsyncRedisManager'] From 610c229fd93eca7c8f3433ea8bdc705bd62a84cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20M=C3=A9zino?= Date: Sun, 29 Apr 2018 00:33:29 +0200 Subject: [PATCH 012/571] Write KafkaManager --- socketio/kafka_manager.py | 101 +++++++++++++------------------------- 1 file changed, 35 insertions(+), 66 deletions(-) diff --git a/socketio/kafka_manager.py b/socketio/kafka_manager.py index c47c23bd..1ec896ee 100644 --- a/socketio/kafka_manager.py +++ b/socketio/kafka_manager.py @@ -1,11 +1,12 @@ import logging import pickle import time +import json try: - import redis + import kafka except ImportError: - redis = None + kafka = None from .pubsub_manager import PubSubManager @@ -13,43 +14,41 @@ class KafkaManager(PubSubManager): # pragma: no cover - """Redis based client manager. + """Kafka based client manager. - This class implements a Redis backend for event sharing across multiple - processes. Only kept here as one more example of how to build a custom - backend, since the kombu backend is perfectly adequate to support a Redis - message queue. + This class implements a Kafka backend for event sharing across multiple + processes. - To use a Redis backend, initialize the :class:`Server` instance as + To use a Kafka backend, initialize the :class:`Server` instance as follows:: - url = 'redis://hostname:port/0' - server = socketio.Server(client_manager=socketio.RedisManager(url)) + url = 'kafka://hostname:port/0' + server = socketio.Server(client_manager=socketio.KafkaManager(url)) - :param url: The connection URL for the Redis server. For a default Redis - store running on the same host, use ``redis://``. - :param channel: The channel name on which the server sends and receives + :param url: The connection URL for the Kafka server. For a default Kafka + store running on the same host, use ``kafka://``. + :param channel: The channel name (topic) on which the server sends and receives notifications. Must be the same in all the servers. :param write_only: If set ot ``True``, only initialize to emit events. The default of ``False`` initializes the class for emitting and receiving. """ - name = 'redis' + name = 'kafka' - def __init__(self, url='redis://localhost:6379/0', channel='socketio', + def __init__(self, url='kafka://localhost:9092/0', channel='socketio', write_only=False): - if redis is None: - raise RuntimeError('Redis package is not installed ' - '(Run "pip install redis" in your ' + if kafka is None: + raise RuntimeError('kafka-python package is not installed ' + '(Run "pip install kafka-python" in your ' 'virtualenv).') - self.redis_url = url - self._redis_connect() - super(RedisManager, self).__init__(channel=channel, + self.kafka_url = url + self.producer = kafka.KafkaProducer(bootstrap_servers='localhost:9092') + + super(KafkaManager, self).__init__(channel=channel, write_only=write_only) def initialize(self): - super(RedisManager, self).initialize() - + super(KafkaManager, self).initialize() monkey_patched = True if self.server.async_mode == 'eventlet': from eventlet.patcher import is_monkey_patched @@ -59,52 +58,22 @@ def initialize(self): monkey_patched = is_module_patched('socket') if not monkey_patched: raise RuntimeError( - 'Redis requires a monkey patched socket library to work ' + 'Kafka requires a monkey patched socket library to work ' 'with ' + self.server.async_mode) - def _redis_connect(self): - self.redis = redis.Redis.from_url(self.redis_url) - self.pubsub = self.redis.pubsub() def _publish(self, data): - retry = True - while True: - try: - if not retry: - self._redis_connect() - return self.redis.publish(self.channel, pickle.dumps(data)) - except redis.exceptions.ConnectionError: - if retry: - logger.error('Cannot publish to redis... retrying') - retry = False - else: - logger.error('Cannot publish to redis... giving up') - break - - def _redis_listen_with_retries(self): - retry_sleep = 1 - connect = False - while True: - try: - if connect: - self._redis_connect() - self.pubsub.subscribe(self.channel) - for message in self.pubsub.listen(): - yield message - except redis.exceptions.ConnectionError: - logger.error('Cannot receive from redis... ' - 'retrying in {} secs'.format(retry_sleep)) - connect = True - time.sleep(retry_sleep) - retry_sleep *= 2 - if retry_sleep > 60: - retry_sleep = 60 + self.producer.send(self.channel, value=pickle.dumps(data)) + self.producer.flush() + + + def _kafka_listen(self): + self.consumer = kafka.KafkaConsumer(self.channel, bootstrap_servers='localhost:9092') + for message in self.consumer: + yield message + def _listen(self): - channel = self.channel.encode('utf-8') - self.pubsub.subscribe(self.channel) - for message in self._redis_listen_with_retries(): - if message['channel'] == channel and \ - message['type'] == 'message' and 'data' in message: - yield message['data'] - self.pubsub.unsubscribe(self.channel) + for message in self._kafka_listen(): + if message.topic == self.channel: + yield pickle.loads(message.value) From 4f1d8d932907cdd95a402aa3024d52cd5b873c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20M=C3=A9zino?= Date: Sun, 29 Apr 2018 16:13:27 +0200 Subject: [PATCH 013/571] Use the provided url in KafkaManager and remove the method 'initialize' --- socketio/kafka_manager.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/socketio/kafka_manager.py b/socketio/kafka_manager.py index 1ec896ee..7bb1bcbc 100644 --- a/socketio/kafka_manager.py +++ b/socketio/kafka_manager.py @@ -22,7 +22,7 @@ class KafkaManager(PubSubManager): # pragma: no cover To use a Kafka backend, initialize the :class:`Server` instance as follows:: - url = 'kafka://hostname:port/0' + url = 'kafka://hostname:port' server = socketio.Server(client_manager=socketio.KafkaManager(url)) :param url: The connection URL for the Kafka server. For a default Kafka @@ -35,31 +35,20 @@ class KafkaManager(PubSubManager): # pragma: no cover """ name = 'kafka' - def __init__(self, url='kafka://localhost:9092/0', channel='socketio', + def __init__(self, url='kafka://localhost:9092', channel='socketio', write_only=False): if kafka is None: raise RuntimeError('kafka-python package is not installed ' '(Run "pip install kafka-python" in your ' 'virtualenv).') - self.kafka_url = url - self.producer = kafka.KafkaProducer(bootstrap_servers='localhost:9092') super(KafkaManager, self).__init__(channel=channel, write_only=write_only) - def initialize(self): - super(KafkaManager, self).initialize() - monkey_patched = True - if self.server.async_mode == 'eventlet': - from eventlet.patcher import is_monkey_patched - monkey_patched = is_monkey_patched('socket') - elif 'gevent' in self.server.async_mode: - from gevent.monkey import is_module_patched - monkey_patched = is_module_patched('socket') - if not monkey_patched: - raise RuntimeError( - 'Kafka requires a monkey patched socket library to work ' - 'with ' + self.server.async_mode) + self.kafka_url = url[8:] if url != 'kafka://' else 'localhost:9092' + self.producer = kafka.KafkaProducer(bootstrap_servers=self.kafka_url) + self.consumer = kafka.KafkaConsumer(self.channel, + bootstrap_servers=self.kafka_url) def _publish(self, data): @@ -68,7 +57,6 @@ def _publish(self, data): def _kafka_listen(self): - self.consumer = kafka.KafkaConsumer(self.channel, bootstrap_servers='localhost:9092') for message in self.consumer: yield message From 29e9c659d40dd8c405da38ca9295a9ac2f3ead46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20M=C3=A9zino?= Date: Mon, 30 Apr 2018 02:27:20 +0200 Subject: [PATCH 014/571] Remove unused imports --- socketio/kafka_manager.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/socketio/kafka_manager.py b/socketio/kafka_manager.py index 7bb1bcbc..d32cbdaa 100644 --- a/socketio/kafka_manager.py +++ b/socketio/kafka_manager.py @@ -1,7 +1,5 @@ import logging import pickle -import time -import json try: import kafka From 839dba341180b3b728b40a562e3c1b0f3d3cf550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20M=C3=A9zino?= Date: Mon, 30 Apr 2018 02:31:14 +0200 Subject: [PATCH 015/571] Fix flake8 --- socketio/kafka_manager.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/socketio/kafka_manager.py b/socketio/kafka_manager.py index d32cbdaa..00a2e7f0 100644 --- a/socketio/kafka_manager.py +++ b/socketio/kafka_manager.py @@ -25,8 +25,9 @@ class KafkaManager(PubSubManager): # pragma: no cover :param url: The connection URL for the Kafka server. For a default Kafka store running on the same host, use ``kafka://``. - :param channel: The channel name (topic) on which the server sends and receives - notifications. Must be the same in all the servers. + :param channel: The channel name (topic) on which the server sends and + receives notifications. Must be the same in all the + servers. :param write_only: If set ot ``True``, only initialize to emit events. The default of ``False`` initializes the class for emitting and receiving. @@ -48,17 +49,14 @@ def __init__(self, url='kafka://localhost:9092', channel='socketio', self.consumer = kafka.KafkaConsumer(self.channel, bootstrap_servers=self.kafka_url) - def _publish(self, data): self.producer.send(self.channel, value=pickle.dumps(data)) self.producer.flush() - def _kafka_listen(self): for message in self.consumer: yield message - def _listen(self): for message in self._kafka_listen(): if message.topic == self.channel: From dc1e416dabf978343113a919647999bc5cb98928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20M=C3=A9zino?= Date: Mon, 30 Apr 2018 02:33:46 +0200 Subject: [PATCH 016/571] Remove trailing whitespaces --- socketio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketio/__init__.py b/socketio/__init__.py index 859b53b1..32cd971b 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -23,7 +23,7 @@ __version__ = '1.9.0' __all__ = ['__version__', 'Middleware', 'Server', 'BaseManager', - 'PubSubManager', 'KombuManager', 'RedisManager', 'KafkaManager', + 'PubSubManager', 'KombuManager', 'RedisManager', 'KafkaManager', 'ZmqManager', 'Namespace'] if AsyncServer is not None: # pragma: no cover __all__ += ['AsyncServer', 'AsyncNamespace', 'AsyncManager', From 6dc74df2fbb779a832429345a7e54b1af7473425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20M=C3=A9zino?= Date: Mon, 30 Apr 2018 02:47:54 +0200 Subject: [PATCH 017/571] Group __all__ element by type in __init__.py --- socketio/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/socketio/__init__.py b/socketio/__init__.py index 32cd971b..11feaa3b 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -22,9 +22,10 @@ __version__ = '1.9.0' -__all__ = ['__version__', 'Middleware', 'Server', 'BaseManager', - 'PubSubManager', 'KombuManager', 'RedisManager', 'KafkaManager', - 'ZmqManager', 'Namespace'] +__all__ = ['__version__', 'Middleware', 'Server', + 'BaseManager', 'PubSubManager', + 'KombuManager', 'RedisManager', 'KafkaManager', 'ZmqManager', + 'Namespace'] if AsyncServer is not None: # pragma: no cover __all__ += ['AsyncServer', 'AsyncNamespace', 'AsyncManager', 'AsyncRedisManager'] From a84338fd42c218fe55c547281a5cb24deac549f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20M=C3=A9zino?= Date: Mon, 30 Apr 2018 02:49:46 +0200 Subject: [PATCH 018/571] Remove trailing whitespaces... --- socketio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketio/__init__.py b/socketio/__init__.py index 11feaa3b..e4377548 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -24,7 +24,7 @@ __all__ = ['__version__', 'Middleware', 'Server', 'BaseManager', 'PubSubManager', - 'KombuManager', 'RedisManager', 'KafkaManager', 'ZmqManager', + 'KombuManager', 'RedisManager', 'KafkaManager', 'ZmqManager', 'Namespace'] if AsyncServer is not None: # pragma: no cover __all__ += ['AsyncServer', 'AsyncNamespace', 'AsyncManager', From 4131e539f8b480ecd11f0c03239a5359a3012e34 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Sat, 26 May 2018 00:11:40 +0800 Subject: [PATCH 019/571] Update documentation link (#185) Pythonhosted.org was deprecated. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ce09f4b9..40d8cc45 100644 --- a/README.rst +++ b/README.rst @@ -123,5 +123,5 @@ Resources - `Documentation`_ - `PyPI`_ -.. _Documentation: http://pythonhosted.org/python-socketio +.. _Documentation: http://python-socketio.readthedocs.io/en/latest/ .. _PyPI: https://pypi.python.org/pypi/python-socketio From 58c8a6f4301758ff27fb95d28b939bf953a67119 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Sat, 9 Jun 2018 09:25:35 +0800 Subject: [PATCH 020/571] Fix typo in docs (#187) --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index ee560b30..df453122 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -610,7 +610,7 @@ can take advantage of uWSGI's native WebSocket support. Instances of class ``socketio.Server`` will automatically use this option for asynchronous operations if both gevent and uWSGI are installed and eventlet is -not installed. To request this asynchoronous mode explicitly, the +not installed. To request this asynchronous mode explicitly, the ``async_mode`` option can be given in the constructor:: # gevent with uWSGI From 555b69e80754ceb90d7c7aab1825cc37724b2e90 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 27 Jun 2018 17:05:45 -0700 Subject: [PATCH 021/571] Tornado 5 support --- README.rst | 3 +- examples/README.rst | 10 +++ examples/tornado/app.py | 101 ++++++++++++++++++++++++ examples/tornado/latency.py | 41 ++++++++++ examples/tornado/requirements.txt | 4 + examples/tornado/static/style.css | 4 + examples/tornado/templates/app.html | 91 +++++++++++++++++++++ examples/tornado/templates/latency.html | 64 +++++++++++++++ setup.py | 2 +- socketio/__init__.py | 3 +- socketio/tornado.py | 8 ++ 11 files changed, 328 insertions(+), 3 deletions(-) create mode 100755 examples/tornado/app.py create mode 100755 examples/tornado/latency.py create mode 100644 examples/tornado/requirements.txt create mode 100644 examples/tornado/static/style.css create mode 100755 examples/tornado/templates/app.html create mode 100755 examples/tornado/templates/latency.html create mode 100644 socketio/tornado.py diff --git a/README.rst b/README.rst index 40d8cc45..cf3c0c4e 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,8 @@ Features - Compatible with Python 2.7 and Python 3.3+. - Supports large number of clients even on modest hardware when used with an asynchronous server based on `asyncio `_ - (`sanic `_ and `aiohttp `_), + (`sanic `_, `aiohttp `_ + or `tornado `_), `eventlet `_ or `gevent `_. For development and testing, any WSGI compliant multi-threaded server can also be used. diff --git a/examples/README.rst b/examples/README.rst index a0ae1f65..19b6b53d 100644 --- a/examples/README.rst +++ b/examples/README.rst @@ -13,3 +13,13 @@ aiohttp ------- Examples that are compatible with the aiohttp framework for asyncio. + +sanic +----- + +Examples that are compatible with the sanic framework for asyncio. + +tornado +------- + +Examples that are compatible with the tornado framework. diff --git a/examples/tornado/app.py b/examples/tornado/app.py new file mode 100755 index 00000000..71f30457 --- /dev/null +++ b/examples/tornado/app.py @@ -0,0 +1,101 @@ +import os + +import tornado.ioloop +from tornado.options import define, options, parse_command_line +import tornado.web + +import socketio + +define("port", default=8888, help="run on the given port", type=int) +define("debug", default=False, help="run in debug mode") + +sio = socketio.AsyncServer(async_mode='tornado') + + +async def background_task(): + """Example of how to send server generated events to clients.""" + count = 0 + while True: + await sio.sleep(10) + count += 1 + await sio.emit('my response', {'data': 'Server generated event'}, + namespace='/test') + + +class MainHandler(tornado.web.RequestHandler): + def get(self): + self.render("app.html") + + +@sio.on('my event', namespace='/test') +async def test_message(sid, message): + await sio.emit('my response', {'data': message['data']}, room=sid, + namespace='/test') + + +@sio.on('my broadcast event', namespace='/test') +async def test_broadcast_message(sid, message): + await sio.emit('my response', {'data': message['data']}, namespace='/test') + + +@sio.on('join', namespace='/test') +async def join(sid, message): + sio.enter_room(sid, message['room'], namespace='/test') + await sio.emit('my response', {'data': 'Entered room: ' + message['room']}, + room=sid, namespace='/test') + + +@sio.on('leave', namespace='/test') +async def leave(sid, message): + sio.leave_room(sid, message['room'], namespace='/test') + await sio.emit('my response', {'data': 'Left room: ' + message['room']}, + room=sid, namespace='/test') + + +@sio.on('close room', namespace='/test') +async def close(sid, message): + await sio.emit('my response', + {'data': 'Room ' + message['room'] + ' is closing.'}, + room=message['room'], namespace='/test') + await sio.close_room(message['room'], namespace='/test') + + +@sio.on('my room event', namespace='/test') +async def send_room_message(sid, message): + await sio.emit('my response', {'data': message['data']}, + room=message['room'], namespace='/test') + + +@sio.on('disconnect request', namespace='/test') +async def disconnect_request(sid): + await sio.disconnect(sid, namespace='/test') + + +@sio.on('connect', namespace='/test') +async def test_connect(sid, environ): + await sio.emit('my response', {'data': 'Connected', 'count': 0}, room=sid, + namespace='/test') + + +@sio.on('disconnect', namespace='/test') +def test_disconnect(sid): + print('Client disconnected') + + +def main(): + parse_command_line() + app = tornado.web.Application( + [ + (r"/", MainHandler), + (r"/socket.io/", socketio.get_tornado_handler(sio)), + ], + template_path=os.path.join(os.path.dirname(__file__), "templates"), + static_path=os.path.join(os.path.dirname(__file__), "static"), + debug=options.debug, + ) + app.listen(options.port) + tornado.ioloop.IOLoop.current().start() + + +if __name__ == "__main__": + main() diff --git a/examples/tornado/latency.py b/examples/tornado/latency.py new file mode 100755 index 00000000..883da64c --- /dev/null +++ b/examples/tornado/latency.py @@ -0,0 +1,41 @@ +import os + +import tornado.ioloop +from tornado.options import define, options, parse_command_line +import tornado.web + +import socketio + +define("port", default=8888, help="run on the given port", type=int) +define("debug", default=False, help="run in debug mode") + +sio = socketio.AsyncServer(async_mode='tornado') + + +class MainHandler(tornado.web.RequestHandler): + def get(self): + self.render("latency.html") + + +@sio.on('ping_from_client') +async def ping(sid): + await sio.emit('pong_from_server', room=sid) + + +def main(): + parse_command_line() + app = tornado.web.Application( + [ + (r"/", MainHandler), + (r"/socket.io/", socketio.get_tornado_handler(sio)), + ], + template_path=os.path.join(os.path.dirname(__file__), "templates"), + static_path=os.path.join(os.path.dirname(__file__), "static"), + debug=options.debug, + ) + app.listen(options.port) + tornado.ioloop.IOLoop.current().start() + + +if __name__ == "__main__": + main() diff --git a/examples/tornado/requirements.txt b/examples/tornado/requirements.txt new file mode 100644 index 00000000..354f4a7a --- /dev/null +++ b/examples/tornado/requirements.txt @@ -0,0 +1,4 @@ +tornado==5.0.2 +python-engineio +python_socketio +six==1.10.0 diff --git a/examples/tornado/static/style.css b/examples/tornado/static/style.css new file mode 100644 index 00000000..d20bcad9 --- /dev/null +++ b/examples/tornado/static/style.css @@ -0,0 +1,4 @@ +body { margin: 0; padding: 0; font-family: Helvetica Neue; } +h1 { margin: 100px 100px 10px; } +h2 { color: #999; margin: 0 100px 30px; font-weight: normal; } +#latency { color: red; } diff --git a/examples/tornado/templates/app.html b/examples/tornado/templates/app.html new file mode 100755 index 00000000..d71ebd29 --- /dev/null +++ b/examples/tornado/templates/app.html @@ -0,0 +1,91 @@ + + + + python-socketio test + + + + + +

python-socketio test

+

Send:

+ + + + +
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+ +
+

Receive:

+

+ + diff --git a/examples/tornado/templates/latency.html b/examples/tornado/templates/latency.html new file mode 100755 index 00000000..b238cd1c --- /dev/null +++ b/examples/tornado/templates/latency.html @@ -0,0 +1,64 @@ + + + + Socket.IO Latency + + + +

Socket.IO Latency

+

(connecting)

+ + + + + + + + diff --git a/setup.py b/setup.py index c2998ed4..d16ecd84 100755 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ platforms='any', install_requires=[ 'six>=1.9.0', - 'python-engineio>=1.2.1' + 'python-engineio>=2.2.0' ], tests_require=[ 'mock', diff --git a/socketio/__init__.py b/socketio/__init__.py index 9f7f7114..2c798bcc 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -8,6 +8,7 @@ from .zmq_manager import ZmqManager from .server import Server from .namespace import Namespace +from .tornado import get_tornado_handler if sys.version_info >= (3, 5): # pragma: no cover from .asyncio_server import AsyncServer from .asyncio_manager import AsyncManager @@ -26,4 +27,4 @@ 'Namespace'] if AsyncServer is not None: # pragma: no cover __all__ += ['AsyncServer', 'AsyncNamespace', 'AsyncManager', - 'AsyncRedisManager'] + 'AsyncRedisManager', 'get_tornado_handler'] diff --git a/socketio/tornado.py b/socketio/tornado.py new file mode 100644 index 00000000..007c4924 --- /dev/null +++ b/socketio/tornado.py @@ -0,0 +1,8 @@ +import sys +if sys.version_info >= (3, 5): + from engineio.async_tornado import get_tornado_handler as \ + get_engineio_handler + + +def get_tornado_handler(socketio_server): + return get_engineio_handler(socketio_server.eio) From 836f4a400d232ebc26b3f068af1fc58ef79d8131 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 27 Jun 2018 18:49:06 -0700 Subject: [PATCH 022/571] Release 2.0.0 --- socketio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketio/__init__.py b/socketio/__init__.py index 2c798bcc..a8e10079 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -20,7 +20,7 @@ AsyncNamespace = None AsyncRedisManager = None -__version__ = '1.9.0' +__version__ = '2.0.0' __all__ = ['__version__', 'Middleware', 'Server', 'BaseManager', 'PubSubManager', 'KombuManager', 'RedisManager', 'ZmqManager', From 415af129b756d5e955180af314589bdc0c5930ff Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Thu, 28 Jun 2018 11:16:56 -0700 Subject: [PATCH 023/571] Tornado docs --- docs/index.rst | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index df453122..1a26d7a7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,7 +20,8 @@ features: - Compatible with Python 2.7 and Python 3.3+. - Supports large number of clients even on modest hardware when used with an asynchronous server based on `asyncio `_ - (`sanic `_ and `aiohttp `_), + (`sanic `_, `aiohttp `_ or + `tornado `_), `eventlet `_ or `gevent `_. For development and testing, any WSGI compliant multi-threaded server can also be used. @@ -512,6 +513,39 @@ The aiohttp application is then executed in the usual manner:: if __name__ == '__main__': web.run_app(app) +Tornado +~~~~~~~ + +`Tornado `_ is a web framework with support +for HTTP and WebSocket. Support for this framework requires Python 3.5 and +newer. Only Tornado version 5 and newer are supported, thanks to its tight +integration with asyncio. + +Instances of class ``socketio.AsyncServer`` will automatically use tornado +for asynchronous operations if the library is installed. To request its use +explicitly, the ``async_mode`` option can be given in the constructor:: + + sio = socketio.AsyncServer(async_mode='tornado') + +A server configured for tornado must include a request handler for +Engine.IO:: + + app = tornado.web.Application( + [ + (r"/socketio.io/", socketio.get_tornado_handler(sio)), + ], + # ... other application options + ) + +The tornado application can define other routes that will coexist with the +Socket.IO server. A typical pattern is to add routes that serve a client +application and any associated static files. + +The tornado application is then executed in the usual manner:: + + app.listen(port) + tornado.ioloop.IOLoop.current().start() + Eventlet ~~~~~~~~ From 2a8befdbc9e4fa47a355bf2919319df47b30c973 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Thu, 28 Jun 2018 11:50:12 -0700 Subject: [PATCH 024/571] Tornado examples readme --- examples/tornado/README.rst | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 examples/tornado/README.rst diff --git a/examples/tornado/README.rst b/examples/tornado/README.rst new file mode 100644 index 00000000..33f94321 --- /dev/null +++ b/examples/tornado/README.rst @@ -0,0 +1,39 @@ +Socket.IO Tornado Examples +========================== + +This directory contains example Socket.IO applications that are compatible +with the Tornado framework. These applications require Tornado 5 and Python +3.5 or later. + +app.py +------ + +A basic "kitchen sink" type application that allows the user to experiment +with most of the available features of the Socket.IO server. + +latency.py +---------- + +A port of the latency application included in the official Engine.IO +Javascript server. In this application the client sends *ping* messages to +the server, which are responded by the server with a *pong*. The client +measures the time it takes for each of these exchanges and plots these in real +time to the page. + +This is an ideal application to measure the performance of the different +asynchronous modes supported by the Socket.IO server. + +Running the Examples +-------------------- + +To run these examples, create a virtual environment, install the requirements +and then run:: + + $ python app.py + +or:: + + $ python latency.py + +You can then access the application from your web browser at +``http://localhost:8888``. From 1777c527781f788571e4f8a1f8e1f0d813ea9b8b Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 25 Aug 2018 12:23:32 +0100 Subject: [PATCH 025/571] add python 3.7 to tox --- tox.ini | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 84924a79..63841e65 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=flake8,py27,py34,py35,py36,pypy,pypy3,docs,coverage +envlist=flake8,py27,py34,py35,py36,py37,pypy,pypy3,docs,coverage skip_missing_interpreters=True [testenv] @@ -11,22 +11,24 @@ deps= coverage mock basepython = + flake8: python3.6 py27: python2.7 py34: python3.4 py35: python3.5 py36: python3.6 + py37: python3.7 pypy: pypy pypy3: pypy3 + docs: python3.6 + coverage: python3.6 [testenv:flake8] -basepython=python3.6 deps= flake8 commands= flake8 --exclude=".*" --ignore=E402,E722 socketio tests [testenv:docs] -basepython=python2.7 changedir=docs deps= sphinx @@ -36,7 +38,6 @@ commands= make html [testenv:coverage] -basepython=python3.6 commands= coverage run --branch --source=socketio setup.py test coverage html From f2d28ad62caa4ea6374bf9330b4ad7c79eaf2e05 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 25 Aug 2018 12:23:57 +0100 Subject: [PATCH 026/571] better handling of packets with no data --- socketio/packet.py | 6 +++--- tests/test_packet.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/socketio/packet.py b/socketio/packet.py index 570708b8..e1e080b1 100644 --- a/socketio/packet.py +++ b/socketio/packet.py @@ -88,9 +88,9 @@ def decode(self, encoded_packet): self.namespace = None self.data = None ep = ep[1:] - dash = (ep + '-').find('-') + dash = ep.find('-') attachment_count = 0 - if ep[0:dash].isdigit(): + if dash > 0 and ep[0:dash].isdigit(): attachment_count = int(ep[0:dash]) ep = ep[dash + 1:] if ep and ep[0:1] == '/': @@ -106,7 +106,7 @@ def decode(self, encoded_packet): self.namespace = self.namespace[0:q] if ep and ep[0].isdigit(): self.id = 0 - while ep[0].isdigit(): + while ep and ep[0].isdigit(): self.id = self.id * 10 + int(ep[0]) ep = ep[1:] if ep: diff --git a/tests/test_packet.py b/tests/test_packet.py index 96027899..90044bb1 100644 --- a/tests/test_packet.py +++ b/tests/test_packet.py @@ -146,6 +146,18 @@ def test_decode_id(self): self.assertEqual(pkt.id, 123) self.assertEqual(pkt.encode(), '2123["foo"]') + def test_encode_id_no_data(self): + pkt = packet.Packet(packet_type=packet.EVENT, id=123) + self.assertEqual(pkt.id, 123) + self.assertIsNone(pkt.data) + self.assertEqual(pkt.encode(), '2123') + + def test_decode_id_no_data(self): + pkt = packet.Packet(encoded_packet='2123') + self.assertEqual(pkt.id, 123) + self.assertIsNone(pkt.data) + self.assertEqual(pkt.encode(), '2123') + def test_encode_namespace_and_id(self): pkt = packet.Packet(packet_type=packet.EVENT, data=[six.text_type('foo')], namespace='/bar', From da7cb863301524aead8c4c422491e84c5a1c10bc Mon Sep 17 00:00:00 2001 From: Kelly Truesdale Date: Tue, 9 Oct 2018 18:46:33 -0400 Subject: [PATCH 027/571] Logging improvements for write-only connections (#197) * Make logger a property of BaseManager, configurable separately from server for write-only situations. * Shorten line lengths for recent changes. --- socketio/asyncio_manager.py | 2 +- socketio/asyncio_pubsub_manager.py | 5 +++-- socketio/asyncio_redis_manager.py | 18 +++++++++--------- socketio/base_manager.py | 19 ++++++++++++++++++- socketio/kombu_manager.py | 10 ++++++---- socketio/pubsub_manager.py | 5 +++-- socketio/redis_manager.py | 5 +++-- socketio/zmq_manager.py | 6 ++++-- tests/test_pubsub_manager.py | 17 +++++++++++++++++ 9 files changed, 64 insertions(+), 23 deletions(-) diff --git a/socketio/asyncio_manager.py b/socketio/asyncio_manager.py index 08b02d72..01bda69b 100644 --- a/socketio/asyncio_manager.py +++ b/socketio/asyncio_manager.py @@ -44,7 +44,7 @@ async def trigger_callback(self, sid, namespace, id, data): callback = self.callbacks[sid][namespace][id] except KeyError: # if we get an unknown callback we just ignore it - self.server.logger.warning('Unknown callback received, ignoring.') + self._get_logger().warning('Unknown callback received, ignoring.') else: del self.callbacks[sid][namespace][id] if callback is not None: diff --git a/socketio/asyncio_pubsub_manager.py b/socketio/asyncio_pubsub_manager.py index 578e734c..6fdba6d0 100644 --- a/socketio/asyncio_pubsub_manager.py +++ b/socketio/asyncio_pubsub_manager.py @@ -24,17 +24,18 @@ class AsyncPubSubManager(AsyncManager): """ name = 'asyncpubsub' - def __init__(self, channel='socketio', write_only=False): + def __init__(self, channel='socketio', write_only=False, logger=None): super().__init__() self.channel = channel self.write_only = write_only self.host_id = uuid.uuid4().hex + self.logger = logger def initialize(self): super().initialize() if not self.write_only: self.thread = self.server.start_background_task(self._thread) - self.server.logger.info(self.name + ' backend initialized.') + self._get_logger().info(self.name + ' backend initialized.') async def emit(self, event, data, namespace=None, room=None, skip_sid=None, callback=None, **kwargs): diff --git a/socketio/asyncio_redis_manager.py b/socketio/asyncio_redis_manager.py index 7cb2a53e..2265937a 100644 --- a/socketio/asyncio_redis_manager.py +++ b/socketio/asyncio_redis_manager.py @@ -1,5 +1,4 @@ import asyncio -import logging import pickle from urllib.parse import urlparse @@ -10,8 +9,6 @@ from .asyncio_pubsub_manager import AsyncPubSubManager -logger = logging.getLogger('socketio') - def _parse_redis_url(url): p = urlparse(url) @@ -52,7 +49,7 @@ class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover name = 'aioredis' def __init__(self, url='redis://localhost:6379/0', channel='socketio', - write_only=False): + write_only=False, logger=None): if aioredis is None: raise RuntimeError('Redis package is not installed ' '(Run "pip install aioredis" in your ' @@ -60,7 +57,7 @@ def __init__(self, url='redis://localhost:6379/0', channel='socketio', self.host, self.port, self.password, self.db = _parse_redis_url(url) self.pub = None self.sub = None - super().__init__(channel=channel, write_only=write_only) + super().__init__(channel=channel, write_only=write_only, logger=logger) async def _publish(self, data): retry = True @@ -74,11 +71,13 @@ async def _publish(self, data): pickle.dumps(data)) except (aioredis.RedisError, OSError): if retry: - logger.error('Cannot publish to redis... retrying') + self._get_logger().error('Cannot publish to redis... ' + 'retrying') self.pub = None retry = False else: - logger.error('Cannot publish to redis... giving up') + self._get_logger().error('Cannot publish to redis... ' + 'giving up') break async def _listen(self): @@ -92,8 +91,9 @@ async def _listen(self): self.ch = (await self.sub.subscribe(self.channel))[0] return await self.ch.get() except (aioredis.RedisError, OSError): - logger.error('Cannot receive from redis... ' - 'retrying in {} secs'.format(retry_sleep)) + self._get_logger().error('Cannot receive from redis... ' + 'retrying in ' + '{} secs'.format(retry_sleep)) self.sub = None await asyncio.sleep(retry_sleep) retry_sleep *= 2 diff --git a/socketio/base_manager.py b/socketio/base_manager.py index 09bef7c5..b4bcf5f2 100644 --- a/socketio/base_manager.py +++ b/socketio/base_manager.py @@ -1,7 +1,10 @@ import itertools +import logging import six +default_logger = logging.getLogger('socketio') + class BaseManager(object): """Manage client connections. @@ -13,6 +16,7 @@ class BaseManager(object): subclasses. """ def __init__(self): + self.logger = None self.server = None self.rooms = {} self.callbacks = {} @@ -141,7 +145,7 @@ def trigger_callback(self, sid, namespace, id, data): callback = self.callbacks[sid][namespace][id] except KeyError: # if we get an unknown callback we just ignore it - self.server.logger.warning('Unknown callback received, ignoring.') + self._get_logger().warning('Unknown callback received, ignoring.') else: del self.callbacks[sid][namespace][id] if callback is not None: @@ -157,3 +161,16 @@ def _generate_ack_id(self, sid, namespace, callback): id = six.next(self.callbacks[sid][namespace][0]) self.callbacks[sid][namespace][id] = callback return id + + def _get_logger(self): + """Get the appropriate logger + + Prevents uninitialized servers in write-only mode from failing. + """ + + if self.logger: + return self.logger + elif self.server: + return self.server.logger + else: + return default_logger diff --git a/socketio/kombu_manager.py b/socketio/kombu_manager.py index 9906673e..4394a6d3 100644 --- a/socketio/kombu_manager.py +++ b/socketio/kombu_manager.py @@ -38,12 +38,14 @@ class KombuManager(PubSubManager): # pragma: no cover name = 'kombu' def __init__(self, url='amqp://guest:guest@localhost:5672//', - channel='socketio', write_only=False): + channel='socketio', write_only=False, logger=None): if kombu is None: raise RuntimeError('Kombu package is not installed ' '(Run "pip install kombu" in your ' 'virtualenv).') - super(KombuManager, self).__init__(channel=channel) + super(KombuManager, self).__init__(channel=channel, + write_only=write_only, + logger=logger) self.url = url self.producer = self._producer() @@ -78,7 +80,7 @@ def _producer(self): return self._connection().Producer(exchange=self._exchange()) def __error_callback(self, exception, interval): - self.server.logger.exception('Sleeping {}s'.format(interval)) + self._get_logger().exception('Sleeping {}s'.format(interval)) def _publish(self, data): connection = self._connection() @@ -99,5 +101,5 @@ def _listen(self): message.ack() yield message.payload except connection.connection_errors: - self.server.logger.exception("Connection error " + self._get_logger().exception("Connection error " "while reading from queue") diff --git a/socketio/pubsub_manager.py b/socketio/pubsub_manager.py index afbe2766..2905b2c3 100644 --- a/socketio/pubsub_manager.py +++ b/socketio/pubsub_manager.py @@ -24,17 +24,18 @@ class PubSubManager(BaseManager): """ name = 'pubsub' - def __init__(self, channel='socketio', write_only=False): + def __init__(self, channel='socketio', write_only=False, logger=None): super(PubSubManager, self).__init__() self.channel = channel self.write_only = write_only self.host_id = uuid.uuid4().hex + self.logger = logger def initialize(self): super(PubSubManager, self).initialize() if not self.write_only: self.thread = self.server.start_background_task(self._thread) - self.server.logger.info(self.name + ' backend initialized.') + self._get_logger().info(self.name + ' backend initialized.') def emit(self, event, data, namespace=None, room=None, skip_sid=None, callback=None, **kwargs): diff --git a/socketio/redis_manager.py b/socketio/redis_manager.py index 9a6f499f..69be586f 100644 --- a/socketio/redis_manager.py +++ b/socketio/redis_manager.py @@ -37,7 +37,7 @@ class RedisManager(PubSubManager): # pragma: no cover name = 'redis' def __init__(self, url='redis://localhost:6379/0', channel='socketio', - write_only=False): + write_only=False, logger=None): if redis is None: raise RuntimeError('Redis package is not installed ' '(Run "pip install redis" in your ' @@ -45,7 +45,8 @@ def __init__(self, url='redis://localhost:6379/0', channel='socketio', self.redis_url = url self._redis_connect() super(RedisManager, self).__init__(channel=channel, - write_only=write_only) + write_only=write_only, + logger=logger) def initialize(self): super(RedisManager, self).initialize() diff --git a/socketio/zmq_manager.py b/socketio/zmq_manager.py index d8995a04..468830bb 100644 --- a/socketio/zmq_manager.py +++ b/socketio/zmq_manager.py @@ -50,7 +50,8 @@ class ZmqManager(PubSubManager): # pragma: no cover def __init__(self, url='zmq+tcp://localhost:5555+5556', channel='socketio', - write_only=False): + write_only=False, + logger=None): if zmq is None: raise RuntimeError('zmq package is not installed ' '(Run "pip install pyzmq" in your ' @@ -76,7 +77,8 @@ def __init__(self, url='zmq+tcp://localhost:5555+5556', self.sub = sub self.channel = channel super(ZmqManager, self).__init__(channel=channel, - write_only=write_only) + write_only=write_only, + logger=logger) def _publish(self, data): pickled_data = pickle.dumps( diff --git a/tests/test_pubsub_manager.py b/tests/test_pubsub_manager.py index 04304618..295151ab 100644 --- a/tests/test_pubsub_manager.py +++ b/tests/test_pubsub_manager.py @@ -1,5 +1,6 @@ import functools import unittest +import logging import six if six.PY3: @@ -39,6 +40,22 @@ def test_write_only_init(self): self.assertEqual(len(pm.host_id), 32) self.assertEqual(pm.server.start_background_task.call_count, 0) + def test_write_only_default_logger(self): + pm = pubsub_manager.PubSubManager(write_only=True) + pm.initialize() + self.assertEqual(pm.channel, 'socketio') + self.assertEqual(len(pm.host_id), 32) + self.assertEqual(pm._get_logger(), logging.getLogger('socketio')) + + def test_write_only_with_provided_logger(self): + test_logger = logging.getLogger('new_logger') + pm = pubsub_manager.PubSubManager(write_only=True, + logger=test_logger) + pm.initialize() + self.assertEqual(pm.channel, 'socketio') + self.assertEqual(len(pm.host_id), 32) + self.assertEqual(pm._get_logger(), test_logger) + def test_emit(self): self.pm.emit('foo', 'bar') self.pm._publish.assert_called_once_with( From 4a030d2f9c812b2dd0237883b072189feccac9b7 Mon Sep 17 00:00:00 2001 From: Anthony Zhang Date: Fri, 9 Nov 2018 11:19:38 -0500 Subject: [PATCH 028/571] Fix synchronization issue in SocketIO (#213) --- socketio/base_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketio/base_manager.py b/socketio/base_manager.py index b4bcf5f2..f8aa9982 100644 --- a/socketio/base_manager.py +++ b/socketio/base_manager.py @@ -71,7 +71,7 @@ def disconnect(self, sid, namespace): if namespace not in self.rooms: return rooms = [] - for room_name, room in six.iteritems(self.rooms[namespace]): + for room_name, room in six.iteritems(self.rooms[namespace].copy()): if sid in room: rooms.append(room_name) for room in rooms: From 015ea2037f8fb75c35f8478d14628bf91c54e603 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Fri, 9 Nov 2018 17:40:49 +0000 Subject: [PATCH 029/571] updated dependencies and fixed linter error --- examples/wsgi/django_example/requirements.txt | 4 ++-- examples/wsgi/requirements.txt | 14 +++++++------- socketio/zmq_manager.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/wsgi/django_example/requirements.txt b/examples/wsgi/django_example/requirements.txt index 0ef528de..42f0edd7 100644 --- a/examples/wsgi/django_example/requirements.txt +++ b/examples/wsgi/django_example/requirements.txt @@ -1,8 +1,8 @@ -Django==1.11.1 +django==2.1.3 enum-compat==0.0.2 eventlet==0.21.0 greenlet==0.4.12 python-engineio python-socketio -pytz==2017.2 +pytz==2018.7 six==1.10.0 diff --git a/examples/wsgi/requirements.txt b/examples/wsgi/requirements.txt index b704e5b0..240fa2e4 100644 --- a/examples/wsgi/requirements.txt +++ b/examples/wsgi/requirements.txt @@ -1,15 +1,15 @@ -click==6.7 +Click-7.0 enum-compat==0.0.2 enum34==1.1.6 eventlet==0.20.1 -Flask==0.12 +Flask==1.0.2 greenlet==0.4.12 -itsdangerous==0.24 -Jinja2==2.9.5 -MarkupSafe==0.23 +itsdangerous==1.1.0 +Jinja2==2.10 +MarkupSafe==1.1.0 packaging==16.8 pyparsing==2.1.10 python-engineio python-socketio -six==1.10.0 -Werkzeug==0.11.15 +six==1.11.0 +Werkzeug==0.14.1 diff --git a/socketio/zmq_manager.py b/socketio/zmq_manager.py index 468830bb..f2a2ae5d 100644 --- a/socketio/zmq_manager.py +++ b/socketio/zmq_manager.py @@ -57,7 +57,7 @@ def __init__(self, url='zmq+tcp://localhost:5555+5556', '(Run "pip install pyzmq" in your ' 'virtualenv).') - r = re.compile(':\d+\+\d+$') + r = re.compile(r':\d+\+\d+$') if not (url.startswith('zmq+tcp://') and r.search(url)): raise RuntimeError('unexpected connection string: ' + url) From b214380d056dbbfb08273ac482633254176cb847 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 25 Nov 2018 11:24:51 +0000 Subject: [PATCH 030/571] ASGI support --- examples/asgi/README.rst | 39 ++++++++ examples/asgi/app.html | 91 +++++++++++++++++++ examples/asgi/app.py | 82 +++++++++++++++++ examples/asgi/latency.html | 64 +++++++++++++ examples/asgi/latency.py | 19 ++++ examples/asgi/requirements.txt | 8 ++ examples/asgi/static/style.css | 4 + examples/wsgi/app.py | 2 +- .../django_example/django_example/wsgi.py | 4 +- examples/wsgi/latency.py | 2 +- socketio/__init__.py | 11 ++- socketio/asgi.py | 38 ++++++++ socketio/middleware.py | 28 ++++-- 13 files changed, 376 insertions(+), 16 deletions(-) create mode 100644 examples/asgi/README.rst create mode 100755 examples/asgi/app.html create mode 100755 examples/asgi/app.py create mode 100755 examples/asgi/latency.html create mode 100755 examples/asgi/latency.py create mode 100644 examples/asgi/requirements.txt create mode 100644 examples/asgi/static/style.css create mode 100644 socketio/asgi.py diff --git a/examples/asgi/README.rst b/examples/asgi/README.rst new file mode 100644 index 00000000..f1ce6aaf --- /dev/null +++ b/examples/asgi/README.rst @@ -0,0 +1,39 @@ +Socket.IO aiohttp Examples +========================== + +This directory contains example Socket.IO applications that are compatible with +asyncio and the aiohttp framework. These applications require Python 3.5 or +later. + +app.py +------ + +A basic "kitchen sink" type application that allows the user to experiment +with most of the available features of the Socket.IO server. + +latency.py +---------- + +A port of the latency application included in the official Engine.IO +Javascript server. In this application the client sends *ping* messages to +the server, which are responded by the server with a *pong*. The client +measures the time it takes for each of these exchanges and plots these in real +time to the page. + +This is an ideal application to measure the performance of the different +asynchronous modes supported by the Socket.IO server. + +Running the Examples +-------------------- + +To run these examples, create a virtual environment, install the requirements +and then run:: + + $ python app.py + +or:: + + $ python latency.py + +You can then access the application from your web browser at +``http://localhost:8080``. diff --git a/examples/asgi/app.html b/examples/asgi/app.html new file mode 100755 index 00000000..d71ebd29 --- /dev/null +++ b/examples/asgi/app.html @@ -0,0 +1,91 @@ + + + + python-socketio test + + + + + +

python-socketio test

+

Send:

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+ +
+

Receive:

+

+ + diff --git a/examples/asgi/app.py b/examples/asgi/app.py new file mode 100755 index 00000000..0771d476 --- /dev/null +++ b/examples/asgi/app.py @@ -0,0 +1,82 @@ +import asyncio + +import uvicorn +from uvicorn.loops.auto import auto_loop_setup + +import socketio + +sio = socketio.AsyncServer(async_mode='asgi') +app = socketio.ASGIApp(sio, static_files={ + '/': {'content_type': 'text/html', 'filename': 'app.html'}, +}) + + +async def background_task(): + """Example of how to send server generated events to clients.""" + count = 0 + while True: + await sio.sleep(10) + count += 1 + await sio.emit('my response', {'data': 'Server generated event'}, + namespace='/test') + + +@sio.on('my event', namespace='/test') +async def test_message(sid, message): + await sio.emit('my response', {'data': message['data']}, room=sid, + namespace='/test') + + +@sio.on('my broadcast event', namespace='/test') +async def test_broadcast_message(sid, message): + await sio.emit('my response', {'data': message['data']}, namespace='/test') + + +@sio.on('join', namespace='/test') +async def join(sid, message): + sio.enter_room(sid, message['room'], namespace='/test') + await sio.emit('my response', {'data': 'Entered room: ' + message['room']}, + room=sid, namespace='/test') + + +@sio.on('leave', namespace='/test') +async def leave(sid, message): + sio.leave_room(sid, message['room'], namespace='/test') + await sio.emit('my response', {'data': 'Left room: ' + message['room']}, + room=sid, namespace='/test') + + +@sio.on('close room', namespace='/test') +async def close(sid, message): + await sio.emit('my response', + {'data': 'Room ' + message['room'] + ' is closing.'}, + room=message['room'], namespace='/test') + await sio.close_room(message['room'], namespace='/test') + + +@sio.on('my room event', namespace='/test') +async def send_room_message(sid, message): + await sio.emit('my response', {'data': message['data']}, + room=message['room'], namespace='/test') + + +@sio.on('disconnect request', namespace='/test') +async def disconnect_request(sid): + await sio.disconnect(sid, namespace='/test') + + +@sio.on('connect', namespace='/test') +async def test_connect(sid, environ): + await sio.emit('my response', {'data': 'Connected', 'count': 0}, room=sid, + namespace='/test') + + +@sio.on('disconnect', namespace='/test') +def test_disconnect(sid): + print('Client disconnected') + + +if __name__ == '__main__': + loop = auto_loop_setup() + sio.start_background_task(background_task) + uvicorn.run(app, '127.0.0.1', 5000, loop=loop) diff --git a/examples/asgi/latency.html b/examples/asgi/latency.html new file mode 100755 index 00000000..b238cd1c --- /dev/null +++ b/examples/asgi/latency.html @@ -0,0 +1,64 @@ + + + + Socket.IO Latency + + + +

Socket.IO Latency

+

(connecting)

+ + + + + + + + diff --git a/examples/asgi/latency.py b/examples/asgi/latency.py new file mode 100755 index 00000000..d53b7997 --- /dev/null +++ b/examples/asgi/latency.py @@ -0,0 +1,19 @@ +import uvicorn + +import socketio + +sio = socketio.AsyncServer(async_mode='asgi') +app = socketio.ASGIApp(sio, static_files={ + '/': {'content_type': 'text/html', 'filename': 'latency.html'}, + '/static/style.css': {'content_type': 'text/css', + 'filename': 'static/style.css'}, +}) + + +@sio.on('ping_from_client') +async def ping(sid): + await sio.emit('pong_from_server', room=sid) + + +if __name__ == '__main__': + uvicorn.run(app, '127.0.0.1', 5000) diff --git a/examples/asgi/requirements.txt b/examples/asgi/requirements.txt new file mode 100644 index 00000000..4892ab9f --- /dev/null +++ b/examples/asgi/requirements.txt @@ -0,0 +1,8 @@ +aiohttp==1.3.1 +async-timeout==1.1.0 +chardet==2.3.0 +multidict==2.1.4 +python-engineio +python_socketio +six==1.10.0 +yarl==0.9.2 diff --git a/examples/asgi/static/style.css b/examples/asgi/static/style.css new file mode 100644 index 00000000..d20bcad9 --- /dev/null +++ b/examples/asgi/static/style.css @@ -0,0 +1,4 @@ +body { margin: 0; padding: 0; font-family: Helvetica Neue; } +h1 { margin: 100px 100px 10px; } +h2 { color: #999; margin: 0 100px 30px; font-weight: normal; } +#latency { color: red; } diff --git a/examples/wsgi/app.py b/examples/wsgi/app.py index 7f4fe4ab..92eabb42 100755 --- a/examples/wsgi/app.py +++ b/examples/wsgi/app.py @@ -9,7 +9,7 @@ sio = socketio.Server(logger=True, async_mode=async_mode) app = Flask(__name__) -app.wsgi_app = socketio.Middleware(sio, app.wsgi_app) +app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) app.config['SECRET_KEY'] = 'secret!' thread = None diff --git a/examples/wsgi/django_example/django_example/wsgi.py b/examples/wsgi/django_example/django_example/wsgi.py index cc738a61..0f25a701 100644 --- a/examples/wsgi/django_example/django_example/wsgi.py +++ b/examples/wsgi/django_example/django_example/wsgi.py @@ -10,11 +10,11 @@ import os from django.core.wsgi import get_wsgi_application -from socketio import Middleware +import socketio from socketio_app.views import sio os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_example.settings") django_app = get_wsgi_application() -application = Middleware(sio, django_app) +application = socketio.WSGIApp(sio, django_app) diff --git a/examples/wsgi/latency.py b/examples/wsgi/latency.py index 45252f9d..8ce20483 100755 --- a/examples/wsgi/latency.py +++ b/examples/wsgi/latency.py @@ -8,7 +8,7 @@ sio = socketio.Server(async_mode=async_mode) app = Flask(__name__) -app.wsgi_app = socketio.Middleware(sio, app.wsgi_app) +app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) @app.route('/') diff --git a/socketio/__init__.py b/socketio/__init__.py index a8e10079..10eb3250 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -1,6 +1,5 @@ import sys -from .middleware import Middleware from .base_manager import BaseManager from .pubsub_manager import PubSubManager from .kombu_manager import KombuManager @@ -8,12 +7,14 @@ from .zmq_manager import ZmqManager from .server import Server from .namespace import Namespace +from .middleware import WSGIApp, Middleware from .tornado import get_tornado_handler if sys.version_info >= (3, 5): # pragma: no cover from .asyncio_server import AsyncServer from .asyncio_manager import AsyncManager from .asyncio_namespace import AsyncNamespace from .asyncio_redis_manager import AsyncRedisManager + from .asgi import ASGIApp else: # pragma: no cover AsyncServer = None AsyncManager = None @@ -22,9 +23,9 @@ __version__ = '2.0.0' -__all__ = ['__version__', 'Middleware', 'Server', 'BaseManager', - 'PubSubManager', 'KombuManager', 'RedisManager', 'ZmqManager', - 'Namespace'] +__all__ = ['__version__', 'Server', 'BaseManager', 'PubSubManager', + 'KombuManager', 'RedisManager', 'ZmqManager', 'Namespace', + 'WSGIApp', 'Middleware'] if AsyncServer is not None: # pragma: no cover __all__ += ['AsyncServer', 'AsyncNamespace', 'AsyncManager', - 'AsyncRedisManager', 'get_tornado_handler'] + 'AsyncRedisManager', 'ASGIApp', 'get_tornado_handler'] diff --git a/socketio/asgi.py b/socketio/asgi.py new file mode 100644 index 00000000..6596dbb5 --- /dev/null +++ b/socketio/asgi.py @@ -0,0 +1,38 @@ +import engineio + + +class ASGIApp(engineio.ASGIApp): + """ASGI application middleware for Socket.IO. + + This middleware dispatches traffic to an Socket.IO application. It can + also serve a list of static files to the client, or forward unrelated + HTTP traffic to another ASGI application. + + :param socketio_server: The Socket.IO server. Must be an instance of the + ``socketio.AsyncServer`` class. + :param static_files: A dictionary where the keys are URLs that should be + served as static files. For each URL, the value is + a dictionary with ``content_type`` and ``filename`` + keys. This option is intended to be used for serving + client files during development. + :param other_asgi_app: A separate ASGI app that receives all other traffic. + :param socketio_path: The endpoint where the Socket.IO application should + be installed. The default value is appropriate for + most cases. + Example usage:: + import socketio + import uvicorn + + eio = socketio.AsyncServer() + app = engineio.ASGIApp(eio, static_files={ + '/': {'content_type': 'text/html', 'filename': 'index.html'}, + '/index.html': {'content_type': 'text/html', + 'filename': 'index.html'}, + }) + uvicorn.run(app, '127.0.0.1', 5000) + """ + def __init__(self, socketio_server, other_asgi_app=None, + static_files=None, socketio_path='socket.io'): + super().__init__(socketio_server, other_asgi_app, + static_files=static_files, + engineio_path=socketio_path) diff --git a/socketio/middleware.py b/socketio/middleware.py index f7f042a8..aa1b33b4 100644 --- a/socketio/middleware.py +++ b/socketio/middleware.py @@ -1,14 +1,21 @@ import engineio -class Middleware(engineio.Middleware): +class WSGIApp(engineio.WSGIApp): """WSGI middleware for Socket.IO. - This middleware dispatches traffic to a Socket.IO application, and - optionally forwards regular HTTP traffic to a WSGI application. + This middleware dispatches traffic to a Socket.IO application. It can also + serve a list of static files to the client, or forward unrelated HTTP + traffic to another WSGI application. - :param socketio_app: The Socket.IO server. + :param socketio_app: The Socket.IO server. Must be an instance of the + ``socketio.Server`` class. :param wsgi_app: The WSGI app that receives all other traffic. + :param static_files: A dictionary where the keys are URLs that should be + served as static files. For each URL, the value is + a dictionary with ``content_type`` and ``filename`` + keys. This option is intended to be used for serving + client files during development. :param socketio_path: The endpoint where the Socket.IO application should be installed. The default value is appropriate for most cases. @@ -20,8 +27,15 @@ class Middleware(engineio.Middleware): from . import wsgi_app sio = socketio.Server() - app = socketio.Middleware(sio, wsgi_app) + app = socketio.WSGIApp(sio, wsgi_app) eventlet.wsgi.server(eventlet.listen(('', 8000)), app) """ - def __init__(self, socketio_app, wsgi_app=None, socketio_path='socket.io'): - super(Middleware, self).__init__(socketio_app, wsgi_app, socketio_path) + def __init__(self, socketio_app, wsgi_app=None, static_files=None, + socketio_path='socket.io'): + super(WSGIApp, self).__init__(socketio_app, wsgi_app, + static_files=static_files, + engineio_path=socketio_path) + + +class Middleware(WSGIApp): + """This class has been renamed to WSGIApp and is now deprecated.""" From 1768e251b90eabefc661489595916f7bd0d0380a Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 25 Nov 2018 11:44:16 +0000 Subject: [PATCH 031/571] updates to asgi examples --- examples/asgi/README.rst | 7 +++---- examples/asgi/app.html | 0 examples/asgi/app.py | 1 + examples/asgi/latency.html | 0 examples/asgi/latency.py | 1 + examples/asgi/requirements.txt | 17 +++++++++-------- 6 files changed, 14 insertions(+), 12 deletions(-) mode change 100755 => 100644 examples/asgi/app.html mode change 100755 => 100644 examples/asgi/latency.html diff --git a/examples/asgi/README.rst b/examples/asgi/README.rst index f1ce6aaf..4fe8af88 100644 --- a/examples/asgi/README.rst +++ b/examples/asgi/README.rst @@ -1,9 +1,8 @@ -Socket.IO aiohttp Examples +Socket.IO ASGI Examples ========================== This directory contains example Socket.IO applications that are compatible with -asyncio and the aiohttp framework. These applications require Python 3.5 or -later. +asyncio and the ASGI specification. app.py ------ @@ -36,4 +35,4 @@ or:: $ python latency.py You can then access the application from your web browser at -``http://localhost:8080``. +``http://localhost:5000``. diff --git a/examples/asgi/app.html b/examples/asgi/app.html old mode 100755 new mode 100644 diff --git a/examples/asgi/app.py b/examples/asgi/app.py index 0771d476..ab440e9b 100755 --- a/examples/asgi/app.py +++ b/examples/asgi/app.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python import asyncio import uvicorn diff --git a/examples/asgi/latency.html b/examples/asgi/latency.html old mode 100755 new mode 100644 diff --git a/examples/asgi/latency.py b/examples/asgi/latency.py index d53b7997..97c72684 100755 --- a/examples/asgi/latency.py +++ b/examples/asgi/latency.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python import uvicorn import socketio diff --git a/examples/asgi/requirements.txt b/examples/asgi/requirements.txt index 4892ab9f..0c0b87c8 100644 --- a/examples/asgi/requirements.txt +++ b/examples/asgi/requirements.txt @@ -1,8 +1,9 @@ -aiohttp==1.3.1 -async-timeout==1.1.0 -chardet==2.3.0 -multidict==2.1.4 -python-engineio -python_socketio -six==1.10.0 -yarl==0.9.2 +Click==7.0 +h11==0.8.1 +httptools==0.0.11 +python-engineio==3.0.0 +-e git+git@github.com:miguelgrinberg/python-socketio@b214380d056dbbfb08273ac482633254176cb847#egg=python_socketio +six==1.11.0 +uvicorn==0.3.21 +uvloop==0.11.3 +websockets==7.0 From 87fb830bd803809a6577332a838285b4bc22dce5 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 25 Nov 2018 15:18:36 +0000 Subject: [PATCH 032/571] improved documentation --- docs/Makefile | 191 +---- docs/_static/README.md | 1 + docs/_static/index.html | 12 - docs/_static/logo.png | Bin 55497 -> 0 bytes docs/_themes/LICENSE | 37 - docs/_themes/README | 31 - docs/_themes/flask/layout.html | 24 - docs/_themes/flask/relations.html | 19 - docs/_themes/flask/static/flasky.css_t | 577 -------------- docs/_themes/flask/theme.conf | 9 - docs/_themes/flask_small/layout.html | 22 - docs/_themes/flask_small/static/flasky.css_t | 291 ------- docs/_themes/flask_small/theme.conf | 10 - docs/_themes/flask_theme_support.py | 86 --- docs/api.rst | 88 +++ docs/conf.py | 285 +++---- docs/deployment.rst | 277 +++++++ docs/guide.rst | 346 +++++++++ docs/index.rst | 765 +------------------ docs/intro.rst | 148 ++++ docs/make.bat | 298 +------- socketio/asgi.py | 2 + 22 files changed, 1013 insertions(+), 2506 deletions(-) create mode 100644 docs/_static/README.md delete mode 100644 docs/_static/index.html delete mode 100644 docs/_static/logo.png delete mode 100644 docs/_themes/LICENSE delete mode 100644 docs/_themes/README delete mode 100644 docs/_themes/flask/layout.html delete mode 100644 docs/_themes/flask/relations.html delete mode 100644 docs/_themes/flask/static/flasky.css_t delete mode 100644 docs/_themes/flask/theme.conf delete mode 100644 docs/_themes/flask_small/layout.html delete mode 100644 docs/_themes/flask_small/static/flasky.css_t delete mode 100644 docs/_themes/flask_small/theme.conf delete mode 100644 docs/_themes/flask_theme_support.py create mode 100644 docs/api.rst create mode 100644 docs/deployment.rst create mode 100644 docs/guide.rst create mode 100644 docs/intro.rst diff --git a/docs/Makefile b/docs/Makefile index 68ceda87..298ea9e2 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,192 +1,19 @@ -# Makefile for Sphinx documentation +# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -PAPER = +SOURCEDIR = . BUILDDIR = _build -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext - +# Put it first so that "make" without argument is like "make help". help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " applehelp to make an Apple Help Book" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/socketio.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/socketio.qhc" - -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/socketio" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/socketio" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." +.PHONY: help Makefile -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/_static/README.md b/docs/_static/README.md new file mode 100644 index 00000000..0d94aa7d --- /dev/null +++ b/docs/_static/README.md @@ -0,0 +1 @@ +Place static files used by the documentation here. diff --git a/docs/_static/index.html b/docs/_static/index.html deleted file mode 100644 index d2f10fe5..00000000 --- a/docs/_static/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - python-socketio documentation - - - - - The python-socketio documentation is available at Read the Docs. - If your browser does not automatically redirect you, please click here. - - diff --git a/docs/_static/logo.png b/docs/_static/logo.png deleted file mode 100644 index bd27187c0c824245393e5bf2c25f392e8d3ffb23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55497 zcmdS9^Iv9P+c11)yUDg~YqG7$HQ7zBGf%dgY};I>X!dx~}KGulMu(1<(Fr zf7V`W9bDLl!j%=Jkl^v)0RRA!jI@L*007SN*)D*E{(L88QB(i`@HJNA;>t4O;v~w> z4i;9n<^X_ncybDiI>rj#@a04ywk`>{s+dFI9UbKz0Di7dQW_YHE=I!9QD3?pK}H6C z8o{K|r$K8XE>XuAfrQXb5*b*}gjJhc4|u`2JmJk(wRX+K7nyq)cD6N>bkz*Et0nPH;H6=3~BgR!yu=9}eeS9MJELu$?3cf$> z+@)=ca3@ItNR!ngW>jcwBKJ9*eLjp?Zv#3dyO3|gXy z2*2c<{(h^nXe^}4B&6O+cTYzSkYe@{Mi03{JG$#dMvbU7CRAgjaK))^oUUp>31r ztgUU%qi@qiFj($oXQrND>)$l zs}=}Z7$BewZY_dv3}M=+X0r{@pF#Eo3d2I98BuNnh@`>E;=q=G(InXWLBEVpXrN&F zjFpjM0x)uMv!Jm1zBodxLqGOm%)mhR*|5M5^+7_4di;c+C83lioC}B|aT&pK4iq2} zra_GW=I8QA1H$t-XDn!l>4IhR#Fdfm5M1%vVZQ~n=Se9Gb0BsFb@#%`2OJn{Xk#0| zuJz0R!A1_%>UDC2&q6xt<=;kc1s8(n?!(@}zw&KG@k5Fw1(1@vNhc6_u`pOsIAx+E{=rgpcBxIL31t)9G+bZ1^M}V4nhRSms_HkF3A5>?DNNlS4?#@K{J_d~rxRMM$GY|U zk99wNh-#$eX#C#joof%uPP9$5%h>OG^H*(8zE1^D2v7V^qOa88>A8w5)E?+$aNzw^ zVyt;7j@gc3R|zblNu;N!=i%=XOstXGA_8Kzl$&J8=+x5n6oLulv}u@1tI~9&i-}B$ zUNV=XX7yAnF`Y>h)D9%6aj}Ut382v&T4zl02z+ziuktIRW%-RNL@JwVW$NF`5jYbv zBsWB>@(l8Y3j7lCWzwbV3R_kG7L65V6kbSo#Z$@tm1Zx}lKn1+EtiwjO({qwo-~{6 zlxRF&nMj++%Q#QYP9>MfnRuV7kg&m|NAIohLmHX6g#L(hIC(stpTS+m9vC{)Xlc)Z zY{z~Md(LL3OhjEnJ)IDnI-M#^wM8jhPEul2hFTt1rd;f(m=_x}<95Q}Mb-uM3w#H@ zn`fU29CoI8t{pB;cd%a;?D~Ag9>f_G7=#`KMHnXG$P&rI$#x`VBxlo^CLtxK(tW2P zR_?A8U^DdV9`KoZ?R=&_6xIyTK-2)&(5xWPY%60cE-yYUqEp`}@se?qh^{`&$EwjR z{9$0H{j2z*a7v3=wI}yIBCpqo4IP&<3VEnBG$OPmRC_N@GIc1pINRdqA=M=GWQi^X zqh6JE6viSzq`cUMzlpjM|{9v=L$xuLi7wZ4{eDuBDW#$p7krZfcgXV0|63&4(kp_ zEh>HJWXNskjmi~6I8lLWlF9+gnmJ0p)yDX!|FT%KIJ{U&J|}H7{W;B!&4$zayS?QU zD>Hk~chRP+Ms0^#lk26q5w@Wl;DI=O#-d3DqvpBhR;69#*QNWV0j^ap6|Ms=!RDK0 zu;w+pnpNX-{qv=@Uriue2fG{#|NeI+7}GGwFx{}cuU?TRScEzp+odNXKLqyw9Q}5T zaDEvro7NdfKiA)RUH>{B@>libCc&yV>N}Bnw-cw^mo~0;Ki39#y}wI`F|FQhM{Vw& zBc8z?NnQfC3wKesmA3@<^*6fbi-*zo5BHk~8w0B5PTHK6D#Z9XO+tj-m|G-Us#`~X zp?+jNOM_P?IfEV8k4&>VWZH9QD6cy29d9|%$54fkSvnDVc(skTCy^ozy_^BRL>qH8 zM4LqS`l|b&`jo`(#D9~X=gsAfi`$3^hqi@%3B3o>OcS#l2&TAoa8Ei@(qsP$7)}!o-=sGpS z#J}H86i>`ga@fS!6)nar;Mp83a4qb{z0%Lq-)^lZtfx*NIPq+_s!t5Z^}Q_#8=1Vm zfxN2jdH=$2d)Sq3O0TBXUN0*YXTo#){vl{?`%iH>; zA-Si?CUdaacdh%#>9+bP^AP*Ue`9LJ?@g+KSk5nN9j&9FqPKm!S8-jjZwjU(%UuI| z49fx=`OKiRqwJ?jlGdH^bh+Si1Y;!11?Bmd%X9i`daIR^E{CJ;nxf7*ka@K^(gEGU z+L-Xg(M69-?}5mh=wi?_Owa4@kCPqJ8gYd$?Oz7NoWkZ&dU(*i3U9x!<=D_A)8Eqf zXuy@fltWbTTiaS&TWeS&oNn_uZCuSBNI4L@2rsER==+xjY{vN$5MUGg-^(p9qbNRQ zTMAow*S{xCmEB0{ni^m%BrS5RPS{-7gj=2nox8MmE#@^{M~$YJH{g9ApQ6p8=TYLz zaq+tmdkCrHGF}@QY>k9CuUO5|hwpK%F}0FfHs2c8s_`&rTNpj-bX>eXU9ovsRy1ts z+VmNCk(^hk$c*mMeOvuwzOm>!eyBNa*RfW$5!pVy<<#wRT@Bixf7E`oUpI8_0f~+T zg$a&(zYsYSBMZv<+&wtG?1fX~d)0M$Dw$_v39ET(UT5|>Ao(u&9=&XU9E4E3-pzpQ zS1*qXTRq5!ow?mKer50V79&2#XSGLH=UU?iNZnZ7**K$b;t#Pm+%FD_ZznfLle1T| zeJ%MdiI>eiT3&|teK+DD?cP5P*M-;Nv7R|eLK^2M1|0WmGwqhpxw^FoKt=|N=UD0JCYGGCulABvGS~b2-V4OLNPwp$c3$3D0p8~- zbh`&ufZZPZyAK_i3bBv)_lW9di2(oT9)QCis0QKgR$Bl7?3b0gwyU;+JfEq99h0${ zgNZqlhn?f6X$t@dc<_BT?aW<`Nj&Uq?Opgh1j+t2gYUEbPd76e$-kz!+6a`Vkht>a5vUmB9Sf2ti|1-kO%EZF_Ke#_Z1^(&fQ?~Ljx7C)gvNN}L`J^Gl$;Be@ zulfJi$p2FOFG!vLg=FPo|8L0u8u<^T0P{Zr{Fgxg*4DpzKh-4!FTngisuzM!ry9fu z07L*X5~AuJU}s%0Kng84CLgMY;Owkh+bNljF<$hjU=~S=AUJ5@z&`3rS}Nv+zZ3vCw0a9uaAF2 zOz|K;0S=?Q&HRFV{{{S?G2-4Q-%p7DMa#3tvkj5Pei8qS^zh#Zd;oio|3!P-WjksQ zh+;I=Ds{;H=f4qLfkkcohrxf-2w;za5CZb<5v6Y#{r?#KWRm#*NhT?>ofseR&;C*M zKZ^6eA%G1LQ@b&0f7=Y&+uiLnbn`Cvt>GQnLQ^C7IWUmjj{RGm{cY%;y*X?! zo%v%Ob^_NIxAtq7n%)5s?wLJn?iJ?_uY6p*+crifw{1^VH~&Zfj=jA<7tO|=o?0(j zd^KZZV#Y>BYNMi~6<&X(hF@P_CuC$4i-eF20we77CT~6cZ?gxsVhBGMoa#q*$QJs) zl|I1-gNW#X&$cLZc}aC<=#caHxV+YEQ%_~I(t6Fy>uA(MAkZp>OuR{be$F_zvcj~n zK?q)6p19T9}AnXQd-TzzMN|#=dK+b*jp4-dL{x`Gcb6x84737pEJ;Ma*93K%b2S3r% zQ)@6yzM`HUtf{UW9Mzm4&73g9Tu_GV^`Jwt zd)MEZ{mKTWxWYmTOS^<7syO3oa9kpX$No<{GxhUGqhBy;qL)Jw01d%@O9Ey9gJytjbE&Qo#iW9BxOq_kFSMe zZ+6GxLZiUP>)y@H_+Rt!6r_5)weh*NwJL6L77=9;1?WgSwRdkToQ!DE)~qyMD(VuE z^76ycFIxK4mwt~@R1+-4eOK139z+inqrT&-Qdc+kH6LBgtn?cDS=RAIMP!sZWTU5b zxYR{1TGA{XB6I9>6r7YQ3FzVpNh~pJp||~an5lqHbY{~%I5v*JKk!LJuEczq6Q6PEK@TO|(nXSxHRoT6C56{U_jI zd;sk9wvWuGF5A&Az0kh=^JRW5J*IX+b&C&~zAdW{AU9i=-WTw{@9#pM+W{Tha?U-S zSN$09+7lcS!toL`v}{HZv_KguVVVTE?YInzgusO0Evk8z2{uSpUuDb2K8A*#6)VJZre$R=rCKbnCoeV6+~MgYEm;~lFrD8KA2RWS_ZA6^PMt95=F8sB-LnTu z{_F$q!6Z-Dz5e=_5zA<4!AyLGz%!7K9(xswZ-bg*5XV58WRqp-qM)LJz){{4nb~Ou zR@8x$nWJp%>yP;QA zR;Ky1l74EAWdK+q&3v?|dWd(RpEQzqb7k9T4Ci~Y{`onSjDSJVZ?@;{l%B0LDEMMq z+n`u(EGk4S!%J|bVwUNm9o50 z(c)62`;1O7i~eE+OH)9vV}?_#bUGwNm>j~nsf>z7LmfyhtWm;?Jxi3gWIfiF^LjTb zS$A7ja=f5uyPIo)m@$KOX+V|67{Nh<#JO(KvwtoQOpQv?>XY+30oQTfY)a7cWfg&I zq$79KKB*3PDS=u|60+3YPUjC5n%demx+T*lt(BP&&6|W%WCtsCM4xT-(9c}Wm}t;H z*Dr|>o`YH3zozQHd?H}Y$<(G{Y{Ufvaza4=jFv~VJATsiycY68~DQMD2A6Eb>p6; z`i6s}rTVQ)S?FaJ{~$qbvwSMu-~&o6)8&e4#i-iwH8yPr0D^6@Gklpd94C)`6$O0- zlNZBgMAbnSMz_|nM0fU}P^81EPR{B#Nt0EjWQQ=K`~ofvcZ&1nNd*0x#;KN^R;2@M zIMpf50+t9s720l7RMVoQi@MoxlswE)e&n9{wOI^bO_NJW=;PU614;77kB4BaAYI&y zlnLh05)1p(mLA`thYj~CQFYYSOl0EJhMIT!QSg}*QJ(S{0tqDe8l_$_yYW%24!kphN@-MP@S@W+wbP0#ys3n#av zmzVMzcJYmIZSym`(%#wGvcZ+uB*HJZ8Xda&=COM;zFsP*xk9g#wgjy>@;2%Hlr&9B zC#z&dIkZ@ag;y~Q#7G#5-(b%a9({-x?u}C5>5S;d8>A<+_-KB<;RLG>QTp&#qJa4s z<)EL%R8Z=Wi^W|mw~<H4DwB|17rHFRZ4wu*OB0KoT%U+Y z=u@`gS@s2T+pH}uZ?5}06>cQ$6L-f3RoxvcD#7?+jWRNj&=CcoZo?RandV#D^1 zxO@=uoRpx(L-+zP(3)SU?|B_x2v~r%E=Lw_?Pg>((fG?1EJmNF#8_RNCE1+3l7kQk zRUcwpl=V%CSQq}@9T?u8KuL|z;DFdISITFIo&f#ljd=5V3G|QFSkC3SDKR*?$~x=rf|wl7cs*uA(Db+iTTR` z2+T-gY+x+fsWAW$DiUE70-p0W>|E*5gdG(@_A5k4fzr@Iu{!CXfbdE!-6`-yIuuVL zVkv47Jd(4Byo%)froRP924^y+~j0B93RLK8RC$B_uhWu)XVv_X&9$K z%lEu2P%u0sKU^C}=aqzMS~ebhgWJ1HrBufI1#psS4ggFGji`-r$H!@E>QGxD@-hkq z^AxZ8=)+(fLln3>Mc3D^7tyIsG7=`tn3g*rudu?dBAxctQ&P^;eRAxmapr7yl0)w; zkQA_pmG|S7o~40ElNcJFto#G1RvGY&$xqPw_Mo?L6taEL^_KuAx6E>Ja9M8j^_G&? z{R;~$-y}(|EcXlMB-=WX6wPdzGWdeesPbbO)t)|p6qs>_KvCcwmOWT;ZR6mebtN{l z3k6m@YGnAG_eZ7X#}CrVuZ45<1g(u8w!iEjCw$rH%I?8RnZQg+tz(NJ< zR1$b4i;0;PT)t%Pbumy{8eZCIk!_TgCMAjim|_$|QF36S`imJZzz3F&htb-SR?_r^ z#j$uYz^hU#L5iOef_>&S0I#eZR#wR&e1V`kA!7p{U?O=?G97W&4|%`o?U3B&lBxi} zVTVBN)xLiDE$_ttUQOfJE7Dul;-}O^ct@N^MUF;z@Mbpra{*J$lo=m`+2pYiPm zY~t)@tEYV$?4Z}|N{l#VaK_P)gE#{lfUPR}EH=tmop2VYIyzC6lypXy7Q%vm$iPDs z!(4vaqLsm}4VxSur+TIYiMM;kYe#wppDG5+DI>YIS5F32@fcH`s0&Q1*x~-XL)LxH zc!;3`zrYh<+3U@QJ`*>Lfj#^xkV3J-|6^F{sq^FA%~1eJ*5}p%pl?j4o`VH@kxs^U z^Fz#B3c!vwh55_3TQkk8lu+YYi^AEokB!xLkSRv8GpLXUQRp-=e?=mIVJ(6*szD+oZ%bOaAz(WvO84&OH z&~RD0@-qV)f|<6@woHWQ`$4`NI)g!U0RZdx&!1}!077r%VZV_!EdkS)+D^^r8eEw$ z(66x`{~XYo*`Gq^8rQrc=?Xh)01Vl!XB*-Yp|t_5L5iW{uO!BHNC*aiEQ-Z^RCX@JF8(i#d4I5H=ED5} z0Bb8wKI`ZI@%Tivl!1&1R{!|7G&@i$+OHkr7_ojzc#(tPClu-6yD|8do=eX?VTC8Y zzFzpw=Gz-Y0s$bwzrZYJ%+W9>0adUl_%Y9>x2!tN0Eh`=d-P zuPT6uG1% zHkbRl{)iv%lAxYnYux3%SN_;=&KofKN{c2S!f?<*gVm0D@6vb@bHKf^te+>k=#>plw?MK$52pX$)I&IAH~>N)qjM-Trb^_vq8+(A~PPRH3=RD|9EBz$a9*`JH}tuBrYzf0DJrgMn2mEBJNZ#fXg$c~|W>FAPw zsV_60FHgDI->fDtE~_Rnw+YQmJ%O3;7gMi>^4dCt74Bx|4PMUtTGayh^ePT&SB`x? zmPV_5MemjcNgDkpuVz0jG{CXVV1y{)nNkBx{gZy_x$jPkwUbabs3p$LBWUER)p{3P zc8Yy!w`-%P{h9F!OP?jGhira#h^p0 zqQw5|-Qx>@>P6tO=J+!%xD9||f?(Lph;9*mifWDLb|N=@)$f9 zfi807?DV9YNlWeBD8-&zE)$a$04aj7B}2bXap@cb)cDDx6yrVKy8Ad*?r(hOHa6*o zQL|io#?4^>aWEutykPn`PQxZ5L#l9pC}|gzDE@ddqT5(AyI*bw)@wA|c|+xzGp8_E zsvxQR`x67lPziFrWfSoCEF#8!H*U6Lyz2Qr>3i)2iR-wIl{CxrE+r<<5Dw_c_j_VR z>5q<)h)c}WV^K*V6K7u*)@!sJw7HlDL23596^$tSo7iAX;Y@*jR89rs8FJYoCcuZ0%I9Zq^h~ zpRW33-NNz;Q12@GR|o z2j*sUs5>(CVP6j?nMZsdHv*DR#%psAy;bbfTHPV+4M_H&*x5H?sneX`@js)xP!`W& zF9c6&yFhn9v)yKoJ7{t&_T<8;{jH=QCfoF8Tlg9D_m>0J>b2LhM?p_RG{XX48g7CR zehP6Y_*`%Au<*NZJWrj$!`TX1PuLcpR#7&+V7=)$k(Hf&V*jqs=rmFCKI*G}n$R}I z$T#APOVlfEW6@A4vAHE)s(Z%p5vNA2$N9@cAxi`6cv)JoXCpTDBQa1yH82FtLmt;? zFhzs~2{C6yeR?t*NkJI&a2|(k=%btMeZl$-tFWgB+aH3M`-vv!8>1>16b5!3V*mFv zTFf-KKOBsR;YW1_5lTR+pR2hNsgW`*Z76V5d%Nmi3b+NhVo-ks*Pw`0uoDcK5sGP6>5vw6(27(uj8gJF9mfz$0F<=0>I*&8W$sG} zzhFNFovEX`JeyP=s0&dvg~iCoiFXy249ZxOD6+BdQZjy7qy1?FPclj-Pph9X?!g6~ z%f=Ql(!CRR_Yf`T?f`49ue&-lLquag6pF^h=nT+4W~Fejg&=a?F2eJSsi*Ema3Nw1 z0bp~O+daq1r>)(eO`Y+g<`VKXN)dUV89eV1`!m8IZLf%X5%~v!GB?MS-uQE{aeN1b+D~uKm+lUs-IF4NKM$ zO2i!VQ5e3KC*t5}cxcbp z{-zhACm!G7wOCziT1szSb{*Oh(ZefX!pz_pD*tK+B8`S+r6+hg;DpC0Xw$W4`oYg; zYjkP8$-No6wQSY&v45+0^-zdjV2IMB;1EcA@Qo>pv~qv)vRJ1#J%%6qgGopT3_Kn= zS#&ha(L{laSy9~svBp85Ap<0{#5Hwlm11cVL=hwEyd8oBVcqLLV!3hg_ixIj&k?aY zyURiHrTRJ8v$jT)^5$dr^a!)RCp&@OHmmv`lBG+Ya+ReZlDMKg^5|T5?b#UR1@mVyA z%w<%qiI&*M6M3qwLd}J#mN+Ao)Ot;p=kZdb#1%_}XKE>opsp#ZDTI*uacjBNbLx134W*MIkhx4#fyc zzWF|`4#Cmqvxo1`%-r)@nGsNGuA><-$Vs2)+s8#))2i-#i$FomEjNdTo0OqJDTBM{ z=-3CQf5HzIt%`*3mEAs~6CrPlHT{4_1TIm#Utqu9!Bme^9}<-(?l}uR_Yf{A54s`B z(MKSKP=@d%NgasWhYrt6m^oYQfvexDj;D*U-#c|~-S0K68}D-TbNyOXn$N4_VC&iIYIyC_q~+4=E+&hawjzSZfpMDB36v>%SArwNEm2Lzx>Sihfs`}Bq3I=Q{rXZ_l? zEWN2ey0BZ*3?VK@gR3`k7GJ&BEbIoSkb8Nx85Tp1O63R#l(S{ zo1ZTd3Gw8jksjXRF!mq*q3q%yrehPe^04tW;L5oRlmEF8oI&^deGkkP9~A=pw+qbM zoGT)3y|w1@JDVDmTQqs$Oa4p?$_$1fBeoaley<}@agEBipKs;m)?gEI{&X}G3DP!qPtD7n6Qq=fXuMVGM-S~82yFUUw%}QuCA1pd2r#qp~myk zKmYJ9uu*JmaKalnJ3BAvoAs|E7|=9Q+Ze$*UUq}90nFUx%`q0FOuxq{MgLS&1alqj z>R_i}%Gn#oo9$0SXoAT~_YaVh;0lCFf-NGoh-@sM!Vmn+)li8ZQ$Imo2oA=Ng4s4E zHOfl~9eu`xHP*v+2wA26vMdW$YP^Y$-anjJn+}V&<^d`npfOrQA~_$Lx!LGrCU`cjQ8r%bvlAx(9*d4es|wJq=dV%Vr_p zH0{T~m~nIif2Q^ZTH(pIt1erKiJO*Ef0{$-&3fU9Yk&kVQ&m?l#|x>ix7MjOq78N# zg*3XJ(x#1*fwrj3_HkhEqxSwmU<;?)Z11b{%MMHgikr(Dy(%=)A`Xx@r!H`rnSjL@UhSI?wTV$m5=BII2JK->t za9uQXOceFlySSjU(fB-omGoZp1P^EaxL};gyCe_>0%mUpFG~DxZ~j!-4u0W72vt#= zcHXb48b~_G1Ro~IIA~i)stgI9_PJ3`wA=ySj{ey%AMnQ0ctm(9yc;6Sf9yqH(y)=k z1v-dj&UGKkL-R^Yr9r$PeonTfBcw|F9vB#yeH%ki=ep<7bR&}a3fZMTQX+OOIHCP5 zd}L8$J$hu_RQ!U^Yp=#$I&JHlm1_#0r3S2eEo?=G|BMV~B@giXa#}sr6|N~lvYgDj$x3^d#x9!f`!aU; zIy7p-xi;)n*Osc|kI$^)^7% zQXAoIdaenw&Z%%v>&gLZ6El|1HEH9dLG@&?Q6;!g3o$`|B}q`F0ORFES3yTlAg$~d z=ozQRloCbnX#erlR;= z&07bu8A1f&!iRCCHCX1eTsgntVLR(}J2LvvlB9r;#grSOy@Mx1vtPh-haZd z(~30Z3eHebw7jdNOl&bIG=1J)vkVpV64{$DhDmSyGg?f_!jWb?t!L}c3AU?uQPze} z3}s(+BEC1}js(bA^OTyCUgKh>YV~)D5=VQ;DB$(G1kc;N!UB=93>G_5?eckb--6r{ZLfS;kHUs7&U~hK5rtUVoe6Xl&s&Hu6`__B|B6-IET}XUk?y`10CSN5L;PB`u((Kr zYMk*R3p?p7FN=%T?}M-JJ={pwcSbfkL7#Frp)Vu8G`BDESxG@k=uNgOkYM>Yb{^fK zU&kg83VzswCjz`Ps_hu%n{T?zsAMbIEzisJ&pWa0Ce8(ECGRe-9Npk?lC>Y)KLwiH zVLOQu3_o7N>Wv2yZw8tD5Z*~#7Lr7f0HPB$e)r^1%nPu%9_bx7Z0S9xCNfC7#j8Zw z5sb4dsk(xbZUii-H*}X(=v#L-WtB{czW^+Y9-P@jhIKWK&CUJ$O9*joV8C0n&M@(E z9o-lqsco^`QDswAW6xwvtOGesM~OLhX9`-R!xD9~DWs%)k%~CjMZ^%c8Y@Tz#ePee zSCMT5@I-~BUeVIg7-`~0mK9zbv+3q@0IP@OaYxO9i9%se=4XyGaxv?8C?vktYPPCa zA#v}!uy>FUy)_V4e<~+zoQk3>XS|-Xf}-Kh)>03ss&@fnz45n^VJUm1^kln&e!f5f zA}klV6XUew5ip{UYygH<%$(Z#bt^&S+<)vem=VxFzCV-w4Zym0b9*n%^anMeodR?Z|Ll)zIsewjkWB+4uDY+g9fxW{){f^n9C}4H;kUr%2SdzwPd_s(E@927LiB|J0S1;Y8AeNMNNPDQ zz;9ZN^(ng88793q3_QZADXD#_kW)1)$><{n6xSy%Kpddh{mQYp+p@+jeD%0{+PM*R zh>?b2eB&VM0*&(*KJ$XY7D0+zjLXEGK{+$wS7D}a$AHg_M2Dh&Yc4G{3j<;-Rhlv6 zDt7l3DWIx^!(lvZ8e2ydd>wLfJ@A0uyzg|J!X8n&`}&8ae#{M)Y3!4&cFZRDHVCJ` zKnP_LF)iAU+2uoJ*Sd#t{J=9xA+qqt7SZ!|8|iZoPbC!Wu$(%cI9ppW4exou`)O z=?=lgEk(EY^!4wL#`mE}N~7%40t`xm_rMp?R%Pwg)n8b_fobeF>aap?bH7_hRwIh8 zhwOHVR#XuaX!4X};ctwiGJm12-?`_aK!%fq#nW*7Qdgh&nqT?>#(DPTW%u@Li%my? z=~3c#h#xex0ajI)>dg&nZ-^D*YG~dLD=r>RQg^ofiKS5aY6t_5b#*2x!08m zbK~JeM_pdU-P}8dq<1ONOT-cFoZW-2HJ|2U zx93A{hXgGHof?Z=cwn*y0J>s~^*PKC;T=BLyllT$m>R*RAF?5mGERps-0A>cGfSL; zNMMo(4=gYySB2;pA9LbPBMo)#NpWXHqhCX0S9^bdCfydjku+--59|+x*@Lu~kVWqV zQ6x=VWt5<*%8$B6@u~ALf8-`MEoQgCt0io)xy21+m)WEJ%a8Z1ZM8n+8g!Cz8T3CK!_Si(^0JB_e;po3NNtS84O#2ympc^^m!wxOCXSFm;~jgi;x@ z=on=3fDLF~XR}O}vou}WL!hy0i@=MfhDL66++Cn~0#4-+LWH8s@bX0}m{@MNiKc-y zABPW)SUO6d8=C_+*Cud`jzwG+cvboNI;>iV|*2*g3F`Mxws3c$<&t^Xan@Id&zvZvDLEvL5gf6VZ3-9mmuG1qAt*3ZCY ztS5M~edYq>OxOreNgBnooX8p2IikQ9eJo3%8m9DdiS+0NePxafA?C)QPIO~sNbKaN z;E+dF>8Q0IM3!c3Rbg1>gVQhG9=E`xra%V~^wo)ft3h(<{O)K^{UIk=C&3~^y@<~P z-KNv2Tj|$#k3BsK|2T(gFyhwKDXF5U`RA@Lh=t~|pLzOG=CTxhiRQwiNf^zYBwhsn zNf>BRxd5KILjZIDJT^3exXL8>T6CB|`a%x`VTjRJa?xJ=Xn4^{M{UWVnJu7kl?>`8 zL*at;(^q|lfbK5s%kN#7`&pFtlQw3V14}m>v?n20E%$|sKb;-{PC0tZ@@FldX<1k{cv03RbfWG43W2W zv&xkP=RE|dMDa!HJ)m-}MHu+pxH5R{GMWr>aYxy9VOTB_;{ArzXlYYKanH9}Cy)c1 zO5Rcm&{y7tv^QEmuLwP0*bJ<19?KpS+$PF6=J|n&ls`TFF8wYa%Jh#UhF1l(Cv^lC zGxbe_5JFfb?za0jcCxUd@tpc}PRkP1PCAmdn$0>M3BFua8`hcp%8**B>VVaSPG#;R z;6(Zp2eVYyc78A`e>42lml03>xaE(1!zNHb--X^9ckEs3A>_kHGC*_2W7*dQcJAUX z`CUO_(KoAsijaG!7S0a706`lt&r;ow2)UW~VS4oiH;}KhrUU5^oEtp-`_#(uQMT4pFBYd@F>-g&v5Lp zCaE8uPmReX(6N=5pn3Ht*JGZ@*Wc--8TePZ#o+WmL>=r_abB?Gzw&{?nt?l2kaAYX zuA+^71k?>v&_jJ>Uf=Kw>8^D~eg8JqpM)TxZ_U!=v@eLrr}}()*EFA{b4&CLfx@t~ z?DXQQ^Q!BqD^Kb(V!bB02a9miNs}k!9sr;yhrBAm6H-3cyxc+xS|`hy$8$SEq0i3B zOb}RMqu_QU!_{Yf@4;Ka{7P|i@jLcK`o00!q&#JQssVzsl@Qra<2^j0&Dm45k1LSL5A^daw%i)gn9>d zpKJWx>S3+(Sx4S_s~AsBk!Rv$eWI5DK~@2t!;XC6}B~20=!)nN4483Ld#_mQNOb^k^h5z^PcjyT}rk4zv$^aDf|I+`K{^8D(P5S9EeTjT_V<#K}TkA>4vi zYqnlkJ|*bXTn0Bg2p-rw;<)@QaaHwx9hZPcx2WEa0vOWZ;Z2GDed96Q?{w5#&Ti1IAI{0yt(o5bCiRXnCAAd@q;b1+o}y&? z?pm;LrW9|BtG-1M`s;G@jm!tMMC-UCF`&wDMcBmZf5Rg@uP>8zf+2b8LseUK=yt5W zIO8bs-A*625{}-M?RQ){2k!0=t~>Wat!sNeYJpV<8b#u0zsRWTQB->QJN!f`E~14I z)rjj{x^5;jgbn<0eLbC<*gmU|Hi5Q+h<6LMVPelO_!ZxV!(QKM!4~n3EBxGTBs^Zz zo_R?{c4=B@K8vbCg=J^mnaCXDd}=vJns!t?GYqCkkl6L7cP@`K#QMTjJCjuJHtqr& z;Da1UrBmH@xZ(VKaH;&BL}JJFY%_RPl*s>7b`vsL=Y0eeXGi-*h`T!SMsRgPB15fs z_-M2MS7y6uO-lpiI65r`DFv2~Bb)a&jF5?#N1%iONaLC89ZL5S2v!q-ab;&!);{5S z!G2`-eH018yjzks_sl>2X&4n1hH;;Uc4^Jm znDZ^jo-k1biJ`k|e)n#Mo~J=~RK-_bOCl_f%?&M4#K{7~*IlW+(RQsjsx*|4L#Fw5 z2Eik|QHH*zGP@IxrOEAQeT&4>GUL2U$xE&Yx+#dbHq6d`Ik?S1(j?BJ_oMoYEXtcX z1WOyW=o!&FZzU19*`-{@>1jvqxa1&gemsfqX4XR5MsxypZ2mZaZRHXEh)yp_{7l)K z*;zdQAYztdi3FTEAMoAZOIX?ZJ@59>5)(r+SIBpqb3Ptnspd<^51FUpzq~&XtS9`3 z_@laYUQ-iRpF1bMHA+9&uhC&MJK9j+-@A!CTrA3jQi4pK`PttTiT0Gf+xiQ3X9x0l zcd30nbjMJvv;xl)hWP$RpQNJs0U%Gsd8#FnE9S-)%bO01ZcRgTrgKI7S0-1o&rFxy z(%DZkf18UKCIdVQ$2=i1vBtPw9>YLgovK1Bhnh*x_#*f=?~znhJEy$&VN-bVulxG7 zX(`)fa50nxeDKqa`{CDIY=J~-Mu|RMBN70ueymK^u|x*s7en+{?Sxr-d+L7N=x4Ho z+~RN;jBWbxbToMg91eo!d@2OvJemSiCxFadMZjuLg&Lo(9>?@os~=gR+y*SuNF4Id zUAU(Gd077+0Es|$zeb`At8WAS8?L_kU2@e`*K0lyc5qWS63iHXSR0*6M>qZ)IdTXg zem~bxQ$`XoDcPDzXC@+u;d9Q+E0u}`tE3RXB_(?f6bFkTV}Wd_RZzlx7%1T^o!gUv zi4Lj|+UzwHwKKX8 z`{84f(z{d2GWJUW*p`!jwbbm{F3Y4wPx{+U0u$&aXwhzm!}Dh*0F3^KkG{Sh`SO?l zMxJ=$j|#{?c&^i|B?ZzDQhcc_So;q6<8(rG@$efHx^}^tuGy!UgslBZ3^w~9Al{Gu z13*F^m3~Z8>V6`t&R#9gJ=zQa-mlKR_iuNN$O>DHf*Ig(q==5j%u_M~z(RkZnIJmY zD#1q&iw~0AsG*z3eZ&nM0O-a06=^z=?+mWZ2`?wU$BymCQWJPipc`OW3E-7ag_r-L z%0BFw8jR_|XYvQ%=gy}w8@>{gIt&B=+GRsqWIr_0#aUSL0q^bMRzw*d+$!be^A*I{ zvI`vJsg0o~#>V$Zu_9&xj0q;rs_FQ6^yp!YF4(f=MGS6RRm(!AP%se{Gs>U}T2*Qr z_?<$XCK3+BRD*NIt9}mXZ(ijG0g7oE%mI@%lTHz^Os@4e?C*t>YJeO_rbh&NtPt^SiA!-x@2U-G@ zsR+E$+#l#zbqmRbPDU z_i8%fqH?>DUm@0rJ|yg8Nk}Y~n+F7VDnuItBRO@G zRHnR$m4)HN`pPgsOuaCl4A;xqx^+Dqd;dZO2~WfKgwOozU;o@|!Ao+Ath(e~Qnmo+ z?R*CP6#z8m2`>t@07t(MmEgz0aPWp8ruco5HTq*&cFvWu<=H zx7GwQG>vnj%1Nq?(`?^Nw;e-NY?0k8%_J=?jq?2Sk3#_7190$&+QA7#=)*vb1I;6v z{u?raz=ff&^lRWTw1dAM(E%%w4wD=H)h3v}gVj!JT{6r)@f3ZyxG6_Ox?m`>Uo-Z>qE zFvE_+>;(E3+%g9f7QC#A{7}Q5orwsFOsK)ja(2jys$^*!td!=qcxh>DlmXa`FPz_l z0&n$0T=Rtd=~S}-wP!cn@JU24%~dB&qIC`w{ZIe&w-{u0 zX+#m1hi9G)n#*#Es-<$#S(0D55+Rr?B#u!FgEasx54!gf~uI{^!$uN1iB`Vl8SF%TvW!($l4K(;;15wR{o^Qnu>W;; zH)Bk1)T=Sf9bHfg@)EQ}p$W%bVt6DZJ-y=q`*D&I;QJ8f>2XKR;1~Kjbv%th;4My$ zz;~+*lhpbeMERkM{uaDolFO#1Bd|ndPdGbU&K!bl@{q+V>t?Wp@vv0~`}l>YErfC4iP9_JhoL(n?FEf94QM0K*NHph`}nhu&Yn~Z50$3vnc zE59>DnbAI@$a~Q%?EqLK(8N&2)8Ua$qVxrMV$TrT-5@{x>3^vA`t{d;SQoxB`=Dta zc)!y<*u#hS;Aw8KBFMU z!V}ra4dC&VrlI2YWHRtVjn_>#{grB{nDgU`#V+p$BZaD$T__cc&Xug<F4y-bAkrWL)s8>;(j4eLrwzf9y3k3c9_HETnwLIt= zu#}oM0_(G{cn=ots{yaU`!kiuo5nk!i2;Zq_|3im(Md1KyrpXZ>W(U?yMNoOH+Ray zOU**iN>MY#ei?1JU=t@?duSYE3y1*$j)=-)^Vzku975Yds^y*&epeUHNmBU{ZhTa# zUHgY+|KX8|Yb{@tEORRn3286O#4j_v(Rv>Q?^SN>0;taG{sm-K65JF= z&{qtqM8HfMl!;X_Gf-#?PJ?G=DuGK%5ScVHB_V)Jnt^=CVyLRAu4ds2L^Z$qF5o08 zu<_E+4oVd&g_;U=yj3(MP|&GK0ksMw9E0y;BRw)Q&<%|YfX&>wFfk=d_NNz`002M$ zNkl1o<5Tl?9 zgEqBJKls7FB1+{6k`hPejTWFj!RKS#DYSUMi4_`69kgNil&aI;Hx=m!ihBjYR0nk z_0Y@=V)^=Nsc-0&I;=JFt~KLQ1nu~vyOHVgm8X;k?|IKBrMi0lgpUIq!Ppm!F9b10 z_M%!}A$a-R=iaAI+BA2(`z#1sSZ^3nG;=2zjlC07-nIc2M)0UV7sk2SBuvNRg5~~ za6}DvUe2rIQZ$;})H*r=UKj~VOT&)6!`hbIYzWCn66FZA?HXAKvu_~rhsdc^E{#VH z%fey@U^+V+G+K+H<}B2}Q05tc;pCM4)#!x%)($$!de#99Z;Us%Z{H5N@y5T@ni>}l zpy$nT;c7Gw5%r}*!;*<*=yMD6Br7{hf?4^_RKieBnn^Uj0uJ`3mY&QA0vBo=pl@+` zAHv%NHo6=+F3j^0@Wca+P=*@;=p+CPBw#>DVTx=xXNG!Vy6D&`6$OP@qJ4qv+fysa zNX2>`WITOt)*l!|RLoBmk^b4Az5lp}YNuswAclS%H{bkE;KNQek1;DHLw}jIG94k@ zi&kKz--_iDD7+8@7~0&s12**kQZ2|E4>u%lSiCLsY!B7|7@=6o^581G(HEm(kS4U< zv*f?cm9|HvH03GDOKrxpS|x|~9gquPRw4kSLn3ns5XimfJ%6KXzHpm;!8*DZ#2Bz+ zOZdANywEQFfpeS*v8V#c)>fV?`Neb5P5?Rh>;O6N&t{p>21cFqfnNlG$UBjYh2~Bq?IAwFvkh}2l_+Vvd3Oq-gA{{mo@R9ZC`xn=_H z%bVqs{9q0aj%OLGCD8VW6jv>ix&xckNuU0D99O6bo&fk9&ouE~H|lVSiGJ^SC=9zN zVRF^g4Ji!bx>>PIeSNKvP1NGMF_>+wE{}rN!^;$yqmMRX5(EHCQ}a*Pi+t`1r@1{) zGa~1#jxc~>TiTy+0EQZQWHK%&j8g!^B|09(&jSNp6QG6Q#Y{*pJqcG8kDM84RsjqL zDBj$2&pjr8^EcN+$Rlb4&*P#6qMr9=CP_yKb5VH(7VyfJoI;pNCA97SS3cYks_jfai@Ouvj^HK0vMRunwuefL0m)0h%-4xmF?Q~g6^W` zh1qrE;pDb#d4(PJCaN*9;;O~@;0gOHeAuvDlL8o;&FT0`fjZn8A94l6afX2)wAPdqMp0yz{H28Y4@yhhbJ~s<4|4)X z7X{HQz%6ZK$TtNygh>0s(Cj<@KlsuqsLoE`pzUFQ&n{_Wqss8L4L zxNx8*A9Apw@uF4hB(Gv6a&go^XZJ1KF9v#=CSrvMqS_vGB^SUY=Q8jIH^7LT^1sF) zTrOqtcS^#t3fZx33?QaUBexhXyJgEWShcxC-tw0BOn?^yb(3p`ksEla%6%2QRLq?( z%U3Uy^B-lSLBFEF?>^CpSrF$FSshSV0RX%WCRT10g$2{Y!gakK@*5dByq`k9gP zq3Mh~IlwyFxmAwt1DFAA39~6VIYR;ekt6%iH^w!x@R9|tMT?itgXuWceTmYiO*$fe zx$Z5P0K5hL6~i+COfYajFUdScG$Am$a-id|r02Rm6!G@V#03~ofM?j%)*^!lgJ+JZ z11&Rg;>ew2aIjNPZVv5*aNbSA>f})&u1Qq*=hCzB?_?Nx%pfvkZ+IAEx(a6aCef)k zJ2S%lfMY6G?^gGL6%tJRBd#2WoLV7G*+E~Xo}M_R9v5So?*mGR;gtE6h-Qc1{#eP9f4E#uOmPStFd^p1`u(SO9& z)3EDyzk-Ereh)#t0v+w$XgCIYYG_J73g3$asg@^Y52BpXlE+j-z=$O-8K0W!65i3V z$ubr`_0;|Ho$q{E!^Kwuyf76yeSga5Et2^w&c?uv9@2I<+JO7kC>U!*4A$+BI&jHl z+L@$_YDca+0<&Im=od1!e2%=bVH=`v##M#99%J)!&)pASm?gSIyBD+&LPm>r}Of&Bi}4W$-zIV2_S_lGc&4hXPvujYH9(-21YqJ0nP+kj4n(` z4amF&)sb(T=L#cuP~Bb7w59}b{S-_*sL7pSQlGo9_P879o&p0iu9R^I4-lVABV-9v zM)dZLOa7SzU>JD@|GcC1c)%cr;PP3=GL4R6QVe1eOj2=Sgn@#)jQgYpd!SZ&1_JuM zz6(QEc&uQ}e!%=zQ2{V)LC+S~A5*JgCFzrN|qF5>&2w(=_7fR4#iT}J#yi;jm zIL^-mMIFGHiQwgzpFIw|{Pjg{aLO+!l{M#IB57rKfV7b}+yM^ZWj;=IG84D{2G7Bq z2`pHWUAkUIOgBIb1vO*lOFk&ctq)>`7(}!ZdS005kQfYhsFNz4co~&+;J{8S*!I6Q zYKh?G`Zdn~XYSHfGH-sRq{(jBAP?d8WBhHmn`BL=!MdNSSwME(=`Q-1h>_G8xnkj-j0vPrgCh((kGehLL4hhW&nK?PKY6%kb z4dCTEU845Lo~50GW=8rysR@cTO^Ybh#Jv%s28aZ;|KNxdfZZ{qNOO&IpTS_d1R$&< zPM83UcW#n{J2&d3m=~uHT|ExGN=?m!-%Gu!sOdXpspjXxft|@0qeYGN&9ZUBR*gpF zH5V?KCySQNn*i6R+z!6W3RVDUO-ncq;6f9(5hqCLKx}ZF4}=##BoDA;l&3LnJBvhX`Q$2Al@dwJ?I8aFGysuhU#(b?@AQbOkWF$~!bd!@~or zh#-iu;?=;I8EK|~i`{9|0Sqt9^?N@3>38c5Q(bM~r2-m>)1AP7p)z!tmk2!Cv{<&%UYPg~{j)yl^d$1@jOR zJ__JQZNZsJZ4Wt(QRzoucEfBQYVkNY@l}pLBaJYP2@>=%x56DSVMca%hjZXgSfyci zrkx^y;Sv!9c_a$4Ma5t-LZz24jF;4Sygf5b6eQ|f&~yBm31E)%lLDw=bOp3aCq>lu zkPyy2z{RCN9bu)6eu<%vJf_HvG|~4gZJso3`H8L-#hK+Z``krSQu3kYNP_>1dxzFm zIzNO2s9>t+LTeKx8XOp!0542zNdUcX_d%@fl#gX7AVE$;ZlUtt~viAi97NiPUy|h4My!W^hs&F9dP|^m_EkKASPL%@VZN;Dy4U}#mO^k z;ZslD1;M>n%|(9>z|F|3;_@{ z6qJ_$U{;kQHw+bRU1L~7=_zFCtxye><;tOt(09N4RlFF(nqm1JE7{OEsVa+?f~+X3 zFDQ6MStfx>YE}qj)Bzl|f*QQp{BEFy&8FWC2~OwTotQ#_xF}{UQr_zc=b30U!tckr z_sIUO8}w}Sec}3AU?5VHmtepf!}|g)C&=Y|K{u)_)$G8!GtwUbtVfPEsOF^`*yCRw z6#3*OqPeJ!)YLlwLk%8*3EzK)>TCLiaLozL>5?se2#|*iR1;^R|ASl7BqX)RMrD3= z7!*$&F?$=&zBB+K26ixC7GxF|J7pb`jYtTJSqiq8C+pVdT^keeKBT4P$25RpY6env zCe7JV0x(SSNE)icB-jlQVgGgj7&lpB@>D%!C`tfEv$5k{q`zl!F~;Z`y}p{8u{DZu zkpPB(g@r(d#mq;hfzdyj!H=i`7y~a{Y>%b@(tFQ_W)aULn3*Lj7b7C4`PVaeYqM}F zs)HHE2m2I=Iqd-p3EO3ZiS?aQ4yFcYESi{}Bnj2(1;@MigB z*fpK_sGZ^qQ2bsg9K1#HOUk6Rp-!`m-U*HQv15CY0IyENW~rqjh^eoykp~~VOTFFS zw!(qk6l9`Yx&WXdR)~ViqTVLAiH?{PQEr(S)iIk!UWN9C8a?uulh2d?YSzE!M%w>{ zzzz|7^_y=~?aKgg;p4mAZKtH{T2;MTMY9}2||8r^lFR% zp@!%ce5Ka7v$X-HW4!B{k|6>OV$wwt!0}j!LQ9dIjwj4+&GZWU4q>?u*kdSIN1=_O zV<*7_>$8^gZ=y@>oj@y75~D6fc7bKT29BYLkfx)YoJFc_ooTbelubCzom?1~lo5=c z$xH;Pv8Gx}QfXvPtHG)r$3tGvBuPQnji1M36GoXPNt0N6wc-^Q0 z7+#F@-S7Uhs+5@Q&DNDzv0y|3-{b6|-1{=tBF1)yK4%RRVKu7#$q^7$du-0m{7Q?L*(*A&zhLaV*Pz&?Gb_~&2 zXX(I!ZMs0FEeLk!oj+1;a3*wiH8-So6JhHGb0`$FHBV6Yqm zbpj$A9J*uTI#uICkDph-iNOpJHELWzoS;&-`5oKToK=1fX6&y@%hA1hFS>f7JPQcYhwQff*R0Zafw0TdeUK*W{;f|cy&!_+xy8*6S!-W`03=@XTmf`}eE0H_1H#KW# zW2iM9KoK!@?1&RkTqB9ZkWfBcmfG4KNT=VS(Mi{>a=(`rB}-WW2G2Ju(NVVXH^{*B zc|3T4J1h=X(DzeGLvlVWajn^lIm0BWEoU;B;vV{T9=}s_7J+XHXu_aIxA6*7o1ulW>WP~EAu%zGwjk?O(<5mVh`jE*ds`3e3rgJaJ07nrCQ!H=>5d6TncmnbyU? zi^)900PX}c_vyGXbLTtZnSS-FAF0Zdso$C7gtTa0^w*VeSB}7E+Cn{PXX9SvJ8;Nl z+c9^YKmdXo#7%^kk>`LDh5AR#oj>4=HrLI)O>{g$S3w zP0FjwW#9I#s$C&~VLkz_zRhI?Hf?%bUkch;%?Dxuv^HlfVKFr6-6E~+?&YUH11S#R zBShhPB2s~iQR`#}Ec)dT%y?@mT=hGD3m0k}XrSYIQDMr>0mG>^;(kl~sN3wLo-zS) zxNj=L$V;OAfFrV%;5pJ;_nho``4RrAiMgl}=j<{VEK|XjfONCN!*2FzDfA0-z_-3V z_zTn&@CsBswVrJTJ~Ge3C9q`3m_rB5y+UmQX=1>3UnBQPP(T`PhVXLR9k5W7$fy3{ z5zT1r_PqKTbpR8b9x-KSFGpLM>k1lElteh*v(Q1GNh(=rD&{ej{z!KpQ||?FBLhVDw;o@b{UV9AQL@Jh9Q; z-CeqhEU(73eP{vTni5$VQB=)E`d0Ze_wAPYMu(gkG0+|!bm*+c2iMVYOrwvedSk|6 zQdJr9#iT};58XZESUnm}l33G;0H&fOG4gg^s_9$qg9_!dcoQ+T$vj7j^Y+L-@SqY} zI0BU(EXdc~d<6FKeA&74CFKuSwWi<*B*#fvQS1(O+SutnIIDv5+t{(&Mr~CU;-LC@vB+w3{V@L`N+-7cCGakeYx5u3tBVMkAvM-lCFa1i!vDOSt ztW5MsN>4|^%49b)9}k;Qn3`9!*Ot|K-ca(&Hazr0wXQ-)|? zjLgNuXXX1rLWbg=UMHk15+xoQtUv($34qWEgE%-lH|XK9^pTX5rlvq@(^$+g-~8Lm zO#FsdJCEQ^GjyF4)o3p!+l01bEOb0r;nFb%_J+f+wm0(LwKRgCVgF8Me9Kcw^pLymX2<`za2IZFG<}Fm0zlpRjyrHZMktFy|IkeXq zsp)J-qQhEZBJAp*mX^a>)9)h(V-yiAR~3yW4Go86`(CX2-R@8{uie=_E@!U{psf=< zGBQo#B7UVG!i_^Ch1E?n+nxv~Me3Bf`6647!55XY`rAM3KIRpbp1PQ^PM3@Qvoxv zATBh<@o}%!{7}}jctMQqv)Z+K=02m=b1d>`F@gw)YALv*n%QvXcsBK!(wAt*t37lC zD^?E+)xDdKxIr6}M&UL?)79~d4K@68Y);O-bvz!dx`B1xR_cfADl+ttVcvJflnVx6n9Y&q23scVi9!1VOAN?jfGGA=qxlO=H* zTB2yvD55$Vo5$qv(GltB;;O2y1<-5))n0L>+G#auh`0&0J*n$K@eG@4 z%`6cQf$hwT(Z08-u1CQfF%XOAJv1CATVH-fFU_QN%n8C5hXUEm3lw(!{m!IToatIg zP&JdPXLr^qJ}1`8dKKK8kf0WDZMFNG0;H9XC}H^xTHN|NYNWHRSp}7u9FvoSI-VFf zph+ec$p99gC-YuqW@c)gk&&TwdV0E=koeqVm8bzE5kN4JxBnN!I_o8W;mG zHXiALk^ZoRgD_1`wU!$}ije^p?>_r&ChLex1iO(vd$w!4Z3a`A4|E*kcNhOVQQ<_ZS#F$ z9)@%Pg=i8rFZ&OT$iBnKXMkr(KMi{S(}9jNv&__S)uPccB_$9inJ|Y^&=%yPK)_81 zTsp00tdV_>e^t8M+w@!(Ns^M12f^)B#nHUC>8_QN9ifpLkyPWkZ;?HSzT;Xr%zs1e z3yDeVQd4u(H^OG>EiGU&$f7`^7RE~L%QVC!iqx7?30P4Y;rBLmK3;^QFh`@~ftUxx zl9$mDmf(dZ8S;xI7Ks3c+B9li2wJkUv*Ey-D_L1t3TCK%VcV0KB`(Ivd_5xGOpX;w zm@KI(23TeS7sNrHxcAdQ&8xpfyk%&Jn^dqK54mS%K+3Le@_xGS+|Oes#zs5vH0y}= z97A8MTU#5oiBt)bB0ht0mx<1261Fp0m zJ4{YT>ftBov(LbpkGpY#8r>8Kh)EL8;=1s%Z)XX^%F2oy<7UO1a{BNc{dLPiit6c17a4hj^HmK2vMKTi-HAMub1T8sOzl>TK12DW79vADx z48v$@9l@Lx?Y*1~~%v3m}nisCGK`Me7Eozi=Ii|e(q;Jrvo2g0Q z>%opU4zy3uKmbGDaM}2X6IVASfJrIS0W7DmP?OqGtKk3))b!vJH8LFEq8F`(i($Eg z-o{PR)xk&FpNKVdy0O5j@umK}VW*m-oBZ!LW9qz%Rlta2O<<5GyBm|?XXcBYI zZi>i+Vd6XLoC97)#vBOh5}Y%j!YJbWp6&wd1fB?_=?**X{Q!fbSy#!%NAFSRW#$O^ z10FqzIbRr^M1cNus)X$h=cDzFjzdHtunAj%|-$BrJ*4p1`ubIHYjIjMgb`Bq6tB0U*^J) zeis0Wdk%FS{DHFys_VO@89oVIB(XnQtDBn8ByvaQ$xN8ixU4{S*kr)Am4$&Gf#qpw z#(0ubrR?*8x!@)v!y-iE8RyBK%`ZrMbA$9k`A1C_XKjgz zY2ZV(^3a5v`AJY98-SUZ@L-}5ZK4lC0wy5&MWs?&oD4vH3<4vgAXBUS+n+w27~)Eb#KhR>Ya{5|44WkXJrC?nVhUA~nF zKYMK>XB4Ca#2`5kZ|?xt24f*vc(-X{bZ4YL48Xwr&!|k&s~Lh{m1XukS=+X^X;ba< z{(dUk5eG1A3;_%?^N|cBMBD6H*XY2@(=aJhLqeyxRA^Y}!%zebENA*aq(HQ^P)xALcWT=s1=b^EiwYM>}W{r;hAYzvu4^+v~`hL70Th*lc zJAiwFJ8BZLvKD}EPWwb*Mr{IxG0h)TdJLdG(QYrPi( zS>y%SSl)HA@!_9HcSoB#i{JW!)0A?e5e$}U|1%IkO$I?9`4)?00*qQ1D|~;vCDs9q znMlIoD++85kG~if6XTqT+5ilD0@oZOrG(#ysD*d)qA+VJ4Y$A~4j%-N$qo9l+4JW9 z$=lo)35xcxFacm9J&~DUR>5_cLO&?wa~DeO{(W-cJS5VCGJ?seI7?<>GOd?u2(%z9 zp#h-kzZ_awX33>M%|^1fv}D(@l3@lU@Sp}MF|AWtoA&6c)4zUMgsbV2GBf7@>}08F zzy_PKG55gp0hmbCDnyN#!k#qLFtNI6o5L}f0RhE}-le)zle_xC_6GYoJz0%7dd24VlAuMdGO&%?j7^UU4=P5(F_Wjk=5W;=M#o180m&t(N$ew@LgUUg|?%!{FeAiTxaj zdN{FKwmkcUG#ojk3rGI=87RkKd8e6|3m+0F6Z9I-48X9Cb)rBcSeab7KbFY=Ow<-l z3~N}J?KUyqIkIySP0Iz_VseC&ZxV))=n;c1NXXjV-38S&$d=ZyMJBn=z{}*Czs)p7 z^!DBClfb~Ko4k#W%79eySs;Z=Rd6q@+6m{ev#>@{v$QlEQx)85IQVLi4iDabkc$PX1m1cwh9TOQ+77WgTP;#atuYjVNfn;H!Q4I+W zFAgT7?b6@tR4=Cbv((g7?~~{o6g8r7Ab`OdQh_9=@vs+@Z7>JU^k}U^5?tYK9gCI? zgaM2JJ(`ZZpo2n|g|lA@6w|^GtPr@^s^hHr-9QX4z(JhH^X~{?sC{@-5&d4UT)?Ks zAJ*HV11bRwDJ`u805DGhp9y30iJ<1icuL2Al9`;Nrm>|HCD%(Dta0J~tq*8BBK^_P zlO$XBVqGYN+0zvLqo*;kYUfqsp^;h)u$z7ycrncj`yaQ@1TulfwtBZ?x$VrARRH!5 zKr0^=vRo_B_%hoqwUk_sixX*PbsrmZK-wmHQOyvXjA&cX=s=Tnv^do?(;<+!Fy~a@ zl>3bM%x9V+3SijSslbcvkHiff$Do``@csj;R{otN2OpQRwx^^lqgRHPmP+?f0m6pk zk#D41I$K-dtQnFyX(6dB97QNJwIx*7g#iuBO-$pxrwN^i6LZgVppBHEhrSKLU^RqM zAs*(d>wK7M+?<%H)geVMEEE?Dl;}0bM|w5Pn_mpD*y^U1F8%+t_a;z!9_5{HX=(MY zR!iNjeX|zZvW$0w%^G4LBtU=!0tuI8LT>KBxyj8PlFXSi_hgc}_a@;c8D^3>2@@a; z-~a)$gW1An8}F8^eOGJW)oQ6*tyV9(|6f)ATcwYdELr`1YBz74Q(x7)e6>7pRXzWD z>ZzytbT(4OoQ{i+PRM@Jqy%8hu()q9Q#lFDqF5Zv$oFXX2hPXVV@Wv&Ffa~wQ{Z91 zFk{*?OQisoGiS~Wdq5157@pA|=+tK3R%}cekAq)ynp)ows*Zdk+3Lf*k^jvldN%|Jn{Z;}oA%UoJ=E+XTjoYO)0QP+9DN$c*v2;{ zL@G_(KE5!@KV=6nrR9auqj&`4FT4GsQ_pHm=x)vMC1P#)l`yI0^~(5Es6D+Y)JlnT zrp$DQS+vl)(A76XN|rUDTkWUoRF4=~k9HOHhiNT6Vcv<3xJ<+c1FZ1TK%VS4^2w{% zXTZ{KIIo5YOJGY7HP8BVu zUm%4tGX~U^Is=)~`=fuYcBtKD<#qse(PWjVkCWZk??)FIHg3e1iKP`cmnwlmt zhx4{npb?TPORKo1J&sl939Q4hTF#4=%S-DO(+%|5WE7}sTBy^mSO5&F2dF#r_a{V! zR0vm5^8(d#B#PP*PWGhs#s&xYIY1i#!}y11oOB!zL);*Sa5P=T01O4E5d#7+1cnxK z*~MSG|Et!1elec86KWSFx^bwU&Mb;scumQk-J#*kE_scvcro*HgA+nm&lK&-(e9Bm z3pI1>m0m=5XgG8*EbQ%x(isfWv76^%;h=pddi2f#)v+(ko%^EUfteW--b#tq9JK$PU;*$rr}&FQ8{FA{*sB`}Ups+6jIm4F^@P~dfF8?R}_ zQX8ES&;j7EWDT7SmA|q$F3qNq#CSQ$OGuW$(8&#A2*-ts#uDG3sxm|UPD^pu zg$F{<4J*R74eP=??i!2|EllDcI@%U`7Ibw6oD>7FSB`kw6Rg z^80!GO4F~z`vx#w+O1jguC0J4EsT7&>y=b@JK2B@lBY z+&K7?(0gWC*t30ec-Kpke#gEOePPFr$HUB-vqX*+`aHIaM4xjr2L zdP=sHI4rfJb08Xu#G}4`f6Ugl z!=Nc_$_73gC84Q^V}pZuNq^^>uwQ%+xA|boxw?94?7tq*7}Ih9V_=is9^Kzcu~Bun z)(BwO)q(iySjRxw%$8q3BAY)(-vWCVxy=heOU=RrUWiL%7dQ=+qwKjaYpl@;5l`xA z57T=0%1g?krKuUR;WB^ztzp+rH0^drg;RFTeBr5c+D3aYY~TK9m^JIo@k8=4y@WK( zlq)4w@Zn%;_&(hf<4ibadc$XH4BdlaRLn8cz}db)y84;liZL$JiV~pAsjajuX(e!{ z_)y0LsK7KVIUoe2a^JIF01Mud7wf+mz!=2%{hz7y3-!kFe-%ofr77aC#Q`rY_`NW( zza!kVXlv;0Q6JmCJG}Gm5I&)=tMdo{bLCI|dI~j>d7uQ93uRT3~pI z+F5E#v@zP1myOW30=y6+_2WLzX_t0vB!FI&sbYFV&OEZ#bK0?)mR?VvKCZI1M$lpa zU)nEVZqRV7x;>Jx<5)4M$T2Hqrp*=+z)0fJP_vyo(fkI$6jDtSd$PHSsf`ty-PKdiCJ5n(f-E<156YHQglE% zV?NUWwB(*=iK!)NiwADcK3#{rL-TB0xr6ts-?VBQ*{c!|T=?%ob^o8Jzn>0IOL<;$ z=9uR?aJoIW!sx+arIVVi;EePIeJj8VailLi13cr{IDnKlN^#Vc235Y`<{aNB|4&VQLp-Q+i9Ye# zeSF)^nzU+1U(byKT9XA-XT~K}mQ|5V@}22VW7)CU!N;t0N(xicT7mL%v86=s^)tQUra33WYi`uJVQWL!vgJuJ=b2*4Z;8vU z(50@fLCdjS(RwjE_W%V!$yC~X=f|fbK)DpK2Y+H|8Nh-L3xI`F%h|z#6=x(N3&niU znZpbNZpN%Rp{aR#*u8D5X4Adlw_h5<$0e0IqBZv8$2Us4@{&jkK#G@;th|fl#oz@d z0bn+Oq5rT%56F^fv6NXQex-;%d;W~DbJP0pniXmHUi-0L8FV}xHf}s7|8V%qS1(PY=kz{PrO z1~48g%Q4Ww;y_OXR)EUyN6Aax$L5 zpi^4}SXOFzLjo{>8|lwfNn}22&p(8_@3|>F_VBX;F(Lf=?Xt}K%wX8IZB=;o+2!%0 z=cTEbRAHF)OL8;>Ve-{0^Uw+obvYSQT)RF7&m{?rm5F%(LtyZa0*Z?ZXnr=m=X=K= zMLWuBYBifa)fu-hGJEdtPcsjac6)WmJ(vi7=guwRt6#l8{P2h0)NY$0`SZ|o-dnly zj__xH_78FP$^Y?4X~v(^$J_PSAuRdRp}wNgckb_o)`9;TZdkcAta*BUc*XMG$ly_e zfBf+W<1+KzcfTr`9Of)o5L&f{Ufny;^#Kz2QCg zszzEO;}rL8hX>*^8U47s`&9VSmp&2O{kz`vzsIH6u_}Q?AE~deGi*B~0HgWq0o7xr zR#M9DdbX!5P-UpUG0d5*GyK{mwObql;01un7(Q=yMcB7c_Z<>99uIqV9f{>)$uk|s zs;*tvHY|N%$F%(5zczMIIJ4k^jGO4R)^O1zYcfab6a@o(N*CtH8$C$ zk|=c`Jo@N2hHD3pq{G9rDk6nJi8nfyi^x1Re!~28Q?HjZ&{oV@_eDUlZ!ne9TfHd* zvq^SsA*vto9IDYSsAgMH>ZY@!>gor>aix!5(>C?n-d>>?IRh2P2^oMlZd@I{``s^# z5&t+!UXUK3KV!?wymwGKu1Ag>4FBOjykGt!aqzaphUeqXq13zkBOxsQkCI@vY4_iq zvCqsO{8Z>!abwu{?CS7FNn>I7>-P_YAOHAET1J{4Zn)t^V$@BM95y6~KFPIkGp0|H z9!r0;1ND*bC-6c!Qcu#-jhZbCrLj7J78LsLT$I2I&p1nI_WXsRb*A)ITIPhhmYV>o zKvlmbkXn|;4Kao4_78{KZmtQBKG_u7LTA(u`p1U^VmMdp@OJ6Z&5De`v)fX}K>MLh z*4Ea`rj?GckhG9Ci-fx(4n=%6NX&eDn0oS$!%ObHE^KdCA$IMEG`X(zd?^5Yk8BI! zyBhx=c;Mr48R^%5{ddMJfUm0JRCMWSNg(nj2@*Lcj?E`i8_O&Y?%vEm1GkUMu(P^8 z8m_x>W!SNCU8ohJo-|<~T(_Vq%xF$y=6MR2Dc1a|aCZOK!}d)om)uX7&&y$b*N3sX zeF9)u)-=Edz!iXr%@bkM zrf1`QK+7xCwr`r9zO@U^krkhLygxkj(6_^X`cEGSfA9x?9y|KD1~D>E5cYjkOGv+; zmXccEC^UE=EE)Jt=vjVaXqQMDeHbJc`%ZL9a`i=l7Y;&Ag!e(3C%Y;LU{-u0DfDYc z)zamw!=3|sLR({h_~T!aq-bYSUgRhkmV?k|p^b22wGSViEbFPwt!<&XWkzVmPEhNj zP&-2nf|(1pk1*u`c%Q64F8;ql!?u4PZo73}c;unZaJ^LS(2sdcbEWq7XTq{&w+w^k z96Y;Cd3>Edy(R45zboo%@C==tiny+IaqQzyDrn^L&|F<<-Ir^12-*T+6nk+NQ4Ce_u=8oT#KyF4gd^PTq}>KA}1 z^Mo5)Fx?v^)mkA`{EJ~y*P5_0JRFwJnU{3ICU=zI1c_;dLcUfVwr~1;I4j8@{q)K9 z^bo7s@O40nfeT9IcuQhRB`{8Qw8F9j7#Ea|wrJ6^@XmLBAl!fd--RD-4&l0%bOQ2I z&z=b{xxFTy0PXk3d`Khf_>qu^MzTeJm_Q7T?bPb9bZ}GHx2rBRpF14~BkIa-p8?6A zQL@8T?sU_k3oKYTDsVqya0_2=YM z5|kX=f5M+)qD>eVAPWc;AQ;TgbtV9UKajFmT3^ zvOD@Tlakgqhx%jxDyhPa;i)IKg=rJchyjy(4i2R7jpglHZL60OYlDndHcg)<;L;iz zT9;{4*N)GIEo)at zUBm~4SCOP)0JXk;W&|%@Is&+`yh(@@hWh{<>%f#IJsQ^)0Wb_YRD=q>;~l>nXRYhj zJsCdzSP1WbnfSke%%e}A2`{>F%J8~gMFy&?r65L!(2Lpg_maf?TYXh=thxUt)SmkN zu0nQ4?kDUx% zot;r~SzV*ST6!ssQ_)YUQCJ`9n+3=?{T*p1>74k9M*MxMfZCQ_zozp$|Fe+pUnIb+ z9Uc=}UK_3(d?f5?KvVf-c)ftq7oXA4vS&j$cyLeLq1D~p9W8`o-Ps~~#>oU=G*g!I zk{e4M6>%)9@Ay-3cR_RMYXpozN_l%i!;N7w9}XZW$>t>4<61oFbK%H>#qqF8Kt2Fw zzrZUylE$KgXVuoWXgP0Bl!)D@>ajGp??8W8Fjr%JIUVK*8eXJ04uub^8-~EE54u@hsMyuEnsS}}e(`iqb2IDAxntrG6YSA?3B7g&Dki^GH){w~zceSKIwzbf3Z zVlce&C3WE!UOp?_d+#EB)7!$*+wKdqR=zXTFZ#8_z)w6R&iqM9S{~3+fIv(RUR-{* z$_f}o2MV73ZcXqeg%vk1iMQ@vob;!VlCEC;BW*?D{L6%k@4X z`VUD->^+iV-e1lfeL{o0+WLnDno_a+GYvF{8jkNzx;!+d-vAxQH@o=y zVB*J5^D7=uDFsfp(Bs%5FFQcXmrn^#>U7{+=JkepjmMlg&bWo%1NsgNG}(;Hanf&^ ztF4Z0b7qClt1V8eJpgn=tW`knrGt@4aURvcJyeFoT1t8v1oGwOQZ*n z420$STiG2FllH&V2mS-~8+8nHR%X59e70%>fgOrEGfSpP6T?2KkJ-IJezTqp^||x^ z7&&%oRbu#(zC=aX*}oNL_kUEnGc!ZS(L>?2D?<2Z;k|wPHb)y;%x+5=1d~H1hHTHr z8SHVYJpHC(r`}_H?UX(ox*H%r03ft${bhY)fft4qnW3nyR-NS}u!^p?_w=wHUb-`O zexWT=hN)9$3ux_+Y7ciVQZ+Tf-oC3pEL~J)DGV}l=!&nfJ6F3j9Nx28ee&tJbcAFS zDKxsAO-)OOmyRrX@h&++?!o8b_ZD81tN0Vkm2hdg?r2mDbcXfKSG?j4IyY!ZoMq8b zKk;y4V9dG>9UTm-)}IYO{Ao{EyQw$q(c0tj6PV&2jLd(`NoS8iBMOB z=IKrGn(dT!*71mTEqt?_A%iRq^degg^$m^ja+<~#Tz|h1m7Gt>uC8XJsgRo@MkeXY zsUE~X=i>5_myIZak7}>jY!U_pbHdH~&Cpi2C#<+-WvJ7Z*Xw6jh2OXH>!Y|yQ zU37~Z!rXat;_iSuUiONx;;vVQ1uI{x<1q%qg#G_R4DkO;-ePnP6Rn#Su3Op`lfP}g z@Qpyx;9$S>9Mt%KUL5^o|Hl*Kr_M4-!1BqW)DlK&z)cPkYUiONl64too(P$=lVOlOUA&mhKfcea4{#*FwH$NLip#BGY%kfxpylYWXhGfUg z85sxJcQAFvX;UYL#)gTq%9*SXav&tj9+(+ttL+p?5Gu0e)`iZ)qXICkZ%MNaSyG%@ zw>m!c$)Q~Ma4@WPR>U!&kQKOu(@h&>CHRmA#;IwWH_EBFwjq&jT_sP+u3e-V+osUj zm6{<3W8zQ{}*s?fGs-B*<*B^Qy={qoh<2uEF5ByIpmAqG#5?WE)n0fl|L#=F_ z1lhyDuo-9J?Aj&ZG=>~ISjq}yS#m(Bckb_`cB$osn$z3D^-FYO^^uyeZ04Ep3h|8Z zGntoT?H#7fvI9z#E0&3tm2ezCa!3qV{iSSXLSnUWURBwOe%T41S|hZ6gQQH; zBt=gE#+wBac&X4>c-aL`FB-tq)HFzu`bO#NYz*I3SH+TibG;Oh*Y}2dwE1LWxfOb^ zu-sSs`r+P{;n5#{Jto7l*%zKpDc00TF}-1So%X2l;&($y`eWewWc>b_RN{ET8(Sxp1#VWljes~y|WaFyE@s=>jBCQ5YQD8`GMB>;o-5|R%GYpAP`1cqgVNs>@?tq=S6?vG{h z*&36E4plrZ@?q5!hxDx??!Z55204`J?03f9BF(dwh8vgFhud#!3%B2OeYo+CmxlQ_ zzcox-@X9c0Pz*>Xw8mW~7@#tEp|FV2k9<=IT%ZWo{BrF|stpU4To*5ITMz@yti^H0 zhyy5Y+Kz5TnVE`?;TV=!YTJ6y&ZOzJhh*d~NfKVMJT0|C7F6LL+nEToYhT~c4!$u$ zu+b>S@$V%G4{0^ODooHBIM*$i6;r-*k@VGsL6Fq7w{M7281!Jex@4tTx-iy*;pIEj zE^(Z*ghK*@34&|x7#DF`Qko#09vlK;9J(ZB*C$C?UGXo>7bXm|Ub^&7F=ic=Ai>gS zpVW}5Y8*X27B05AV6K_sradWO#5S{i`~00qel(iVV= zJzxeU7Mj-Y?Qa>vw~qXZbNUutKVO#rzs7JH%dRYdn#10!t z6yo+bog_MWLRy|-0J1x9BEX6UP)p+MyZh*7&AJl<_rN=4ind+$pVGiK5Oy9*Rxm4O#=G;UQodZMrH$8Xz*R#- zPXmu*IZnpMkj#WrPp2gVq&Amw=t>;^vh*o_FG*o!FR1PGhQL=~_Y&8!ERaegc_fA|$32HIj7`tG}bSzG+S zDEbcS(B2T~2>M3B25|z6boRmV|lQ zv}KVFfH^N4akK^Ja`|j7+GDByOPPv0oNPq_%=f?lweZ=`esmbTu0+ zvh{E_9Ne>8r`Gm_<+IO)rEUGu$Q2^db^9cJIhrIf0Gnv!OoM-&Iw=4lTD_D!3DIOz zt;{S>OIur;>(7O~YGlkP@47{(r`+yz0tjKUj=7(h08B1nL`fC&j>8i1)CNYZGCS#+ z#KS3d!ggb|dY&8-7zQ{7I_#HV@fK-cL%n_~;p%VFN8Br8S7dfBDu{XQe#(?-Y9l8# zyF72k`|vTH<)qmfgG$+*^&T`E=BNl|EZ`y}#q!c=aZ-+!!06rOx{im^G7@J3HNPS> zYpJnD1L7GiBQ0$WVU61JHFq+qwr}^buz823m~w}-rOj`UO)p6A~#ki}b zd$M#%RX8dDvqFrU2@ARh=te!fEd-RGx9mJCa|=2|uDU{PwGtnv<3~}UId0>h{^~IK zz^B7}ZNuNXe$6m|Id$ro46B`v(iZ>>acslDG+3Iy?Y4U~D{j*B&ai5C2rpX(*z3FY z0LFbqeU}4Ge%8Sz$DXqU2HL{7TUu_=Q1E7fm@jKp;9O{`6R6fOwncpgm4KU>BukCq zh2{<)l^)))G(-_=1HPq+m02tN^+&rWK4?|>cyF-JZ)`KhJ*dB?Kjgtv7`Fx?9pfa9| z%Biy?P3q9H$W-a`l-;qbBHSj#vOF|QIwU=qshZfGjOTki*{(I+`Kbix7qR23BChG~ zaPIg+p?$kXTDdiQQ-{HL)a1!na-0?c4978mAJF^<@HN&~sC?*Y;77wDg<+e$w$}#% zw+WJl`QW?7V*$WmV`A!^Va=wm57+jP2DO`(s}q&mp&)zYYeLj-*JvcPqFqTXr$3@C z>}j_a01p6zLnnr_VDy%jh|fmjm+`@P`8gu`&;Q>vlhDjU+qY*l?+RO0H(U5+xtG(r z0WNsr*o4833Q~dw_3K~wg*Sw+fBpVA_5on%Gwpl(!lG-b3*%GgC21NMI`hnvxUu}{ zEP(;QptK5uzWw%BXfoWR<6*uXzPBL(FAgQ$qe(If=C`hpZiw_VD>jZ^1p(O!%Y^07 z(Mf7YJ@M$4&pnaOuVUFYNnXx_7o>eiSpYAbjPBFXxn?;D`gr#dOK+8MWd|_Kn(;_p z_UzdaB_x>GNAhyrO?QSlcYIK`jwCWMGMC+i<`a*F=7y)jvRMtVbE^Jsw^QU`@MWq_VX+JwXo)jlR_TWCsn$5ng3wG z2>`<^w<1b*m~+R~ERe0e#xQqLTiCkp+3@1U;-{h!J7REiW|qAMP-yQ(!D+q+hJ zu1BIo3KQ)(It`7#u5N}z<-6jxY7SFmCXNBA8(t9SW~I7Z@Z0f|1YnRLHQXGUWhaO# zvn+aTod9&(uWFg=s~70K02nK_4Hw4U^kQ|^g$nPEyR}+pX(xp2PuOuX@M-N0{uc=l z-~aN|ceY9r!&cUcNab9Ra)up0P$&FfH2J9)9qtN^N1lzjH44w*0P;)(0WGBf24z?n z?OWga-UwjW!T0F)5bjwbWqs)j&ekc)WmU{hMZCzb3Xao;g8l&w&T>9BP24p@@_79DRbh)UOAI z0F0D5`v=p=W66;7+H@cp>^h=l5tg>fZvD1HFy8sw~44$zy--^6Y((Kp1_Sa<~@r&U*YI2;0jcjw3HaNWaR%}64NP>AmxrfFT zwg29}6t~`2IwNlIg15)g1@wX;dl|`6hF|%W--@(+_~CEJI_*G|s$*sW{Y*BL>^nG7 zeWxnw6e5wiHgvy`lE*p*VxJ~F)jc8do)bE#l;xted(&zLV{lW`5{<)c!{CKFTk-;M zX>DyC1~5opa@`jupZH4Ex2yoh(w7Gx{Gzt^?Ttr(V?%>Jv~cdkP~D{&N4GpqgI1XK zk|@fKGZ5`XkIIu)pzPWZrilqmV@HSVCIDc1&P)}9lYWbqW#WRJ($$fPBH(}yC)D=p zDLTudXI)g@t=!t=K&p=vKFESIo7Tat#_NoifgO6S+C7W01hXR_WP z!3Vp5IJk~MxFQYw|zN3=rrSZ|cU)qz}^t7C)B@Ko4X>#1v zJs6MR=;&0a^Te=nPuVf3`iLZSobkluku&XS+L~95c6&6`27=d2k=Yy8A6b&ZB5to{_}Y3{Q3vqC^&XaZ zt~y4Kse#}oh|%Zdw;0v zOXW=f-Ira*Q=SbCbBDo8(saL3X7)GhfJeQzFX=z9Y%pgg z_65e;aCaGW!%8Utrh0a$nkc!m9Lt{9)X`5$+_QTVfH|Q-k9G3enhHo^&=Zo*(V5QO zVZ*bV!dXdHS!(sx`pJ{4wcBHk`VUJe9r55tmXgqM`>D=RyX%(ffF$Mx*)DSWdsmwj z-1i<%-{#1WP?uSnPeM140lee@24*{J*1Yh65By2^i@*4Tu<2kjN%8hO(^xYgrgrPf z>dIO*N9u?ZKs|QkNQ@pH(rcQiF?(R(OuXkG$txT!dpLMjn+3WKgo7R8HR0)>$qyzB zO-rRPTmu$g{pvSq7fzd&?LHyEcszV*)j(J!P{0I$jU*feapd^fP%kM=>kJ*GBb~=u z?OLjry{%f^yV{+x@Dr^bM$bfEcs4E(VLOYRt{4F&6y}X?=h3twhJJ-^853#jZZ$T} z6A)?91TtCC^;T&B3frUsEJ$DSr6cYeyg&;}UZ7bezzY>B8-RhZ_-)%ZhBa%Rh{Qv2 z7Y75nm zGFI%T<&9zYsxQP{CUl-}3$v=3sN*z?K{X8{f3LaDox5CS)Hj8H74LtMW;VhRw8{=4wWPyTgysy#gi#O#v< zv+MMkaEoYFk%KYG8kxAl!6!9o`FlHzzei28-O8k+&8}QxhUqu&Io76;5ZcR zvRyo1WT;h1tjg>)Xv=3@WuaO1Jty1P42v6@0QSo@=*0Z5a zvzbeR;L$Jr)u-5dAf=&MXWu!YgV1&LX_g@c2Df$lwGDTvX6*uti_2_B1Y>Lak2i%q z2OkN?4;_f$<%>_J!4|-eSbK_=gAr51oST|n7TVh%lalw5c#hh;?~%!bS=yPj;cPr^ zV2Koa>om)KAxdNVC(Q~wfBgT14Nv?qe%YT&%+DEIxT#a;U6e_fdBAAtLk&7I7$?j6Wn(;A7UU>~o$VT*<(SZ= ze$T|EQOsA8ayd*)U{DiVL@?|I?Kp(VBc)KaF8MvNVFWlq3Sp4K$Zu=u z3s<~s~wFM%#eR{&tJI6~0N z2##m;Sx#(cQiy)hNeQ)B8p8OiL5o!xs;YEeQQdR_FqZ7=<&))H`WN7Z9YuJ63f{Yo z056QmUN+)g+8VSm4a(MrMyNNpCSTJR3ymA=g!^MVEJ3~7N_NtThhKIJMlZ@Gd2dFE%4VL5ISF4t1mqVq& zoup5_Ro91g4}VQt%~rd|MEPz+0*Wqw$>@+FeuJrcgTCWPxX5NM2x zigLF}6n?cL*!W>8^@Fl1PKw^${h)a*Giru379C6z00Xl_1YM5p{)`jHItIi3BU8fe zecFwtX(NU!zbCBC@{z$yZSAxOUSJw{#MZ5?ZwRYaeJ6Bu90>pP$hm-YfhB5;@S%K+ z;mYaL5Z6D?v8KWw1;*Mg%i-<&cj(mf^)Yws-XMj-)z;1w$e$e<1#Z*d^XD(q*;P-6ty@>?1o0!1W}eiJFs%&8A=L!Xu=^7L!=bvwvB}j~b1v?jWd|+K zsulXk1?XZ&ea`eJ=_HoQF(c9;u)^WDNKR40N9tn0g6<`KskRncH>zz+l}FD7yEW%0 zjr1)`z8QM~FE&(19N@yZT)J#Ty$zsU#!7`{0WdE7?%g}W&YfH0C89JO3QeQgIZ~=) z=(p~A^^6_iz+uWHBYG0qSz!$^INR@7Z#;2gyR_Fsz!EVBwr$*YHr#P@b!BK%Y49RS z^wzyBY+3cNmX7wuLF4NZ!qCC0s+u%lB;6MV20DjLRMTnbNN;(|ztN0)V_aiqtKU2C z4x+DY=xC4i|s`TvexxOj-MeIZbaAEepe?W%bbO>Uv`o&k(KOWnWQUJWnn6Z4AkwOo= znW|^NFcLEp_oW##TB4o?OzcZv`X5?K7zls+106fCFof6MBuRr9=q`OVY&C%$tmH`BOj8{MsZa+PBq3vJ+85Feyr29 zX$$ndC+hnhj3++xtW`|@nA!-893z`%q=F5>PHA|hn+x5l&oani~4cz;*K^Bm_QNR4Vgen<`ZY^PH1Ou;`D)Au)xo;MgX=JmAsSDM)YT|(58GseXY-3=Gtl_@n9Ulmv z{N!Khc!@J%jxg~r2q$9S0tw85xf*;Fj(I2JhPZ(f+CsiXoLUZv!&r0-lmP5E-XamS zm>b*Iua?G0Tv)HiUPk-AfpEM-ClMG47hL|!1P>WLG^f*}3E=oZyK$pJs zwzqvSJow))l4+aDyIr;`p4I8o=;h#$M>w($X4U z{_^z!{n2+3Pz6}4ofNM;MLVNN9GKD zX=5;LLcD2hdK*B}4xu|h<~)E&%Q5_K=FEBV_=jUUMYp5lh`?Q!cG338pVs%6Z5-KQ z>60Jm?~@d%Uu|JvC{&|}&S737z$Dw1$s3QPh@C+27}^BVmU_jf43~}axy;l7Ad=S; zkb+N=@2L^Yc+!y9pE{Bjpv68fSta9TLlX>EYt}p&>w=OUji1g?vBLr@Nk!*{VzKla zc5K-m&Z>c8DflVPDskuSo1{T#-n^S7oqAl`*kpowjs}v2I)rV@**Iv-Xx3WWIB{KP zr)dU1DCNvmaVGRlNs&1UB5A%aPzP#?127%QGXPM(kw0LDPWQ@J{<3B>JGEu;3$kw^ zI%#J15|NvOJQ*|rF%{Vcel_0(T>vSWZ(F}EoIceVXNhbPAFB>f@tPHsPC6wN`xy{-XSnEM^e()c84mBuKRHCAdIS~x>-(F~#H zL~TNtJP|KRsfHxRaMS>h$~>22?4xc6w3BU{04hhpAZ5cfHtI+dNul|Et`~}=eB<4_ zr)Y9_BJ7u_q`!l{4VW56agKMn#hD<$Oa(L z4O&BE%ETB1t$*{=zeC%_Xx}YVp2WXVa9g?9I|_f zHibKP&eB5zhV$*!-#P~pl*K-;7v-jGc<7EdKy#aNyW$#epbSm;zteMm&VPgtQIT7t z4qza<{e}&a7s^?Acg8e6^kuaEid4M}nap@?IJNz=I=>_>^0L;;fCNg$Fc1x%#zRMF zZC#8|?@3OYE6Qrf_ zTxjbZkOgcpV|A}u0h%e2E>Nc_0>pqCc7bruJ40BWR5oJX8n+m_U@i)%8#8~v3QH#V zs4yy!5*0;<6i9m&8mthapTL2xdpeTN1+-wP<2-Pgth&uqz2~YNNxEW&3Ii>EVs~-+qwmc=~+9Cakf$*a(w86wQQ8vY+qrox` z045*0c?+P0YDi9MB)l~F%vaVNX&9}ShQ$)ICgBmj23FSMY?j> z`#O6TgMIxG%ut8)AreqIzhnY$K`H?AP9g7^9)vhEcIW^zc1T;oLo?_JU7@$z9dxIR zFn!+PUH{){5^|4c)Q|S-`T<}#JseMawJ}25^07T11I`GDBbYhQPYq~dMTUMTzkOam z0}VpTYft&9A7yj-auDOVpHmOK>uWmZdFJJ*bbQnSj4J@K4@lnu=d(PfJzwQrCpwlx zZS5YM+!|It`p+^B*Bg}(5!2!jhgVgdACze%49YKl@$17+fBMyM`0#Go50GxhUA#RQ z)=H{y{dH5K-2m>q&>Y+QI!@I@y`B>thvV$}2OG83wf#JJK_QV&ZI~-TdCgLYOr5j5-+7V?j}r-5@AfqAZG%WhY+ukWj$%ao9qs*78M7 z%?vRW^yFX|X||lx0w!uZ9NrNE9q2D#Az@(d=#`!zN9h2_g{mk!bHuxr-_ zEpHqYEjuGkyZQ#T2>~eppfefDV{Ez-$HWauM|BNltVZHVinR2jda>drb0#iuQLY07F^w z^bH#FZ$}*sUO2-FZ)?g1F}CeP{QxqAt}Ee4j(l_P`ca?UQx?;m`r!>y+=hJ3yLi)t z`rw_9={iz*Dt)i40Ot7dG)f^|z@b$>Ufp2Jo5Qm|_+&VK_&`)nw74$pu_r=s&D(W)4s2C#R(`~Qfuv8SK@aroA{-hiRNWMgPB zJfrJH^Cru(FnSNE(_KsQ0Yr97Npk;@Ns=Bt9R?*52fW~6c(t{&Bl9+ZVMY#v-6S*l zix+4nH)In6@ja7B29Zm-xGO_r9U- zI(B)^o;@M(c`iJ>RXRsEBq~P*G>N4ZLKHo% z9|A*7k_+<9>qXyZJfxmH!@=As5A6WPZ-##<0OR`cf760{X6mL1_0PQ-HF@Jp+aTjK z-i#igJ==j^bTm4oJ2N32 z{$20-{Ww5A^URMW$><9EB$j6f3g+dR^`rBE@+J(oMZS^;*_j$KqN%b;wS4osV`dZd{uTkBbNeBDj@QUU?gfvv#kZm`V3_yOYtC|chFpyH(r zkw*9ROrhTar0^eH{IBBizq*g406hgj#a8KIVEY9Jcxi6FUP}wRG$TI{ekdu-(zdkq z7F#RqNP@OF^gCD@qO1pxVB$XBj^GR_13EsKEGHkB9Sn`-qPn_S8q{aRftEqmgD6Y_ zMn=1!9pJreL>#7VMnbz_aA)9Uuq_?5YieE^7B9Xj9%FLw;GVdpwY$4hOyp$f&~A{! zY)Kh%Q&b%#ci}P#v>x)%jQHib#LTT^IZ2GWt{Tt+7)O@?Luff@IX_#@r2w>l>Pnpr z=IASUGkp37^*7_9d}cJnxsT&L9M5tFYvu~gz05&gmU#HACHkHFAT+1X+vV{*Z{p6+ zc~U>hLcTC`_kY)ix{x0EB3T2xj09jPL++ec9+GFCKiAMB_h!~+^m$t}ZTuZ5H_uIL z($1ap<38_@w$mdGgOxm_o~H4oBrwnpK?WWdSfF5u3`gwmjZkXZNJAWnXvLzpGu~7Gcz>KQf59Iw=}O)hp3KC^xP7&J60<@?b~M z@e>;4G_El7M85$k1CAY77^*{2kB;Jj!GjkJ$^gc|jv0#w3_21X=Yf>Y{<-gvkm3RWD^AQ-6(9LAJZf$;MLkRq23WJ2msg`Cd(u+^HTyZ z(K6=eL0rg)ba2ZtguBkoo`(6i71 ztn3CE(9W5-69g%e2#M3sm5*NOTW}|VvZOScc7p&?lAm`p04pF*D+JVPPh#enK!5*X*lf633+&N~mOzrAUfe+QS- z--3{Pc)w3RxV+BkP6Cb2k9_!#+qpqXY3Omy^o7Ruqzip>dUMZp%;V0N&~GS7k@MoLAHFPK~qIc51w@-{a0U7L9Y(1Q;Sz>7urp0V~>eR&-06A!3@B(o0 z>86Osxu1gQVOE=rlv)zBMQuECPe#~KK;hXME2}iD-d%!?#^K=Fv}U36i@Ii zr4A?ncns@XC|ziuD^3DncrI0j2r8Mdo-ZBgoS&V-a4~R&o&t*1`9N1&dxws4SS{#B6G9||I8V7f}~TOa|S~Dve%az%J1U+&gCJ*n-Q2f z5W0L0%`DB}D6`9LU_trq=^M}tpMw|Y=`!$z^EmYJJ(q{PNyGUd@xXhT!=S;mF?ev_ zJ5QUK^l@4~lny?BTJlNaErR8M52yz~4VH|@E0oHB6jGMNs( zCU2KB|31F&_o9^u%OYl|3AR z1_qjTXvu^vZwb6`FxG_%VGN!C7Z1*ayhCFpyz8!4sAD&3C&d@Ee*a|n$(D46#Vzwg zxOi$Mz@2OW^Sl!6zY^B(_U0Kn%Rl)BQ+@(zB>Eq6s*3lBg1pw8Y}qXQs!g(KU} z#jUBB??z#BzU)fOYd#kZvzfipUp?lyE`*ok00e*oq#Wo#bV+amNI5Ef91f~OFNbS% zAV@Ol|FlKm0wd3N+Q2{<&}Yw{9ci;;$Lc5|nIfh_BrZr>lhUSl-;uW4 z0d3t(7{+Brme0sxY6dQ*5C6-ViN_JQ4?+fE-r-%!Pd?;9+5i|cX+kq-i}XD!&KV2s zEN^Rk2il*V%aC7F2A9Rmg8EY~GqRkz)kEdVY(|0*94an>7ayZP(N8Xgj zWip*elk}kn-k^aH@B)3HO(|W-Bc}z|&d>EHbh~nXc|8qWTrb|mL34i(11RTT%4eJ> z|06z<_c!60XtWXfS9Qo@p>Rn_17CIa4Ra&KhRg zJY(>JW(*uyBt3roxa`Ah(^lc{%2fJ~HRyIkbhG9Mqc~J{aB7q+yszG1GpkJw)SMJ~0;> zaNZM3MjQ`7XDwwU(8J1zNMNj-3BzI_<$eC=0fvs{cEupZfCjx-8)x0VqeI(3MYp3z zkIJCgX6-;(rJWT!v?Q}n@1`XgcA;Q928GP2iqB|B%Cc78M3ED^^8AABq9US55`hXp z+{&8NJt}D`c`ItNOaqWH-IKB=fUH?{VuuX|vl2MVX+k@|!>nLzot?)e_1Gq5%41@< z-C`h#kx+MbgaD#(NVxPH+uivkj09NHHE{;%^IJ(41M5o*A<1&36Z2)y&+~Wv?1d1P6~KJq3*}w;KNQ;gA7m!mOvx2pL?lXMm=R+eONlIrid@SO zgOX)zGi1v$WM9W_WUU+Fs_aXIVHjH}$u<~6kr}dPzV7FKKd<}!AHL`3^SqwtdB4x= zob#O5^PH#Xc-T+Z_6n&0*wh=z2$9J+fg8nRot~++Zol^=hG@5;e4wH zVtGkY&`thN;C>6EM~_%Pbh;%IGPr?XKYqOO&2e(d^=D`J)h-u+ONy4%t=624l@G~e zk=dK)?^suX75>cqK!$ShW)Ay~B#d__7%gAuRvN#fT94qY`fQOOPzxJA59B*_Z1bV! z(;%_%QyIE6qbeI-qn-xq7(r0%oZUFm zm9<*_>K2qQ?0Sit;qr5L^#!?}a7j@a1Sky)gnj!|d))0P3siIv*M-}#v+F$;R9`K9 zLGUWSL?r_G|44z*4qXPFL9pnY{Sb0DW7ggoY z3lw?jSToLN9uIoinc#R4bFLi3y{WS>yxlMaa-IJP6p31hSB1}Mm>X0#-n2jZjv{vU zMf3h<4IgO$tr>`A%)~{AwHXRtK9xh~x?AE5_X3g8!u|t3uH3(oymeUNBI* z+ovtSI*7V0zK5J>qYqP=DLX75e9mqd!=KTbT8Of0lg`v&-#pCUFPqqHR*c%Igi7i1 zpDNf{wcmbtCYBln3EN42_u^}$Pr%*-`3h*J{X0?740YMEn|{cYe)LN79VvdIQk)}? zL5Xv}fiv;97;gA!gV?7H%PNvS=nFg_^*NN(z9GM5X>(~@v0m?$`z@(V@SF9g7YdOm zu3BOOKmoU29-azM$-8F>R`)hTjkX?Thi|rjKB~>lbYv~L)}Qx_6uNkx!=mlafD3UP zK@xy79^WbB7ChA@mI2nc1K>_FydxDx20;wLb_I~+QG9oSx9EDk%o7QaI?_<`Q!8id zylPr^*_$_}l;h$AXWImhryQS5h7)*yIJsv;@FC_KMYBNeZjKH%x81Jog_`wAG9pgO zhI>6KxE%9E{&^=yM8v6+LiyTXo?O9Y&h+--rV5ZVk&IB1t_ZPa>I8Q*Yr)W5XbQ#sl0uj_B!(}&c<_h)nh9hr zN4CvXR!`01v#r@!Qr&FhQV6r;mjxK`8(Il;HzSUzh6HDgv_A=nPHj9%Z%_R&yi zrK+%f45t_;=WV?RP^xeA?!H<2UMfh+gCIulUKb#<(SPzaWtIk*= zrh63{6k;Uk1)~qyitR*4FYEK%MzvK3xS z`{enQ!J4!g@-TYl2^ojUz*jA_AaGd-8etvi*`G|$9U7GUIF7KR;Hv#J)>59V-!&4x z5-(*uXCTR?#dkKzAm>?e^`7&@Gb3KoYnwBU_ewWwU~Y+N7y3znG&ra1NMZB}*iiRH zx5}5|DzOJT-B?|_5SxpY)t(lj>216&Dy#@F=I$^j$qAs}r?QO){cws|PNkqG|{A~Q7OXb+x042Xgf z+6&z0vA3-Hqg2DCqT(zuhai@YUWZ0`@!PcAk=)t_lc@4*5=|bZt93}qfZ^_s!%(2I zElayz$B83+Lsfbqx9;6H`E-VEYk|Sx<(=f!n^W!^)vR(c=&OO7P1W}kG7fecz3A*f ztzQBMtpaz&_Ov%Ic*>f6jXRYzE^FdNq)sdt=MOLX+&aP$0v^FMDsvTq#RGhIkb>WQ?VERxcYX5CFdtv;pfQh;9o zdsfj;KeP46lCNzlYSFmA$p0kCM<`gog{U)tZ{;D5%yOukTWO-tZx0jpVTx`&VOlA9 z;3(c>_$D)~^ z@*?uIZT0y?)Yt^P#g$2(ZF!VS#S{?6zk*RHb7{K$S+2tU`|zTFjyz#tP-|3LvERAF zQ8J4dA85kIp79T`I~Z(S%8B4xr^%8Q-9&jCCh%kV0rL;tEZcZGo}L zEgHxtR442Azw;%uQMKlG!{TT+x^S$EVfp3hbLy{(Xv7!o?BM6ay9>p%1F8>!cw_ZJ z(VcV2MFeI0N8;C4*(EQy^oyrHIr8$qp@^NNm7p7aXiYVtzu*?YgIXD$y!mGR9qEst z#eRI+5P#l5mP~QA^B0wM8m-eQH)F@U)`F+2?0D1y$8*l|!Kzri?SQh}vpUT#s9+KB z&$Tk$=m-_Ym6{Ji4t51u#v*=$^@Ax2?m7!ehkO!1u(Z?cQiw<-2sV}&G3FO}uTxB+ zju3 zpC@TL8L_ITRhjIs>UK6`WGiqN+l2JQET`0MB%}o6bHppAeugHq8y;B*@A*MTo_S=% z2j*+LTR%&i`0CTv93i-_u1Hj=P;u9GaNS#w^67snO`&Q2PG(h)NqPp>*7=0VtnVdm zQnn3lgm-#ESN+c?0d@S1xLNMh}=aS>qOhQXKc(XUm1oQ4v+3&K2&dYPkWtB_blHamdpZm&J01_7o zO4CMviWip&7Zy?$brbdW?RgY)tOQso(qT>Q5+vl`N;T6)<4G{-=l~dS*;Vp>nOWrN z1LCl6BY0or~ zTDEP*(RjN`2l?+ND7X8PI%1FllS|8sY(3RcmDau)#5qs(#mxva+)8>d+g}EW-)s&L7 zAG^97gq@k0-fgs9xON6FqFBxB-NYJVlTJ-)DDI2N^|)UXDI;Gh z%WVKz;BS)1cz#OahZ9D?yz}>N>%G~}Z$RG5oC4oib#$tUKAn%n1<*0>NbBSS^zu*g z12L{%F?873d{#?bvkBM^sMkMNmyOIj+3ZpMXGFrQ!3I;*O3uUwJTZ%IF_AHn#LUdf zX*ozlH(}mH)n9wtn{S30FTceAtZA}Z`o2sfu*-Yur2=YZeMk7Z&-TIDDF4|nqB18Y zH>w8~f}@~C`fG~_W(b|Z+}D~3oLtav%gDr9LfWc2&r(e;RlIMmCg`tiOcf94whawP zZ5s?AP4$RlJ}6)N(9qh4Gws&ax%Q0LY^$L5&B=AU0dIvYhRtY6U$Dn5^f-r!y%z8y z5So3OD_>Nu;NAA>>X@jQ;bj{eOwVV#AzN#3=QbnCHS21Nm|m=L_|k#LjV|;RT-P{& z4<-T}Vr}>p1u8l}E+x<25arpBc&z1}=guXI9>)QqThBwM=;ABXumTD9FX?BK9s? zYNiu;P1+7tdk#!e6%xV(lFWsIi7SGj|?#W-fWX`YXx^1tp})<^s!9k#qUp<^DKm1`|7e9Rs4b}eJ&+FoI8xq7aJzrJZ>_bRvxFnmZwdvZX$c%tdKmGfx z7U{&252P;YCk)*0ar2%48gL%=px+ac?L9@s6WA#+V}q5Peu)JX^xI)S{Y$Lv6wS)bX~(reEnY+%UAW0xC4J`kb$?s})2*SE1vSTC6%qyV8M zuvU%Q^K4*1LwLX{<2k8=s=;kw7pHomGUu370;lZ5@`ulL+YVAAPaB`Y6|v_WJEBT6 zgDZ+4rAhS26B`y(G(`*2k7>emV}`RX0!*+)T*6%9aKBL*EjrJmSchs~apv$=jBvMr zIrhwMp?>>y^~ST$#h?!*n!x3z5=%Pow@r;lVIkxJ|3Ed1!xFq=Mc$bIuSRGhaheqd z6<*qs46R*FEj+3`K($$&X_6@0ezmlRWwu)H;qgoyW4{caVNYyxustm#CueRxdqd9r zE$dtW6*$(*cTcrO>-^9|-YTWV@E)K$znzezH3qlUomuI0?BBP?YRB2u0w zx?06viY#<=;Hiug^UTMz#7hUo_6?dc;LFENBjck(lLLKG7D{rLhFk&+?P4u^Q}rft zTtg=q-Ny){el2NI{ztWQD%2b6nf6E5-u$Mlrqs0!vrtqr@sJ(u_v^9yrrwMDw?01M zya`%JLl}#XZy_N|6;s|n?o(NovGjMU$$!)`XN1pD&xPmv3F)W~DR~3=H_YPa&A_6-J=g_Ujq4{`~#- zi<<)jezp6%8q)7sL8v0S^skBrD{~S_M>paLC$U`T8i4;A%(qV;K55~d#w9$x!8!;T z?cBF7>@~bIl*7M~+9F!38>_E#!(Frey3L^?UPF-}ok@SNmJ?H&+RJ)4Mt7nXU1F#9 z4{6O%X=Ve|h9O$0_Qx&@8#_E^MJ+H4k(0Y%;--wWTG}COYE&Wx| z6xH5lJ;l(R@NEyg+|`mWQ73k7`X8ykR~yMzr~xCGZG6W~#$0){)ws+@g$e}%paJX$ z%!`MA{MXFC1#FSrJu=~q)+!gdop?#uT1E4z|KrI0NX`il9>(tNc7QOI@Zf#e-*){+ z1F2)DM5nI)%HhhL@ydU=`$xY>uaF#oqITGD?Zf|>j4RXOK=-T?FR$GBS9^~VH!lF5 zgz~)g`oBKQ$-(U9%>mgPdoJ-o|B&<_OnpouI6D$($$#9g`d3+k6|WEg(-Qjmnf<@M zqs1&q02pnDCVc18zl_i_Il03{Upc&~wE7p||6iv5U~_eSz}I*=hs*%tJQkw GC;tQNV!%HD diff --git a/docs/_themes/LICENSE b/docs/_themes/LICENSE deleted file mode 100644 index 8daab7ee..00000000 --- a/docs/_themes/LICENSE +++ /dev/null @@ -1,37 +0,0 @@ -Copyright (c) 2010 by Armin Ronacher. - -Some rights reserved. - -Redistribution and use in source and binary forms of the theme, with or -without modification, are permitted provided that the following conditions -are met: - -* Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - -* The names of the contributors may not be used to endorse or - promote products derived from this software without specific - prior written permission. - -We kindly ask you to only use these themes in an unmodified manner just -for Flask and Flask-related products, not for unrelated projects. If you -like the visual style and want to use it for your own projects, please -consider making some larger changes to the themes (such as changing -font faces, sizes, colors or margins). - -THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/_themes/README b/docs/_themes/README deleted file mode 100644 index b3292bdf..00000000 --- a/docs/_themes/README +++ /dev/null @@ -1,31 +0,0 @@ -Flask Sphinx Styles -=================== - -This repository contains sphinx styles for Flask and Flask related -projects. To use this style in your Sphinx documentation, follow -this guide: - -1. put this folder as _themes into your docs folder. Alternatively - you can also use git submodules to check out the contents there. -2. add this to your conf.py: - - sys.path.append(os.path.abspath('_themes')) - html_theme_path = ['_themes'] - html_theme = 'flask' - -The following themes exist: - -- 'flask' - the standard flask documentation theme for large - projects -- 'flask_small' - small one-page theme. Intended to be used by - very small addon libraries for flask. - -The following options exist for the flask_small theme: - - [options] - index_logo = '' filename of a picture in _static - to be used as replacement for the - h1 in the index.rst file. - index_logo_height = 120px height of the index logo - github_fork = '' repository name on github for the - "fork me" badge diff --git a/docs/_themes/flask/layout.html b/docs/_themes/flask/layout.html deleted file mode 100644 index 19c43fbb..00000000 --- a/docs/_themes/flask/layout.html +++ /dev/null @@ -1,24 +0,0 @@ -{%- extends "basic/layout.html" %} -{%- block extrahead %} - {{ super() }} - {% if theme_touch_icon %} - - {% endif %} - -{% endblock %} -{%- block relbar2 %}{% endblock %} -{% block header %} - {{ super() }} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} -{%- block footer %} - - {% if pagename == 'index' %} -
- {% endif %} -{%- endblock %} diff --git a/docs/_themes/flask/relations.html b/docs/_themes/flask/relations.html deleted file mode 100644 index 3bbcde85..00000000 --- a/docs/_themes/flask/relations.html +++ /dev/null @@ -1,19 +0,0 @@ -

Related Topics

- diff --git a/docs/_themes/flask/static/flasky.css_t b/docs/_themes/flask/static/flasky.css_t deleted file mode 100644 index 5906e751..00000000 --- a/docs/_themes/flask/static/flasky.css_t +++ /dev/null @@ -1,577 +0,0 @@ -/* - * flasky.css_t - * ~~~~~~~~~~~~ - * - * :copyright: Copyright 2010 by Armin Ronacher. - * :license: Flask Design License, see LICENSE for details. - */ - -{% set page_width = '940px' %} -{% set sidebar_width = '220px' %} - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'Georgia', serif; - font-size: 17px; - background-color: white; - color: #000; - margin: 0; - padding: 0; -} - -div.document { - width: {{ page_width }}; - margin: 30px auto 0 auto; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 {{ sidebar_width }}; -} - -div.sphinxsidebar { - width: {{ sidebar_width }}; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 0 30px; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - width: {{ page_width }}; - margin: 20px auto 30px auto; - font-size: 14px; - color: #888; - text-align: right; -} - -div.footer a { - color: #888; -} - -div.related { - display: none; -} - -div.sphinxsidebar a { - color: #444; - text-decoration: none; - border-bottom: 1px dotted #999; -} - -div.sphinxsidebar a:hover { - border-bottom: 1px solid #999; -} - -div.sphinxsidebar { - font-size: 14px; - line-height: 1.5; -} - -div.sphinxsidebarwrapper { - padding: 18px 10px; -} - -div.sphinxsidebarwrapper p.logo { - padding: 0 0 20px 0; - margin: 0; - text-align: center; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - font-family: 'Garamond', 'Georgia', serif; - color: #444; - font-size: 24px; - font-weight: normal; - margin: 0 0 5px 0; - padding: 0; -} - -div.sphinxsidebar h4 { - font-size: 20px; -} - -div.sphinxsidebar h3 a { - color: #444; -} - -div.sphinxsidebar p.logo a, -div.sphinxsidebar h3 a, -div.sphinxsidebar p.logo a:hover, -div.sphinxsidebar h3 a:hover { - border: none; -} - -div.sphinxsidebar p { - color: #555; - margin: 10px 0; -} - -div.sphinxsidebar ul { - margin: 10px 0; - padding: 0; - color: #000; -} - -div.sphinxsidebar input { - border: 1px solid #ccc; - font-family: 'Georgia', serif; - font-size: 1em; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #004B6B; - text-decoration: underline; -} - -a:hover { - color: #6D4100; - text-decoration: underline; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; -} - -{% if theme_index_logo %} -div.indexwrapper h1 { - text-indent: -999999px; - background: url({{ theme_index_logo }}) no-repeat center center; - height: {{ theme_index_logo_height }}; -} -{% endif %} -div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #ddd; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -div.admonition { - background: #fafafa; - margin: 20px -30px; - padding: 10px 30px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition tt.xref, div.admonition a tt { - border-bottom: 1px solid #fafafa; -} - -dd div.admonition { - margin-left: -60px; - padding-left: 60px; -} - -div.admonition p.admonition-title { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition p.last { - margin-bottom: 0; -} - -div.highlight { - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.9em; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; - background: #fdfdfd; - font-size: 0.9em; -} - -table.footnote + table.footnote { - margin-top: -15px; - border-top: none; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td.label { - width: 0px; - padding: 0.3em 0 0.3em 0.5em; -} - -table.footnote td { - padding: 0.3em 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -blockquote { - margin: 0 0 0 30px; - padding: 0; -} - -ul, ol { - margin: 10px 0 10px 30px; - padding: 0; -} - -pre { - background: #eee; - padding: 7px 30px; - margin: 15px -30px; - line-height: 1.3em; -} - -dl pre, blockquote pre, li pre { - margin-left: -60px; - padding-left: 60px; -} - -dl dl pre { - margin-left: -90px; - padding-left: 90px; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #FBFBFB; - border-bottom: 1px solid white; -} - -a.reference { - text-decoration: none; - border-bottom: 1px dotted #004B6B; -} - -a.reference:hover { - border-bottom: 1px solid #6D4100; -} - -a.footnote-reference { - text-decoration: none; - font-size: 0.7em; - vertical-align: top; - border-bottom: 1px dotted #004B6B; -} - -a.footnote-reference:hover { - border-bottom: 1px solid #6D4100; -} - -a:hover tt { - background: #EEE; -} - - -@media screen and (max-width: 870px) { - - div.sphinxsidebar { - display: none; - } - - div.document { - width: 100%; - - } - - div.documentwrapper { - margin-left: 0; - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - } - - div.bodywrapper { - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - margin-left: 0; - } - - ul { - margin-left: 0; - } - - .document { - width: auto; - } - - .footer { - width: auto; - } - - .bodywrapper { - margin: 0; - } - - .footer { - width: auto; - } - - .github { - display: none; - } - - - -} - - - -@media screen and (max-width: 875px) { - - body { - margin: 0; - padding: 20px 30px; - } - - div.documentwrapper { - float: none; - background: white; - } - - div.sphinxsidebar { - display: block; - float: none; - width: 102.5%; - margin: 50px -30px -20px -30px; - padding: 10px 20px; - background: #333; - color: white; - } - - div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, - div.sphinxsidebar h3 a { - color: white; - } - - div.sphinxsidebar a { - color: #aaa; - } - - div.sphinxsidebar p.logo { - display: none; - } - - div.document { - width: 100%; - margin: 0; - } - - div.related { - display: block; - margin: 0; - padding: 10px 0 20px 0; - } - - div.related ul, - div.related ul li { - margin: 0; - padding: 0; - } - - div.footer { - display: none; - } - - div.bodywrapper { - margin: 0; - } - - div.body { - min-height: 0; - padding: 0; - } - - .rtd_doc_footer { - display: none; - } - - .document { - width: auto; - } - - .footer { - width: auto; - } - - .footer { - width: auto; - } - - .github { - display: none; - } -} - - -/* scrollbars */ - -::-webkit-scrollbar { - width: 6px; - height: 6px; -} - -::-webkit-scrollbar-button:start:decrement, -::-webkit-scrollbar-button:end:increment { - display: block; - height: 10px; -} - -::-webkit-scrollbar-button:vertical:increment { - background-color: #fff; -} - -::-webkit-scrollbar-track-piece { - background-color: #eee; - -webkit-border-radius: 3px; -} - -::-webkit-scrollbar-thumb:vertical { - height: 50px; - background-color: #ccc; - -webkit-border-radius: 3px; -} - -::-webkit-scrollbar-thumb:horizontal { - width: 50px; - background-color: #ccc; - -webkit-border-radius: 3px; -} - -/* misc. */ - -.revsys-inline { - display: none!important; -} \ No newline at end of file diff --git a/docs/_themes/flask/theme.conf b/docs/_themes/flask/theme.conf deleted file mode 100644 index 18c720f8..00000000 --- a/docs/_themes/flask/theme.conf +++ /dev/null @@ -1,9 +0,0 @@ -[theme] -inherit = basic -stylesheet = flasky.css -pygments_style = flask_theme_support.FlaskyStyle - -[options] -index_logo = '' -index_logo_height = 120px -touch_icon = diff --git a/docs/_themes/flask_small/layout.html b/docs/_themes/flask_small/layout.html deleted file mode 100644 index aa1716aa..00000000 --- a/docs/_themes/flask_small/layout.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "basic/layout.html" %} -{% block header %} - {{ super() }} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} -{% block footer %} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} -{# do not display relbars #} -{% block relbar1 %}{% endblock %} -{% block relbar2 %} - {% if theme_github_fork %} - Fork me on GitHub - {% endif %} -{% endblock %} -{% block sidebar1 %}{% endblock %} -{% block sidebar2 %}{% endblock %} diff --git a/docs/_themes/flask_small/static/flasky.css_t b/docs/_themes/flask_small/static/flasky.css_t deleted file mode 100644 index 802ebaff..00000000 --- a/docs/_themes/flask_small/static/flasky.css_t +++ /dev/null @@ -1,291 +0,0 @@ -/* - * flasky.css_t - * ~~~~~~~~~~~~ - * - * Sphinx stylesheet -- flasky theme based on nature theme. - * - * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'Georgia', serif; - font-size: 17px; - color: #000; - background: white; - margin: 0; - padding: 0; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 40px auto 0 auto; - width: 700px; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 30px 30px; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - text-align: right; - color: #888; - padding: 10px; - font-size: 14px; - width: 650px; - margin: 0 auto 40px auto; -} - -div.footer a { - color: #888; - text-decoration: underline; -} - -div.related { - line-height: 32px; - color: #888; -} - -div.related ul { - padding: 0 0 0 10px; -} - -div.related a { - color: #444; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #004B6B; - text-decoration: underline; -} - -a:hover { - color: #6D4100; - text-decoration: underline; -} - -div.body { - padding-bottom: 40px; /* saved for footer */ -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; -} - -{% if theme_index_logo %} -div.indexwrapper h1 { - text-indent: -999999px; - background: url({{ theme_index_logo }}) no-repeat center center; - height: {{ theme_index_logo_height }}; -} -{% endif %} - -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: white; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -div.admonition { - background: #fafafa; - margin: 20px -30px; - padding: 10px 30px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition p.admonition-title { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition p.last { - margin-bottom: 0; -} - -div.highlight{ - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.85em; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td { - padding: 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -pre { - padding: 0; - margin: 15px -30px; - padding: 8px; - line-height: 1.3em; - padding: 7px 30px; - background: #eee; - border-radius: 2px; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; -} - -dl pre { - margin-left: -60px; - padding-left: 60px; -} - -dl.class { - margin-bottom: 50px; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #FBFBFB; -} - -a:hover tt { - background: #EEE; -} diff --git a/docs/_themes/flask_small/theme.conf b/docs/_themes/flask_small/theme.conf deleted file mode 100644 index 542b4625..00000000 --- a/docs/_themes/flask_small/theme.conf +++ /dev/null @@ -1,10 +0,0 @@ -[theme] -inherit = basic -stylesheet = flasky.css -nosidebar = true -pygments_style = flask_theme_support.FlaskyStyle - -[options] -index_logo = '' -index_logo_height = 120px -github_fork = '' diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py deleted file mode 100644 index 33f47449..00000000 --- a/docs/_themes/flask_theme_support.py +++ /dev/null @@ -1,86 +0,0 @@ -# flasky extensions. flasky pygments style based on tango style -from pygments.style import Style -from pygments.token import Keyword, Name, Comment, String, Error, \ - Number, Operator, Generic, Whitespace, Punctuation, Other, Literal - - -class FlaskyStyle(Style): - background_color = "#f8f8f8" - default_style = "" - - styles = { - # No corresponding class for the following: - #Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#000000", # class 'x' - - Comment: "italic #8f5902", # class: 'c' - Comment.Preproc: "noitalic", # class: 'cp' - - Keyword: "bold #004461", # class: 'k' - Keyword.Constant: "bold #004461", # class: 'kc' - Keyword.Declaration: "bold #004461", # class: 'kd' - Keyword.Namespace: "bold #004461", # class: 'kn' - Keyword.Pseudo: "bold #004461", # class: 'kp' - Keyword.Reserved: "bold #004461", # class: 'kr' - Keyword.Type: "bold #004461", # class: 'kt' - - Operator: "#582800", # class: 'o' - Operator.Word: "bold #004461", # class: 'ow' - like keywords - - Punctuation: "bold #000000", # class: 'p' - - # because special names such as Name.Class, Name.Function, etc. - # are not recognized as such later in the parsing, we choose them - # to look the same as ordinary variables. - Name: "#000000", # class: 'n' - Name.Attribute: "#c4a000", # class: 'na' - to be revised - Name.Builtin: "#004461", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#000000", # class: 'nc' - to be revised - Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#888", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "bold #cc0000", # class: 'ne' - Name.Function: "#000000", # class: 'nf' - Name.Property: "#000000", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#000000", # class: 'nn' - to be revised - Name.Other: "#000000", # class: 'nx' - Name.Tag: "bold #004461", # class: 'nt' - like a keyword - Name.Variable: "#000000", # class: 'nv' - to be revised - Name.Variable.Class: "#000000", # class: 'vc' - to be revised - Name.Variable.Global: "#000000", # class: 'vg' - to be revised - Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - - Number: "#990000", # class: 'm' - - Literal: "#000000", # class: 'l' - Literal.Date: "#000000", # class: 'ld' - - String: "#4e9a06", # class: 's' - String.Backtick: "#4e9a06", # class: 'sb' - String.Char: "#4e9a06", # class: 'sc' - String.Doc: "italic #8f5902", # class: 'sd' - like a comment - String.Double: "#4e9a06", # class: 's2' - String.Escape: "#4e9a06", # class: 'se' - String.Heredoc: "#4e9a06", # class: 'sh' - String.Interpol: "#4e9a06", # class: 'si' - String.Other: "#4e9a06", # class: 'sx' - String.Regex: "#4e9a06", # class: 'sr' - String.Single: "#4e9a06", # class: 's1' - String.Symbol: "#4e9a06", # class: 'ss' - - Generic: "#000000", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #000000", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "bold #000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #000000", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' - } diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 00000000..5b4f1bb1 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,88 @@ +API Reference +============= + +.. toctree:: + :maxdepth: 3 + +.. module:: socketio + +``Server`` class +---------------- + +.. autoclass:: Server + :members: + +``AsyncServer`` class +--------------------- + +.. autoclass:: AsyncServer + :members: + :inherited-members: + +``WSGIApp`` class +----------------- + +.. autoclass:: WSGIApp + :members: + +``ASGIApp`` class +----------------- + +.. autoclass:: ASGIApp + :members: + +``Middleware`` class (deprecated) +--------------------------------- + +.. autoclass:: Middleware + :members: + +``Namespace`` class +------------------- + +.. autoclass:: Namespace + :members: + +``AsyncNamespace`` class +------------------------ + +.. autoclass:: AsyncNamespace + :members: + :inherited-members: + +``BaseManager`` class +--------------------- + +.. autoclass:: BaseManager + :members: + +``PubSubManager`` class +----------------------- + +.. autoclass:: PubSubManager + :members: + +``KombuManager`` class +---------------------- + +.. autoclass:: KombuManager + :members: + +``RedisManager`` class +---------------------- + +.. autoclass:: RedisManager + :members: + +``AsyncManager`` class +---------------------- + +.. autoclass:: AsyncManager + :members: + :inherited-members: + +``AsyncRedisManager`` class +--------------------------- + +.. autoclass:: AsyncRedisManager + :members: diff --git a/docs/conf.py b/docs/conf.py index a1f6a136..e8398c3d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,31 +1,39 @@ # -*- coding: utf-8 -*- # -# socketio documentation build configuration file, created by -# sphinx-quickstart on Sat Jun 13 23:41:23 2015. +# Configuration file for the Sphinx documentation builder. # -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config -import sys -import os -import shlex +# -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) -sys.path.append(os.path.abspath('_themes')) +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'python-socketio' +copyright = '2018, Miguel Grinberg' +author = 'Miguel Grinberg' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '' + -# -- General configuration ------------------------------------------------ +# -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -39,29 +47,13 @@ # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: +# # source_suffix = ['.rst', '.md'] source_suffix = '.rst' -# The encoding of source files. -#source_encoding = 'utf-8-sig' - # The master toctree document. master_doc = 'index' -# General information about the project. -project = u'socketio' -copyright = u'2015, Miguel Grinberg' -author = u'Miguel Grinberg' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '0.1' -# The full version, including alpha/beta/rc tags. -release = '0.1' - # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # @@ -69,222 +61,125 @@ # Usually you set "language" from the command line for these cases. language = None -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = None -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- +# -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'flask_small' #'alabaster' +# +html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. +# html_theme_options = { - 'index_logo': 'logo.png', - 'github_fork': 'miguelgrinberg/python-socketio' -} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_themes'] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None + 'github_user': 'miguelgrinberg', + 'github_repo': 'python-socketio', + 'github_banner': True, + 'github_button': True, + 'github_type': 'star', + 'fixed_sidebar': True, -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} -# A dictionary with options for the search language support, empty by default. -# Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'socketiodoc' +htmlhelp_basename = 'python-socketiodoc' -# -- Options for LaTeX output --------------------------------------------- + +# -- Options for LaTeX output ------------------------------------------------ latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'socketio.tex', u'socketio Documentation', - u'Miguel Grinberg', 'manual'), + (master_doc, 'python-socketio.tex', 'python-socketio Documentation', + 'Miguel Grinberg', 'manual'), ] -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - -# -- Options for manual page output --------------------------------------- +# -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'socketio', u'socketio Documentation', + (master_doc, 'python-socketio', 'python-socketio Documentation', [author], 1) ] -# If true, show URL addresses after external links. -#man_show_urls = False - -# -- Options for Texinfo output ------------------------------------------- +# -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'socketio', u'socketio Documentation', - author, 'socketio', 'One line description of project.', - 'Miscellaneous'), + (master_doc, 'python-socketio', 'python-socketio Documentation', + author, 'python-socketio', 'One line description of project.', + 'Miscellaneous'), ] -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] -# If false, no module index is generated. -#texinfo_domain_indices = True +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# -- Extension configuration ------------------------------------------------- diff --git a/docs/deployment.rst b/docs/deployment.rst new file mode 100644 index 00000000..7de16445 --- /dev/null +++ b/docs/deployment.rst @@ -0,0 +1,277 @@ +Deployment +========== + +The following sections describe a variety of deployment strategies for +Socket.IO servers. + +aiohttp +------- + +`Aiohttp `_ is a framework with support for HTTP +and WebSocket, based on asyncio. Support for this framework is limited to Python +3.5 and newer. + +Instances of class ``socketio.AsyncServer`` will automatically use aiohttp +for asynchronous operations if the library is installed. To request its use +explicitly, the ``async_mode`` option can be given in the constructor:: + + sio = socketio.AsyncServer(async_mode='aiohttp') + +A server configured for aiohttp must be attached to an existing application:: + + app = web.Application() + sio.attach(app) + +The aiohttp application can define regular routes that will coexist with the +Socket.IO server. A typical pattern is to add routes that serve a client +application and any associated static files. + +The aiohttp application is then executed in the usual manner:: + + if __name__ == '__main__': + web.run_app(app) + +Tornado +------- + +`Tornado `_ is a web framework with support +for HTTP and WebSocket. Support for this framework requires Python 3.5 and +newer. Only Tornado version 5 and newer are supported, thanks to its tight +integration with asyncio. + +Instances of class ``socketio.AsyncServer`` will automatically use tornado +for asynchronous operations if the library is installed. To request its use +explicitly, the ``async_mode`` option can be given in the constructor:: + + sio = socketio.AsyncServer(async_mode='tornado') + +A server configured for tornado must include a request handler for +Engine.IO:: + + app = tornado.web.Application( + [ + (r"/socketio.io/", socketio.get_tornado_handler(sio)), + ], + # ... other application options + ) + +The tornado application can define other routes that will coexist with the +Socket.IO server. A typical pattern is to add routes that serve a client +application and any associated static files. + +The tornado application is then executed in the usual manner:: + + app.listen(port) + tornado.ioloop.IOLoop.current().start() + +Sanic +----- + +`Sanic `_ is a very efficient asynchronous web +server for Python 3.5 and newer. + +Instances of class ``socketio.AsyncServer`` will automatically use Sanic for +asynchronous operations if the framework is installed. To request its use +explicitly, the ``async_mode`` option can be given in the constructor:: + + sio = socketio.AsyncServer(async_mode='sanic') + +A server configured for aiohttp must be attached to an existing application:: + + app = web.Application() + sio.attach(app) + +The Sanic application can define regular routes that will coexist with the +Socket.IO server. A typical pattern is to add routes that serve a client +application and any associated static files. + +The Sanic application is then executed in the usual manner:: + + if __name__ == '__main__': + app.run() + +Uvicorn, Daphne, and other ASGI servers +--------------------------------------- + +The ``socketio.ASGIApp`` class is an ASGI compatible application that can +forward Socket.IO traffic to an ``socketio.AsyncServer`` instance:: + + sio = socketio.AsyncServer(async_mode='asgi') + app = socketio.ASGIApp(sio) + +The application can then be deployed with any ASGI compatible web server. + +Eventlet +-------- + +`Eventlet `_ is a high performance concurrent networking +library for Python 2 and 3 that uses coroutines, enabling code to be written in +the same style used with the blocking standard library functions. An Socket.IO +server deployed with eventlet has access to the long-polling and WebSocket +transports. + +Instances of class ``socketio.Server`` will automatically use eventlet for +asynchronous operations if the library is installed. To request its use +explicitly, the ``async_mode`` option can be given in the constructor:: + + sio = socketio.Server(async_mode='eventlet') + +A server configured for eventlet is deployed as a regular WSGI application, +using the provided ``socketio.Middleware``:: + + app = socketio.Middleware(sio) + import eventlet + eventlet.wsgi.server(eventlet.listen(('', 8000)), app) + +Using Gunicorn with Eventlet +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An alternative to running the eventlet WSGI server as above is to use +`gunicorn `_, a fully featured pure Python web server. The +command to launch the application under gunicorn is shown below:: + + $ gunicorn -k eventlet -w 1 module:app + +Due to limitations in its load balancing algorithm, gunicorn can only be used +with one worker process, so the ``-w`` option cannot be set to a value higher +than 1. A single eventlet worker can handle a large number of concurrent +clients, each handled by a greenlet. + +Eventlet provides a ``monkey_patch()`` function that replaces all the blocking +functions in the standard library with equivalent asynchronous versions. While +python-socketio does not require monkey patching, other libraries such as +database drivers are likely to require it. + +Gevent +------ + +`Gevent `_ is another asynchronous framework based on +coroutines, very similar to eventlet. An Socket.IO server deployed with +gevent has access to the long-polling transport. If project +`gevent-websocket `_ is +installed, the WebSocket transport is also available. + +Instances of class ``socketio.Server`` will automatically use gevent for +asynchronous operations if the library is installed and eventlet is not +installed. To request gevent to be selected explicitly, the ``async_mode`` +option can be given in the constructor:: + + sio = socketio.Server(async_mode='gevent') + +A server configured for gevent is deployed as a regular WSGI application, +using the provided ``socketio.Middleware``:: + + app = socketio.Middleware(sio) + from gevent import pywsgi + pywsgi.WSGIServer(('', 8000), app).serve_forever() + +If the WebSocket transport is installed, then the server must be started as +follows:: + + from gevent import pywsgi + from geventwebsocket.handler import WebSocketHandler + app = socketio.Middleware(sio) + pywsgi.WSGIServer(('', 8000), app, + handler_class=WebSocketHandler).serve_forever() + +Using Gunicorn with Gevent +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An alternative to running the gevent WSGI server as above is to use +`gunicorn `_, a fully featured pure Python web server. The +command to launch the application under gunicorn is shown below:: + + $ gunicorn -k gevent -w 1 module:app + +Or to include WebSocket:: + + $ gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 module: app + +Same as with eventlet, due to limitations in its load balancing algorithm, +gunicorn can only be used with one worker process, so the ``-w`` option cannot +be higher than 1. A single gevent worker can handle a large number of +concurrent clients through the use of greenlets. + +Gevent provides a ``monkey_patch()`` function that replaces all the blocking +functions in the standard library with equivalent asynchronous versions. While +python-socketio does not require monkey patching, other libraries such as +database drivers are likely to require it. + +uWSGI +----- + +When using the uWSGI server in combination with gevent, the Socket.IO server +can take advantage of uWSGI's native WebSocket support. + +Instances of class ``socketio.Server`` will automatically use this option for +asynchronous operations if both gevent and uWSGI are installed and eventlet is +not installed. To request this asynchronous mode explicitly, the +``async_mode`` option can be given in the constructor:: + + # gevent with uWSGI + sio = socketio.Server(async_mode='gevent_uwsgi') + +A complete explanation of the configuration and usage of the uWSGI server is +beyond the scope of this documentation. The uWSGI server is a fairly complex +package that provides a large and comprehensive set of options. It must be +compiled with WebSocket and SSL support for the WebSocket transport to be +available. As way of an introduction, the following command starts a uWSGI +server for the ``latency.py`` example on port 5000:: + + $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app + +Standard Threads +---------------- + +While not comparable to eventlet and gevent in terms of performance, +the Socket.IO server can also be configured to work with multi-threaded web +servers that use standard Python threads. This is an ideal setup to use with +development servers such as `Werkzeug `_. Only the +long-polling transport is currently available when using standard threads. + +Instances of class ``socketio.Server`` will automatically use the threading +mode if neither eventlet nor gevent are not installed. To request the +threading mode explicitly, the ``async_mode`` option can be given in the +constructor:: + + sio = socketio.Server(async_mode='threading') + +A server configured for threading is deployed as a regular web application, +using any WSGI complaint multi-threaded server. The example below deploys an +Socket.IO application combined with a Flask web application, using Flask's +development web server based on Werkzeug:: + + sio = socketio.Server(async_mode='threading') + app = Flask(__name__) + app.wsgi_app = socketio.Middleware(sio, app.wsgi_app) + + # ... Socket.IO and Flask handler functions ... + + if __name__ == '__main__': + app.run(threaded=True) + +When using the threading mode, it is important to ensure that the WSGI server +can handle multiple concurrent requests using threads, since a client can have +up to two outstanding requests at any given time. The Werkzeug server is +single-threaded by default, so the ``threaded=True`` option is required. + +Note that servers that use worker processes instead of threads, such as +gunicorn, do not support a Socket.IO server configured in threading mode. + +Scalability Notes +----------------- + +Socket.IO is a stateful protocol, which makes horizontal scaling more +difficult. To deploy a cluster of Socket.IO processes hosted on one or +multiple servers, the following conditions must be met: + +- Each Socket.IO process must be able to handle multiple requests + concurrently. This is required because long-polling clients send two + requests in parallel. Worker processes that can only handle one request at a + time are not supported. +- The load balancer must be configured to always forward requests from a + client to the same worker process. Load balancers call this *sticky + sessions*, or *session affinity*. +- The worker processes need to communicate with each other to coordinate + complex operations such as broadcasts. This is done through a configured + message queue. See the section on using message queues for details. diff --git a/docs/guide.rst b/docs/guide.rst new file mode 100644 index 00000000..27f5d4e9 --- /dev/null +++ b/docs/guide.rst @@ -0,0 +1,346 @@ +User's Guide +============ + +The ``Server`` and ``AsyncServer`` classes +------------------------------------------ + +A Socket.IO server is an instance of class :class:`socketio.Server`. This +instance can be transformed into a standard WSGI application by wrapping it +with the :class:`socketio.WSGIApp` class:: + + import socketio + + # create a Socket.IO server + sio = socketio.Server() + + # wrap with a WSGI application + app = socketio.WSGIApp(sio) + +For asyncio based servers, the :class:`socketio.AsyncServer` class provides +the same functionality, but in a coroutine friendly format. If desired, The +:class:`socketio.ASGIApp` class can transform the server into a standard +ASGI application:: + + # create a Socket.IO server + sio = socketio.AsyncServer() + + # wrap with ASGI application + app = socketio.ASGIApp(sio) + +The WSGI and ASGI application wrappers support serving static files, which is +a convenient way to deliver JavaScript based Socket.IO clients to the web +browser:: + + app = socketio.ASGIApp(sio, static_files={ + '/': {'content_type': 'text/html', 'filename': 'latency.html'}, + '/static/style.css': {'content_type': 'text/css', + 'filename': 'static/style.css'}, + }) + +The dictionary provided with the ``static_files`` argument has static file +endpoints as keys. For each of these endpoints, a dictionary with the file's +content type and local filename is given. + +These wrappers can also act as middlewares, forwarding any traffic that is not +intended to Socket.IO server to another application. This allows Socket.IO +servers to integrate easily into existing WSGI or ASGI applications:: + + from wsgi import app # a Flask, Django, etc. application + + app = socketio.WSGIApp(sio, app) + +Receiving Events +---------------- + +The Socket.IO protocol is event based. When a client wants to communicate with +the server it *emits* an event. Each event has a name, and a list of +arguments. The server registers event handler functions with the +:func:`socketio.Server.on` decorator:: + + @sio.on('my custom event') + def my_custom_event(sid, data): + pass + +For asyncio servers, event handlers can optionally be given as coroutines:: + + @sio.on('my custom event') + async def my_custom_event(sid, data): + pass + +The ``sid`` argument is the Socket.IO session id, a unique identifier of each +client connection. All the events sent by a given client will have the same +``sid`` value. + +The ``connect`` and ``disconnect`` are special; they are invoked automatically +when a client connects or disconnects from the server:: + + @sio.on('connect') + def connect(sid, environ): + print('connect ', sid) + + @sio.on('disconnect') + def disconnect(sid): + print('disconnect ', sid) + +The ``connect`` event is an ideal place to perform user authentication, and +any necessary mapping between user entities in the application and the ``sid`` +that was assigned to the client. The ``environ`` argument is a dictionary in +standard WSGI format containing the request information, including HTTP +headers. After inspecting the request, the connect event handler can return +``False`` to reject the connection with the client. + +Sending Events +-------------- + +Socket.IO is a bidirectional protocol, so at any time the server can send an +event to its connected clients. The :func:`socketio.Server.emit` method is +used for this task:: + + sio.emit('my event', {'data': 'foobar'}) + +Sometimes the server may want to send an event just to a particular client. +This can be achieved by adding a ``room`` argument to the emit call:: + + sio.emit('my event', {'data': 'foobar'}, room=user_sid) + +The :func:`socketio.Server.emit` method takes an event name, a message payload +of type ``str``, ``bytes``, ``list``, ``dict`` or ``tuple``, and the recipient +room. When sending a ``tuple``, the elements in it need to be of any of the +other four allowed types. The elements of the tuple will be passed as multiple +arguments to the client-side event handler function. The ``room`` argument is +used to identify the client that should receive the event, and is set to the +``sid`` value assigned to that client's connection with the server. When +omitted, the event is broadcasted to all connected clients. + +Rooms +----- + +To make it easy for the server to emit events to groups of related clients, +the application can put its clients into "rooms", and then address messages to +these rooms. + +In the previous section the ``room`` argument of the +:func:`socketio.SocketIO.emit` method was used to designate a specific +client as the recipient of the event. This is because upon connection, a +personal room for each client is created and named with the ``sid`` assigned +to the connection. The application is then free to create additional rooms and +manage which clients are in them using the :func:`socketio.Server.enter_room` +and :func:`socketio.Server.leave_room` methods. Clients can be in as many +rooms as needed and can be moved between rooms as often as necessary. + +:: + + @sio.on('chat') + def begin_chat(sid): + sio.enter_room(sid, 'chat_users') + + @sio.on('exit_chat') + def exit_chat(sid): + sio.leave_room(sid, 'chat_users') + +In chat applications it is often desired that an event is broadcasted to all +the members of the room except one, which is the originator of the event such +as a chat message. The :func:`socketio.Server.emit` method provides an +optional ``skip_sid`` argument to indicate a client that should be skipped +during the broadcast. + +:: + + @sio.on('my message') + def message(sid, data): + sio.emit('my reply', data, room='chat_users', skip_sid=sid) + +Event Callbacks +--------------- + +When a client sends an event to the server, it can optionally provide a +callback function, to be invoked as a way of acknowledgment that the server +has processed the event. While this is entirely managed by the client, the +server can provide a list of values that are to be passed on to the callback +function, simply by returning them from the handler function:: + + @sio.on('my event', namespace='/chat') + def my_event_handler(sid, data): + # handle the message + return "OK", 123 + +Likewise, the server can request a callback function to be invoked after a +client has processed an event. The :func:`socketio.Server.emit` method has an +optional ``callback`` argument that can be set to a callable. If this +argument is given, the callable will be invoked after the client has processed +the event, and any values returned by the client will be passed as arguments +to this function. Using callback functions when broadcasting to multiple +clients is not recommended, as the callback function will be invoked once for +each client that received the message. + +Namespaces +---------- + +The Socket.IO protocol supports multiple logical connections, all multiplexed +on the same physical connection. Clients can open multiple connections by +specifying a different *namespace* on each. A namespace is given by the client +as a pathname following the hostname and port. For example, connecting to +*http://example.com:8000/chat* would open a connection to the namespace +*/chat*. + +Each namespace is handled independently from the others, with separate session +IDs (``sid``\ s), event handlers and rooms. It is important that applications +that use multiple namespaces specify the correct namespace when setting up +their event handlers and rooms, using the optional ``namespace`` argument +available in all the methods in the :class:`socketio.Server` class:: + + @sio.on('my custom event', namespace='/chat') + def my_custom_event(sid, data): + pass + +When emitting an event, the ``namespace`` optional argument is used to specify +which namespace to send it on. When the ``namespace`` argument is omitted, the +default Socket.IO namespace, which is named ``/``, is used. + +Class-Based Namespaces +---------------------- + +As an alternative to the decorator-based event handlers, the event handlers +that belong to a namespace can be created as methods of a subclass of +:class:`socketio.Namespace`:: + + class MyCustomNamespace(socketio.Namespace): + def on_connect(self, sid, environ): + pass + + def on_disconnect(self, sid): + pass + + def on_my_event(self, sid, data): + self.emit('my_response', data) + + sio.register_namespace(MyCustomNamespace('/test')) + +For asyncio based severs, namespaces must inherit from +:class:`socketio.AsyncNamespace`, and can define event handlers as coroutines +if desired:: + + class MyCustomNamespace(socketio.AsyncNamespace): + def on_connect(self, sid, environ): + pass + + def on_disconnect(self, sid): + pass + + async def on_my_event(self, sid, data): + await self.emit('my_response', data) + + sio.register_namespace(MyCustomNamespace('/test')) + +When class-based namespaces are used, any events received by the server are +dispatched to a method named as the event name with the ``on_`` prefix. For +example, event ``my_event`` will be handled by a method named ``on_my_event``. +If an event is received for which there is no corresponding method defined in +the namespace class, then the event is ignored. All event names used in +class-based namespaces must use characters that are legal in method names. + +As a convenience to methods defined in a class-based namespace, the namespace +instance includes versions of several of the methods in the +:class:`socketio.Server` and :class:`socketio.AsyncServer` classes that default +to the proper namespace when the ``namespace`` argument is not given. + +In the case that an event has a handler in a class-based namespace, and also a +decorator-based function handler, only the standalone function handler is +invoked. + +It is important to note that class-based namespaces are singletons. This means +that a single instance of a namespace class is used for all clients, and +consequently, a namespace instance cannot be used to store client specific +information. + +Using a Message Queue +--------------------- + +When working with distributed applications, it is often necessary to access +the functionality of the Socket.IO from multiple processes. There are two +specific use cases: + +- Applications that use a work queues such as + `Celery `_ may need to emit an event to a + client once a background job completes. The most convenient place to carry + out this task is the worker process that handled this job. + +- Highly available applications may want to use horizontal scaling of the + Socket.IO server to be able to handle very large number of concurrent + clients. + +As a solution to the above problems, the Socket.IO server can be configured +to connect to a message queue such as `Redis `_ or +`RabbitMQ `_, to communicate with other related +Socket.IO servers or auxiliary workers. + +Redis +~~~~~ + +To use a Redis message queue, a Python Redis client must be installed:: + + # socketio.Server class + pip install redis + + # socketio.AsyncServer class + pip install aioredis + +The Redis queue is configured through the :class:`socketio.RedisManager` and +:class:`socketio.AsyncRedisManager` classes. These classes connect directly to +the Redis store and use the queue's pub/sub functionality:: + + # socketio.Server class + mgr = socketio.RedisManager('redis://') + sio = socketio.Server(client_manager=mgr) + + # socketio.AsyncServer class + mgr = socketio.AsyncRedisManager('redis://') + sio = socketio.AsyncServer(client_manager=mgr) + +The ``client_manager`` argument instructs the server to connect to the given +message queue, and to coordinate with other processes connected to the queue. + +Kombu +~~~~~ + +`Kombu `_ is a Python package that +provides access to RabbitMQ and many other message queues. It can be installed +with pip:: + + pip install kombu + +To use RabbitMQ or other AMQP protocol compatible queues, that is the only +required dependency. But for other message queues, Kombu may require +additional packages. For example, to use a Redis queue via Kombu, the Python +package for Redis needs to be installed as well:: + + pip install redis + +The queue is configured through the :class:`socketio.KombuManager`:: + + mgr = socketio.KombuManager('amqp://') + sio = socketio.Server(client_manager=mgr) + +The connection URL passed to the :class:`KombuManager` constructor is passed +directly to Kombu's `Connection object +`_, so +the Kombu documentation should be consulted for information on how to build +the correct URL for a given message queue. + +Note that Kombu currently does not support asyncio, so it cannot be used with +the :class:`socketio.AsyncServer` class. + +Emitting from external processes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To have a process other than a server connect to the queue to emit a message, +the same client manager classes can be used as standalone objects. In this +case, the ``write_only`` argument should be set to ``True`` to disable the +creation of a listening thread, which only makes sense in a server. For +example:: + + # connect to the redis queue as an external process + external_sio = socketio.RedisManager('redis://', write_only=True) + + # emit an event + external_sio.emit('my event', data={'foo': 'bar'}, room='my room') diff --git a/docs/index.rst b/docs/index.rst index 1a26d7a7..b267c5cb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,756 +1,25 @@ -.. socketio documentation master file, created by - sphinx-quickstart on Sat Jun 13 23:41:23 2015. +.. python-socketio documentation master file, created by + sphinx-quickstart on Sun Nov 25 11:52:38 2018. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -socketio documentation -====================== +python-socketio +=============== -This project implements a Python Socket.IO server that can run standalone or -integrated with a web application. The following are some of its -features: +This projects implements a Socket.IO server that can run standalone or +integrated with a variety of Python web frameworks. -- Fully compatible with the - `Javascript `_, - `Swift `_, - `C++ `_ and - `Java `_ official - Socket.IO clients, plus any third party clients that comply with the - Socket.IO specification. -- Compatible with Python 2.7 and Python 3.3+. -- Supports large number of clients even on modest hardware when used with an - asynchronous server based on `asyncio `_ - (`sanic `_, `aiohttp `_ or - `tornado `_), - `eventlet `_ or `gevent `_. For - development and testing, any WSGI compliant multi-threaded server can also be - used. -- Includes a WSGI middleware that integrates Socket.IO traffic with standard - WSGI applications. -- Broadcasting of messages to all connected clients, or to subsets of them - assigned to "rooms". -- Optional support for multiple servers, connected through a messaging queue - such as Redis or RabbitMQ. -- Send messages to clients from external processes, such as Celery workers or - auxiliary scripts. -- Event-based architecture implemented with decorators that hides the details - of the protocol. -- Support for HTTP long-polling and WebSocket transports. -- Support for XHR2 and XHR browsers. -- Support for text and binary messages. -- Support for gzip and deflate HTTP compression. -- Configurable CORS responses, to avoid cross-origin problems with browsers. +.. toctree:: + :maxdepth: 2 -What is Socket.IO? ------------------- - -Socket.IO is a transport protocol that enables real-time bidirectional -event-based communication between clients (typically web browsers) and a -server. The original implementations of the client and server components are -written in JavaScript. - -Getting Started ---------------- - -The Socket.IO server can be installed with pip:: - - pip install python-socketio - -The following is a basic example of a Socket.IO server that uses the -`aiohttp `_ framework for asyncio (Python 3.5+ -only): - -.. code:: python - - from aiohttp import web - import socketio - - sio = socketio.AsyncServer() - app = web.Application() - sio.attach(app) - - async def index(request): - """Serve the client-side application.""" - with open('index.html') as f: - return web.Response(text=f.read(), content_type='text/html') - - @sio.on('connect', namespace='/chat') - def connect(sid, environ): - print("connect ", sid) - - @sio.on('chat message', namespace='/chat') - async def message(sid, data): - print("message ", data) - await sio.emit('reply', room=sid) - - @sio.on('disconnect', namespace='/chat') - def disconnect(sid): - print('disconnect ', sid) - - app.router.add_static('/static', 'static') - app.router.add_get('/', index) - - if __name__ == '__main__': - web.run_app(app) - -And below is a similar example, but using Flask and Eventlet. This example is -compatible with Python 2.7 and 3.3+:: - - import socketio - import eventlet - from flask import Flask, render_template - - sio = socketio.Server() - app = Flask(__name__) - - @app.route('/') - def index(): - """Serve the client-side application.""" - return render_template('index.html') - - @sio.on('connect') - def connect(sid, environ): - print('connect ', sid) - - @sio.on('my message') - def message(sid, data): - print('message ', data) - - @sio.on('disconnect') - def disconnect(sid): - print('disconnect ', sid) - - if __name__ == '__main__': - # wrap Flask application with socketio's middleware - app = socketio.Middleware(sio, app) - - # deploy as an eventlet WSGI server - eventlet.wsgi.server(eventlet.listen(('', 8000)), app) - -The client-side application must include the -`socket.io-client `_ library -(versions 1.3.5 or newer recommended). - -Each time a client connects to the server the ``connect`` event handler is -invoked with the ``sid`` (session ID) assigned to the connection and the WSGI -environment dictionary. The server can inspect authentication or other headers -to decide if the client is allowed to connect. To reject a client the handler -must return ``False``. - -When the client sends an event to the server, the appropriate event handler is -invoked with the ``sid`` and the message, which can be a single or multiple -arguments. The application can define as many events as needed and associate -them with event handlers. An event is defined simply by a name. - -When a connection with a client is broken, the ``disconnect`` event is called, -allowing the application to perform cleanup. - -Server ------- - -Socket.IO servers are instances of class :class:`socketio.Server`, which can be -combined with a WSGI compliant application using :class:`socketio.Middleware`:: - - # create a Socket.IO server - sio = socketio.Server() - - # wrap WSGI application with socketio's middleware - app = socketio.Middleware(sio, app) - - -For asyncio based servers, the :class:`socketio.AsyncServer` class provides a -coroutine friendly server:: - - # create a Socket.IO server - sio = socketio.AsyncServer() - - # attach server to application - sio.attach(app) - -Event handlers for servers are registered using the :func:`socketio.Server.on` -method:: - - @sio.on('my custom event') - def my_custom_event(): - pass - -For asyncio servers, event handlers can be regular functions or coroutines:: - - @sio.on('my custom event') - async def my_custom_event(): - await sio.emit('my reply') - -Rooms ------ - -Because Socket.IO is a bidirectional protocol, the server can send messages to -any connected client at any time. To make it easy to address groups of clients, -the application can put clients into rooms, and then address messages to the -entire room. - -When clients first connect, they are assigned to their own rooms, named with -the session ID (the ``sid`` argument passed to all event handlers). The -application is free to create additional rooms and manage which clients are in -them using the :func:`socketio.Server.enter_room` and -:func:`socketio.Server.leave_room` methods. Clients can be in as many rooms as -needed and can be moved between rooms as often as necessary. The individual -rooms assigned to clients when they connect are not special in any way, the -application is free to add or remove clients from them, though once it does -that it will lose the ability to address individual clients. - -:: - - @sio.on('enter room') - def enter_room(sid, data): - sio.enter_room(sid, data['room']) - - @sio.on('leave room') - def leave_room(sid, data): - sio.leave_room(sid, data['room']) - -The :func:`socketio.Server.emit` method takes an event name, a message payload -of type ``str``, ``bytes``, ``list``, ``dict`` or ``tuple``, and the recipient -room. When sending a ``tuple``, the elements in it need to be of any of the -other four allowed types. The elements of the tuple will be passed as multiple -arguments to the client-side callback function. To address an individual -client, the ``sid`` of that client should be given as room (assuming the -application did not alter these initial rooms). To address all connected -clients, the ``room`` argument should be omitted. - -:: - - @sio.on('my message') - def message(sid, data): - print('message ', data) - sio.emit('my reply', data, room='my room') - -Often when broadcasting a message to group of users in a room, it is desirable -that the sender does not receive its own message. The -:func:`socketio.Server.emit` method provides an optional ``skip_sid`` argument -to specify a client that should be skipped during the broadcast. - -:: - - @sio.on('my message') - def message(sid, data): - print('message ', data) - sio.emit('my reply', data, room='my room', skip_sid=sid) - -Responses ---------- - -When a client sends an event to the server, it can optionally provide a -callback function, to be invoked with a response provided by the server. The -server can provide a response simply by returning it from the corresponding -event handler. - -:: - - @sio.on('my event', namespace='/chat') - def my_event_handler(sid, data): - # handle the message - return "OK", 123 - -The event handler can return a single value, or a tuple with several values. -The callback function on the client side will be invoked with these returned -values as arguments. - -Callbacks ---------- - -The server can also request a response to an event sent to a client. The -:func:`socketio.Server.emit` method has an optional ``callback`` argument that -can be set to a callable. When this argument is given, the callable will be -invoked with the arguments returned by the client as a response. - -Using callback functions when broadcasting to multiple clients is not -recommended, as the callback function will be invoked once for each client -that received the message. - -Namespaces ----------- - -The Socket.IO protocol supports multiple logical connections, all multiplexed -on the same physical connection. Clients can open multiple connections by -specifying a different *namespace* on each. A namespace is given by the client -as a pathname following the hostname and port. For example, connecting to -*http://example.com:8000/chat* would open a connection to the namespace -*/chat*. - -Each namespace is handled independently from the others, with separate session -IDs (``sid``\ s), event handlers and rooms. It is important that applications -that use multiple namespaces specify the correct namespace when setting up -their event handlers and rooms, using the optional ``namespace`` argument -available in all the methods in the :class:`socketio.Server` class. - -When the ``namespace`` argument is omitted, set to ``None`` or to ``'/'``, a -default namespace is used. - -Class-Based Namespaces ----------------------- - -As an alternative to the decorator-based event handlers, the event handlers -that belong to a namespace can be created as methods of a subclass of -:class:`socketio.Namespace`:: - - class MyCustomNamespace(socketio.Namespace): - def on_connect(self, sid, environ): - pass - - def on_disconnect(self, sid): - pass - - def on_my_event(self, sid, data): - self.emit('my_response', data) - - sio.register_namespace(MyCustomNamespace('/test')) - -For asyncio based severs, namespaces must inherit from -:class:`socketio.AsyncNamespace`, and can define event handlers as regular -methods or coroutines:: - - class MyCustomNamespace(socketio.AsyncNamespace): - def on_connect(self, sid, environ): - pass - - def on_disconnect(self, sid): - pass - - async def on_my_event(self, sid, data): - await self.emit('my_response', data) - - sio.register_namespace(MyCustomNamespace('/test')) - -When class-based namespaces are used, any events received by the server are -dispatched to a method named as the event name with the ``on_`` prefix. For -example, event ``my_event`` will be handled by a method named ``on_my_event``. -If an event is received for which there is no corresponding method defined in -the namespace class, then the event is ignored. All event names used in -class-based namespaces must used characters that are legal in method names. - -As a convenience to methods defined in a class-based namespace, the namespace -instance includes versions of several of the methods in the -:class:`socketio.Server` and :class:`socketio.AsyncServer` classes that default -to the proper namespace when the ``namespace`` argument is not given. - -In the case that an event has a handler in a class-based namespace, and also a -decorator-based function handler, only the standalone function handler is -invoked. - -It is important to note that class-based namespaces are singletons. This means -that a single instance of a namespace class is used for all clients, and -consequently, a namespace instance cannot be used to store client specific -information. - -Using a Message Queue ---------------------- - -The Socket.IO server owns the socket connections to all the clients, so it is -the only process that can emit events to them. Unfortunately this becomes a -limitation for many applications that use more than one process. A common need -is to emit events to clients from a process other than the server, for example -a `Celery `_ worker. - -To enable these auxiliary processes to emit events, the server can be -configured to listen for externally issued events on a message queue such as -`Redis `_ or `RabbitMQ `_. -Processes that need to emit events to client then post these events to the -queue. - -Another situation in which the use of a message queue is necessary is with -high traffic applications that work with large number of clients. To support -these clients, it may be necessary to horizontally scale the Socket.IO -server by splitting the client list among multiple server processes. In this -type of installation, each server processes owns the connections to a subset -of the clients. To make broadcasting work in this environment, the servers -communicate with each other through the message queue. - -Kombu -~~~~~ - -One of the messaging options offered by this package to access the message -queue is `Kombu `_ , which means that -any message queue supported by this package can be used. Kombu can be installed -with pip:: - - pip install kombu - -To use RabbitMQ or other AMQP protocol compatible queues, that is the only -required dependency. But for other message queues, Kombu may require -additional packages. For example, to use a Redis queue, Kombu needs the Python -package for Redis installed as well:: - - pip install redis - -The appropriate message queue service, such as RabbitMQ or Redis, must also be -installed. To configure a Socket.IO server to connect to a Kombu queue, the -``client_manager`` argument must be passed in the server creation. The -following example instructs the server to connect to a Redis service running -on the same host and on the default port:: - - mgr = socketio.KombuManager('redis://') - sio = socketio.Server(client_manager=mgr) - -For a RabbitMQ queue also running on the local server with default -credentials, the configuration is as follows:: - - mgr = socketio.KombuManager('amqp://') - sio = socketio.Server(client_manager=mgr) - -The URL passed to the :class:`KombuManager` constructor is passed directly to -Kombu's `Connection object -`_, so -the Kombu documentation should be consulted for information on how to -connect to the message queue appropriately. - -Note that Kombu currently does not support asyncio, so it cannot be used with -the :class:`socketio.AsyncServer` class. - -Redis -~~~~~ + intro + guide + deployment + api -To use a Redis message queue, the Python package for Redis must also be -installed:: - - # WSGI server - pip install redis - - # asyncio server - pip install aioredis - -Native Redis support is accessed through the :class:`socketio.RedisManager` and -:class:`socketio.AsyncRedisManager` classes. These classes connect directly to -the Redis store and use the queue's pub/sub functionality:: - - # WSGI server - mgr = socketio.RedisManager('redis://') - sio = socketio.Server(client_manager=mgr) - - # asyncio server - mgr = socketio.AsyncRedisManager('redis://') - sio = socketio.AsyncServer(client_manager=mgr) - -Horizontal scaling -~~~~~~~~~~~~~~~~~~ - -If multiple Socket.IO servers are connected to the same message queue, they -automatically communicate with each other and manage a combined client list, -without any need for additional configuration. When a load balancer such as -nginx is used, this provides virtually unlimited scaling capabilities for the -server. - -Emitting from external processes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To have a process other than a server connect to the queue to emit a message, -the same client manager classes can be used as standalone objects. In this -case, the ``write_only`` argument should be set to ``True`` to disable the -creation of a listening thread, which only makes sense in a server. For -example:: - - # connect to the redis queue through Kombu - external_sio = socketio.KombuManager('redis://', write_only=True) - - # emit an event - external_sio.emit('my event', data={'foo': 'bar'}, room='my room') - -Deployment ----------- - -The following sections describe a variety of deployment strategies for -Socket.IO servers. - -Sanic -~~~~~ - -`Sanic `_ is a very efficient asynchronous web -server for Python 3.5 and newer. - -Instances of class ``socketio.AsyncServer`` will automatically use Sanic for -asynchronous operations if the framework is installed. To request its use -explicitly, the ``async_mode`` option can be given in the constructor:: - - sio = socketio.AsyncServer(async_mode='sanic') - -A server configured for aiohttp must be attached to an existing application:: - - app = web.Application() - sio.attach(app) - -The Sanic application can define regular routes that will coexist with the -Socket.IO server. A typical pattern is to add routes that serve a client -application and any associated static files. - -The Sanic application is then executed in the usual manner:: - - if __name__ == '__main__': - app.run() - -aiohttp -~~~~~~~ - -`Aiohttp `_ is a framework with support for HTTP -and WebSocket, based on asyncio. Support for this framework is limited to Python -3.5 and newer. - -Instances of class ``socketio.AsyncServer`` will automatically use aiohttp -for asynchronous operations if the library is installed. To request its use -explicitly, the ``async_mode`` option can be given in the constructor:: - - sio = socketio.AsyncServer(async_mode='aiohttp') - -A server configured for aiohttp must be attached to an existing application:: - - app = web.Application() - sio.attach(app) - -The aiohttp application can define regular routes that will coexist with the -Socket.IO server. A typical pattern is to add routes that serve a client -application and any associated static files. - -The aiohttp application is then executed in the usual manner:: - - if __name__ == '__main__': - web.run_app(app) - -Tornado -~~~~~~~ - -`Tornado `_ is a web framework with support -for HTTP and WebSocket. Support for this framework requires Python 3.5 and -newer. Only Tornado version 5 and newer are supported, thanks to its tight -integration with asyncio. - -Instances of class ``socketio.AsyncServer`` will automatically use tornado -for asynchronous operations if the library is installed. To request its use -explicitly, the ``async_mode`` option can be given in the constructor:: - - sio = socketio.AsyncServer(async_mode='tornado') - -A server configured for tornado must include a request handler for -Engine.IO:: - - app = tornado.web.Application( - [ - (r"/socketio.io/", socketio.get_tornado_handler(sio)), - ], - # ... other application options - ) - -The tornado application can define other routes that will coexist with the -Socket.IO server. A typical pattern is to add routes that serve a client -application and any associated static files. - -The tornado application is then executed in the usual manner:: - - app.listen(port) - tornado.ioloop.IOLoop.current().start() - -Eventlet -~~~~~~~~ - -`Eventlet `_ is a high performance concurrent networking -library for Python 2 and 3 that uses coroutines, enabling code to be written in -the same style used with the blocking standard library functions. An Socket.IO -server deployed with eventlet has access to the long-polling and WebSocket -transports. - -Instances of class ``socketio.Server`` will automatically use eventlet for -asynchronous operations if the library is installed. To request its use -explicitly, the ``async_mode`` option can be given in the constructor:: - - sio = socketio.Server(async_mode='eventlet') - -A server configured for eventlet is deployed as a regular WSGI application, -using the provided ``socketio.Middleware``:: - - app = socketio.Middleware(sio) - import eventlet - eventlet.wsgi.server(eventlet.listen(('', 8000)), app) - -An alternative to running the eventlet WSGI server as above is to use -`gunicorn `_, a fully featured pure Python web server. The -command to launch the application under gunicorn is shown below:: - - $ gunicorn -k eventlet -w 1 module:app - -Due to limitations in its load balancing algorithm, gunicorn can only be used -with one worker process, so the ``-w`` option cannot be set to a value higher -than 1. A single eventlet worker can handle a large number of concurrent -clients, each handled by a greenlet. - -Eventlet provides a ``monkey_patch()`` function that replaces all the blocking -functions in the standard library with equivalent asynchronous versions. While -python-socketio does not require monkey patching, other libraries such as -database drivers are likely to require it. - -Gevent -~~~~~~ - -`Gevent `_ is another asynchronous framework based on -coroutines, very similar to eventlet. An Socket.IO server deployed with -gevent has access to the long-polling transport. If project -`gevent-websocket `_ is -installed, the WebSocket transport is also available. - -Instances of class ``socketio.Server`` will automatically use gevent for -asynchronous operations if the library is installed and eventlet is not -installed. To request gevent to be selected explicitly, the ``async_mode`` -option can be given in the constructor:: - - sio = socketio.Server(async_mode='gevent') - -A server configured for gevent is deployed as a regular WSGI application, -using the provided ``socketio.Middleware``:: - - app = socketio.Middleware(sio) - from gevent import pywsgi - pywsgi.WSGIServer(('', 8000), app).serve_forever() - -If the WebSocket transport is installed, then the server must be started as -follows:: - - from gevent import pywsgi - from geventwebsocket.handler import WebSocketHandler - app = socketio.Middleware(sio) - pywsgi.WSGIServer(('', 8000), app, - handler_class=WebSocketHandler).serve_forever() - -An alternative to running the gevent WSGI server as above is to use -`gunicorn `_, a fully featured pure Python web server. The -command to launch the application under gunicorn is shown below:: - - $ gunicorn -k gevent -w 1 module:app - -Or to include WebSocket:: - - $ gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 module: app - -Same as with eventlet, due to limitations in its load balancing algorithm, -gunicorn can only be used with one worker process, so the ``-w`` option cannot -be higher than 1. A single gevent worker can handle a large number of -concurrent clients through the use of greenlets. - -Gevent provides a ``monkey_patch()`` function that replaces all the blocking -functions in the standard library with equivalent asynchronous versions. While -python-socketio does not require monkey patching, other libraries such as -database drivers are likely to require it. - -Gevent with uWSGI -~~~~~~~~~~~~~~~~~ - -When using the uWSGI server in combination with gevent, the Socket.IO server -can take advantage of uWSGI's native WebSocket support. - -Instances of class ``socketio.Server`` will automatically use this option for -asynchronous operations if both gevent and uWSGI are installed and eventlet is -not installed. To request this asynchronous mode explicitly, the -``async_mode`` option can be given in the constructor:: - - # gevent with uWSGI - sio = socketio.Server(async_mode='gevent_uwsgi') - -A complete explanation of the configuration and usage of the uWSGI server is -beyond the scope of this documentation. The uWSGI server is a fairly complex -package that provides a large and comprehensive set of options. It must be -compiled with WebSocket and SSL support for the WebSocket transport to be -available. As way of an introduction, the following command starts a uWSGI -server for the ``latency.py`` example on port 5000:: - - $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app - -Standard Threading Library -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -While not comparable to eventlet and gevent in terms of performance, -the Socket.IO server can also be configured to work with multi-threaded web -servers that use standard Python threads. This is an ideal setup to use with -development servers such as `Werkzeug `_. Only the -long-polling transport is currently available when using standard threads. - -Instances of class ``socketio.Server`` will automatically use the threading -mode if neither eventlet nor gevent are not installed. To request the -threading mode explicitly, the ``async_mode`` option can be given in the -constructor:: - - sio = socketio.Server(async_mode='threading') - -A server configured for threading is deployed as a regular web application, -using any WSGI complaint multi-threaded server. The example below deploys an -Socket.IO application combined with a Flask web application, using Flask's -development web server based on Werkzeug:: - - sio = socketio.Server(async_mode='threading') - app = Flask(__name__) - app.wsgi_app = socketio.Middleware(sio, app.wsgi_app) - - # ... Socket.IO and Flask handler functions ... - - if __name__ == '__main__': - app.run(threaded=True) - -When using the threading mode, it is important to ensure that the WSGI server -can handle multiple concurrent requests using threads, since a client can have -up to two outstanding requests at any given time. The Werkzeug server is -single-threaded by default, so the ``threaded=True`` option is required. - -Note that servers that use worker processes instead of threads, such as -gunicorn, do not support a Socket.IO server configured in threading mode. - -Multi-process deployments -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Socket.IO is a stateful protocol, which makes horizontal scaling more -difficult. To deploy a cluster of Socket.IO processes (hosted on one or -multiple servers), the following conditions must be met: - -- Each Socket.IO process must be able to handle multiple requests, either by - using asyncio, eventlet, gevent, or standard threads. Worker processes that - only handle one request at a time are not supported. -- The load balancer must be configured to always forward requests from a - client to the same worker process. Load balancers call this *sticky - sessions*, or *session affinity*. -- The worker processes communicate with each other through a message queue, - which must be installed and configured. See the section on using message - queues above for instructions. - -API Reference -------------- - -.. module:: socketio - -.. autoclass:: Middleware - :members: - -.. autoclass:: Server - :members: - -.. autoclass:: AsyncServer - :members: - :inherited-members: - -.. autoclass:: Namespace - :members: - -.. autoclass:: AsyncNamespace - :members: - :inherited-members: - -.. autoclass:: BaseManager - :members: - -.. autoclass:: PubSubManager - :members: - -.. autoclass:: KombuManager - :members: - -.. autoclass:: RedisManager - :members: - -.. autoclass:: AsyncManager - :members: - :inherited-members: +Indices and tables +------------------ -.. autoclass:: AsyncRedisManager - :members: +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/intro.rst b/docs/intro.rst new file mode 100644 index 00000000..844f2a68 --- /dev/null +++ b/docs/intro.rst @@ -0,0 +1,148 @@ +.. socketio documentation master file, created by + sphinx-quickstart on Sat Jun 13 23:41:23 2015. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Getting Started +=============== + +What is Socket.IO? +------------------ + +Socket.IO is a transport protocol that enables real-time bidirectional +event-based communication between clients (typically web browsers or +smartphones) and a server. There are Socket.IO clients and servers implemented +in a variety of languages, including JavaScript, Python, C++, Swift, C# and +PHP. + +Features +-------- + +- Fully compatible with the + `Javascript `_, + `Swift `_, + `C++ `_ and + `Java `_ official + Socket.IO clients, plus any third party clients that comply with the + Socket.IO specification. +- Compatible with Python 2.7 and Python 3.3+. +- Supports large number of clients even on modest hardware due to being + asynchronous, even when asyncio is not used. +- Compatible with `aiohttp `_, + `sanic `_, + `tornado `_, + `eventlet `_, + `gevent `_, + or any `WSGI `_ or + `ASGI `_ compatible server. +- Includes WSGI and ASGI middlewares that integrate Socket.IO traffic with + other web applications. +- Broadcasting of messages to all connected clients, or to subsets of them + assigned to "rooms". +- Optional support for multiple servers, connected through a messaging queue + such as Redis or RabbitMQ. +- Send messages to clients from external processes, such as Celery workers or + auxiliary scripts. +- Event-based architecture implemented with decorators that hides the details + of the protocol. +- Support for HTTP long-polling and WebSocket transports. +- Support for XHR2 and XHR browsers. +- Support for text and binary messages. +- Support for gzip and deflate HTTP compression. +- Configurable CORS responses, to avoid cross-origin problems with browsers. + +Examples +-------- + +The Socket.IO server can be installed with pip:: + + pip install python-socketio + +The following is a basic example of a Socket.IO server that uses the +`aiohttp `_ framework for asyncio (Python 3.5+ +only): + +.. code:: python + + from aiohttp import web + import socketio + + sio = socketio.AsyncServer() + app = web.Application() + sio.attach(app) + + async def index(request): + """Serve the client-side application.""" + with open('index.html') as f: + return web.Response(text=f.read(), content_type='text/html') + + @sio.on('connect', namespace='/chat') + def connect(sid, environ): + print("connect ", sid) + + @sio.on('chat message', namespace='/chat') + async def message(sid, data): + print("message ", data) + await sio.emit('reply', room=sid) + + @sio.on('disconnect', namespace='/chat') + def disconnect(sid): + print('disconnect ', sid) + + app.router.add_static('/static', 'static') + app.router.add_get('/', index) + + if __name__ == '__main__': + web.run_app(app) + +And below is a similar example, but using Flask and Eventlet. This example is +compatible with Python 2.7 and 3.3+:: + + import socketio + import eventlet + from flask import Flask, render_template + + sio = socketio.Server() + app = Flask(__name__) + + @app.route('/') + def index(): + """Serve the client-side application.""" + return render_template('index.html') + + @sio.on('connect') + def connect(sid, environ): + print('connect ', sid) + + @sio.on('my message') + def message(sid, data): + print('message ', data) + + @sio.on('disconnect') + def disconnect(sid): + print('disconnect ', sid) + + if __name__ == '__main__': + # wrap Flask application with socketio's middleware + app = socketio.WSGIApp(sio, app) + + # deploy as an eventlet WSGI server + eventlet.wsgi.server(eventlet.listen(('', 8000)), app) + +The client-side application must include the +`socket.io-client `_ library +(versions 1.3.5 or newer recommended). + +Each time a client connects to the server the ``connect`` event handler is +invoked with the ``sid`` (session ID) assigned to the connection and the WSGI +environment dictionary. The server can inspect authentication or other headers +to decide if the client is allowed to connect. To reject a client the handler +must return ``False``. + +When the client sends an event to the server, the appropriate event handler is +invoked with the ``sid`` and the message, which can be a single or multiple +arguments. The application can define as many events as needed and associate +them with event handlers. An event is defined simply by a name. + +When a connection with a client is broken, the ``disconnect`` event is called, +allowing the application to perform cleanup. diff --git a/docs/make.bat b/docs/make.bat index ada489d4..27f573b8 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,263 +1,35 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - echo. coverage to run coverage check of the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -REM Check if sphinx-build is available and fallback to Python version if any -%SPHINXBUILD% 2> nul -if errorlevel 9009 goto sphinx_python -goto sphinx_ok - -:sphinx_python - -set SPHINXBUILD=python -m sphinx.__init__ -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -:sphinx_ok - - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\socketio.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\socketio.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "coverage" ( - %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage - if errorlevel 1 exit /b 1 - echo. - echo.Testing of coverage in the sources finished, look at the ^ -results in %BUILDDIR%/coverage/python.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/socketio/asgi.py b/socketio/asgi.py index 6596dbb5..2339462b 100644 --- a/socketio/asgi.py +++ b/socketio/asgi.py @@ -19,7 +19,9 @@ class ASGIApp(engineio.ASGIApp): :param socketio_path: The endpoint where the Socket.IO application should be installed. The default value is appropriate for most cases. + Example usage:: + import socketio import uvicorn From 8b5c737b2dcf5ee3fffc621dae2f12a5291eba10 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Mon, 26 Nov 2018 09:32:43 +0000 Subject: [PATCH 033/571] Release 2.1.0 --- socketio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketio/__init__.py b/socketio/__init__.py index 10eb3250..aa24a7c2 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -21,7 +21,7 @@ AsyncNamespace = None AsyncRedisManager = None -__version__ = '2.0.0' +__version__ = '2.1.0' __all__ = ['__version__', 'Server', 'BaseManager', 'PubSubManager', 'KombuManager', 'RedisManager', 'ZmqManager', 'Namespace', From fce2006eeef2528e9b59ce9097ff215e6117d787 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 5 Dec 2018 14:52:57 +0000 Subject: [PATCH 034/571] fix backwards compatible problems with python-engineio 3.0 --- socketio/middleware.py | 4 ++++ socketio/tornado.py | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/socketio/middleware.py b/socketio/middleware.py index aa1b33b4..252c65f8 100644 --- a/socketio/middleware.py +++ b/socketio/middleware.py @@ -39,3 +39,7 @@ def __init__(self, socketio_app, wsgi_app=None, static_files=None, class Middleware(WSGIApp): """This class has been renamed to WSGIApp and is now deprecated.""" + def __init__(self, socketio_app, wsgi_app=None, + socketio_path='socket.io'): + super(Middleware, self).__init__(socketio_app, wsgi_app, + socketio_path=socketio_path) diff --git a/socketio/tornado.py b/socketio/tornado.py index 007c4924..10a4d4d7 100644 --- a/socketio/tornado.py +++ b/socketio/tornado.py @@ -1,7 +1,11 @@ import sys if sys.version_info >= (3, 5): - from engineio.async_tornado import get_tornado_handler as \ - get_engineio_handler + try: + from engineio.async_drivers.tornado import get_tornado_handler as \ + get_engineio_handler + except ImportError: + from engineio.async_tornado import get_tornado_handler as \ + get_engineio_handler def get_tornado_handler(socketio_server): From 1194bd2c80530b09c1ae47650083bbb3edb8bdc0 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 5 Dec 2018 14:53:50 +0000 Subject: [PATCH 035/571] Release 2.1.1 --- socketio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketio/__init__.py b/socketio/__init__.py index aa24a7c2..142a5846 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -21,7 +21,7 @@ AsyncNamespace = None AsyncRedisManager = None -__version__ = '2.1.0' +__version__ = '2.1.1' __all__ = ['__version__', 'Server', 'BaseManager', 'PubSubManager', 'KombuManager', 'RedisManager', 'ZmqManager', 'Namespace', From f642a7f2078fc639ed1302b5b3f7133f19c629c6 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Mon, 10 Dec 2018 10:23:23 +0000 Subject: [PATCH 036/571] update dependencies --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d16ecd84..2456ad6d 100755 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ platforms='any', install_requires=[ 'six>=1.9.0', - 'python-engineio>=2.2.0' + 'python-engineio>=3.0.0' ], tests_require=[ 'mock', From b2a58502e1e33ab9c735ee5a270733d53f33d2ad Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Mon, 10 Dec 2018 10:23:48 +0000 Subject: [PATCH 037/571] Release 2.1.2 --- socketio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/socketio/__init__.py b/socketio/__init__.py index 142a5846..e4b0bbd9 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -21,7 +21,7 @@ AsyncNamespace = None AsyncRedisManager = None -__version__ = '2.1.1' +__version__ = '2.1.2' __all__ = ['__version__', 'Server', 'BaseManager', 'PubSubManager', 'KombuManager', 'RedisManager', 'ZmqManager', 'Namespace', From b084b2c80e7acd098365a254f3c2c6553281b3e4 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 15 Dec 2018 16:55:40 +0000 Subject: [PATCH 038/571] basic socketio client (no reconnection yet) --- docs/api.rst | 1 + socketio/__init__.py | 15 +- socketio/asyncio_namespace.py | 52 +++++ socketio/client.py | 429 ++++++++++++++++++++++++++++++++++ socketio/namespace.py | 88 +++++-- socketio/server.py | 2 +- 6 files changed, 564 insertions(+), 23 deletions(-) create mode 100644 socketio/client.py diff --git a/docs/api.rst b/docs/api.rst index 5b4f1bb1..55e82148 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -42,6 +42,7 @@ API Reference .. autoclass:: Namespace :members: + :inherited-members: ``AsyncNamespace`` class ------------------------ diff --git a/socketio/__init__.py b/socketio/__init__.py index e4b0bbd9..f18c6b41 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -1,18 +1,20 @@ import sys +from .client import Client from .base_manager import BaseManager from .pubsub_manager import PubSubManager from .kombu_manager import KombuManager from .redis_manager import RedisManager from .zmq_manager import ZmqManager from .server import Server -from .namespace import Namespace +from .namespace import Namespace, ClientNamespace from .middleware import WSGIApp, Middleware from .tornado import get_tornado_handler if sys.version_info >= (3, 5): # pragma: no cover + # from .asyncio_client import AsyncClient from .asyncio_server import AsyncServer from .asyncio_manager import AsyncManager - from .asyncio_namespace import AsyncNamespace + from .asyncio_namespace import AsyncNamespace, AsyncClientNamespace from .asyncio_redis_manager import AsyncRedisManager from .asgi import ASGIApp else: # pragma: no cover @@ -23,9 +25,10 @@ __version__ = '2.1.2' -__all__ = ['__version__', 'Server', 'BaseManager', 'PubSubManager', +__all__ = ['__version__', 'Client', 'Server', 'BaseManager', 'PubSubManager', 'KombuManager', 'RedisManager', 'ZmqManager', 'Namespace', - 'WSGIApp', 'Middleware'] + 'ClientNamespace', 'WSGIApp', 'Middleware'] if AsyncServer is not None: # pragma: no cover - __all__ += ['AsyncServer', 'AsyncNamespace', 'AsyncManager', - 'AsyncRedisManager', 'ASGIApp', 'get_tornado_handler'] + __all__ += ['AsyncServer', 'AsyncNamespace', 'AsyncClientNamespace', + 'AsyncManager', 'AsyncRedisManager', 'ASGIApp', + 'get_tornado_handler'] diff --git a/socketio/asyncio_namespace.py b/socketio/asyncio_namespace.py index 893c5e01..0861afd2 100644 --- a/socketio/asyncio_namespace.py +++ b/socketio/asyncio_namespace.py @@ -93,3 +93,55 @@ async def disconnect(self, sid, namespace=None): """ return await self.server.disconnect( sid, namespace=namespace or self.namespace) + + +class AsyncClientNamespace(namespace.ClientNamespace): + """Base class for asyncio class-based namespaces. + + A class-based namespace is a class that contains all the event handlers + for a Socket.IO namespace. The event handlers are methods of the class + with the prefix ``on_``, such as ``on_connect``, ``on_disconnect``, + ``on_message``, ``on_json``, and so on. These can be regular functions or + coroutines. + + :param namespace: The Socket.IO namespace to be used with all the event + handlers defined in this class. If this argument is + omitted, the default namespace is used. + """ + async def emit(self, event, data=None, namespace=None, callback=None): + """Emit a custom event to the server. + + The only difference with the :func:`socketio.Client.emit` method is + that when the ``namespace`` argument is not given the namespace + associated with the class is used. + + Note: this method is a coroutine. + """ + return await self.server.emit(event, data=data, + namespace=namespace or self.namespace, + callback=callback) + + async def send(self, data, namespace=None, callback=None): + """Send a message to the server. + + The only difference with the :func:`socketio.Client.send` method is + that when the ``namespace`` argument is not given the namespace + associated with the class is used. + + Note: this method is a coroutine. + """ + return await self.server.send(data, + namespace=namespace or self.namespace, + callback=callback) + + async def disconnect(self, namespace=None): + """Disconnect a client. + + The only difference with the :func:`socketio.Client.disconnect` method + is that when the ``namespace`` argument is not given the namespace + associated with the class is used. + + Note: this method is a coroutine. + """ + return await self.server.disconnect( + namespace=namespace or self.namespace) diff --git a/socketio/client.py b/socketio/client.py new file mode 100644 index 00000000..de350b5c --- /dev/null +++ b/socketio/client.py @@ -0,0 +1,429 @@ +import itertools +import logging + +import engineio +import six + +from . import namespace +from . import packet + +default_logger = logging.getLogger('socketio.client') + + +class Client(object): + """An Engine.IO client. + + This class implements a fully compliant Engine.IO web client with support + for websocket and long-polling transports. + + :param reconnection: ``True`` if the client should automatically attempt to + reconnect to the server after an interruption, or + ``False`` to not reconnect. The default is ``True``. + :param reconnection_attempts: How many reconnection attempts to issue + before giving up, or 0 for infinity attempts. + The default is 0. + :param reconnection_delay: How long to wait in seconds before the first + reconnection attempt. Each successive attempt + doubles this delay. + :param reconnection_delay_max: The maximum delay between reconnection + attempts. + :param randomization_factor: Randomization amount for each delay between + reconnection attempts. The default is 0.5, + which means that each delay is randomly + adjusted by +/- 50%. + :param logger: To enable logging set to ``True`` or pass a logger object to + use. To disable logging set to ``False``. The default is + ``False``. + :param binary: ``True`` to support binary payloads, ``False`` to treat all + payloads as text. On Python 2, if this is set to ``True``, + ``unicode`` values are treated as text, and ``str`` and + ``bytes`` values are treated as binary. This option has no + effect on Python 3, where text and binary payloads are + always automatically discovered. + :param json: An alternative json module to use for encoding and decoding + packets. Custom json modules must have ``dumps`` and ``loads`` + functions that are compatible with the standard library + versions. + + The Engine.IO configuration supports the following settings: + + :param engineio_logger: To enable Engine.IO logging set to ``True`` or pass + a logger object to use. To disable logging set to + ``False``. The default is ``False``. + """ + def __init__(self, reconnection=True, reconnection_attempts=0, + reconnection_delay=1, reconnection_delay_max=5, + randomization_factor=0.5, logger=False, binary=False, + json=None, **kwargs): + engineio_options = kwargs + engineio_logger = engineio_options.pop('engineio_logger', None) + if engineio_logger is not None: + engineio_options['logger'] = engineio_logger + if json is not None: + packet.Packet.json = json + engineio_options['json'] = json + + self.eio = self._engineio_client_class()(**engineio_options) + self.eio.on('connect', self._handle_eio_connect) + self.eio.on('message', self._handle_eio_message) + self.eio.on('disconnect', self._handle_eio_disconnect) + self.binary = binary + self.namespaces = None + self.handlers = {} + self.namespace_handlers = {} + self.callbacks = {} + self._binary_packet = None + + if not isinstance(logger, bool): + self.logger = logger + else: + self.logger = default_logger + if not logging.root.handlers and \ + self.logger.level == logging.NOTSET: + if logger: + self.logger.setLevel(logging.INFO) + else: + self.logger.setLevel(logging.ERROR) + self.logger.addHandler(logging.StreamHandler()) + + def is_asyncio_based(self): + return False + + def on(self, event, handler=None, namespace=None): + """Register an event handler. + + :param event: The event name. It can be any string. The event names + ``'connect'``, ``'message'`` and ``'disconnect'`` are + reserved and should not be used. + :param handler: The function that should be invoked to handle the + event. When this parameter is not given, the method + acts as a decorator for the handler function. + :param namespace: The Socket.IO namespace for the event. If this + argument is omitted the handler is associated with + the default namespace. + + Example usage:: + + # as a decorator: + @sio.on('connect') + def connect_handler(): + print('Connected!') + + # as a method: + def message_handler(msg): + print('Received message: ', msg) + sio.send( 'response') + sio.on('message', message_handler) + + The ``'connect'`` event handler receives no arguments. The + ``'message'`` handler and handlers for custom event names receive the + message payload as only argument. Any values returned from a message + handler will be passed to the client's acknowledgement callback + function if it exists. The ``'disconnect'`` handler does not take + arguments. + """ + namespace = namespace or '/' + + def set_handler(handler): + if namespace not in self.handlers: + self.handlers[namespace] = {} + self.handlers[namespace][event] = handler + return handler + + if handler is None: + return set_handler + set_handler(handler) + + def register_namespace(self, namespace_handler): + """Register a namespace handler object. + + :param namespace_handler: An instance of a :class:`Namespace` + subclass that handles all the event traffic + for a namespace. + """ + if not isinstance(namespace_handler, namespace.ClientNamespace): + raise ValueError('Not a namespace instance') + if self.is_asyncio_based() != namespace_handler.is_asyncio_based(): + raise ValueError('Not a valid namespace class for this client') + namespace_handler._set_client(self) + self.namespace_handlers[namespace_handler.namespace] = \ + namespace_handler + + def connect(self, url, headers={}, transports=None, + namespaces=None, socketio_path='socket.io'): + """Connect to a Socket.IO server. + + :param url: The URL of the Socket.IO server. It can include custom + query string parameters if required by the server. + :param headers: A dictionary with custom headers to send with the + connection request. + :param transports: The list of allowed transports. Valid transports + are ``'polling'`` and ``'websocket'``. If not + given, the polling transport is connected first, + then an upgrade to websocket is attempted. + :param namespaces: The list of custom namespaces to connect, in + addition to the default namespace. If not given, + the namespace list is obtained from the registered + event handlers. + :param socketio_path: The endpoint where the Socket.IO server is + installed. The default value is appropriate for + most cases. + + Example usage:: + + sio = socketio.Client() + sio.connect('http://localhost:5000') + """ + if namespaces is None: + namespaces = set(self.handlers.keys()).union( + set(self.namespace_handlers.keys())) + self.namespaces = [n for n in namespaces if n != '/'] + self.eio.connect(url, headers=headers, transports=transports, + engineio_path=socketio_path) + + def wait(self): + """Wait until the connection with the server ends. + + Client applications can use this function to block the main thread + during the life of the connection. + """ + self.eio.wait() + + def emit(self, event, data=None, namespace=None, callback=None): + """Emit a custom event to one or more connected clients. + + :param event: The event name. It can be any string. The event names + ``'connect'``, ``'message'`` and ``'disconnect'`` are + reserved and should not be used. + :param data: The data to send to the client or clients. Data can be of + type ``str``, ``bytes``, ``list`` or ``dict``. If a + ``list`` or ``dict``, the data will be serialized as JSON. + :param namespace: The Socket.IO namespace for the event. If this + argument is omitted the event is emitted to the + default namespace. + :param callback: If given, this function will be called to acknowledge + the the client has received the message. The arguments + that will be passed to the function are those provided + by the client. Callback functions can only be used + when addressing an individual client. + """ + namespace = namespace or '/' + self.logger.info('emitting event "%s" [%s]', event, namespace) + if callback is not None: + id = self._generate_ack_id(namespace, callback) + else: + id = None + self._emit_internal(event, data, namespace, id) + + def send(self, data, namespace=None, callback=None): + """Send a message to one or more connected clients. + + This function emits an event with the name ``'message'``. Use + :func:`emit` to issue custom event names. + + :param data: The data to send to the client or clients. Data can be of + type ``str``, ``bytes``, ``list`` or ``dict``. If a + ``list`` or ``dict``, the data will be serialized as JSON. + :param namespace: The Socket.IO namespace for the event. If this + argument is omitted the event is emitted to the + default namespace. + :param callback: If given, this function will be called to acknowledge + the the client has received the message. The arguments + that will be passed to the function are those provided + by the client. Callback functions can only be used + when addressing an individual client. + """ + self.emit('message', data, namespace, callback) + + def disconnect(self): + """Disconnect from the server.""" + for n in self.namespaces: + self._trigger_event('disconnect', namespace=n) + self._send_packet(packet.Packet(packet.DISCONNECT, namespace=n)) + self._trigger_event('disconnect', namespace='/') + self._send_packet(packet.Packet( + packet.DISCONNECT, namespace='/')) + + def transport(self): + """Return the name of the transport used by the client. + + The two possible values returned by this function are ``'polling'`` + and ``'websocket'``. + """ + return self.eio.transport() + + def start_background_task(self, target, *args, **kwargs): + """Start a background task using the appropriate async model. + + This is a utility function that applications can use to start a + background task using the method that is compatible with the + selected async mode. + + :param target: the target function to execute. + :param args: arguments to pass to the function. + :param kwargs: keyword arguments to pass to the function. + + This function returns an object compatible with the `Thread` class in + the Python standard library. The `start()` method on this object is + already called by this function. + """ + return self.eio.start_background_task(target, *args, **kwargs) + + def sleep(self, seconds=0): + """Sleep for the requested amount of time using the appropriate async + model. + + This is a utility function that applications can use to put a task to + sleep without having to worry about using the correct call for the + selected async mode. + """ + return self.eio.sleep(seconds) + + def _emit_internal(self, event, data, namespace=None, id=None): + """Send a message to a client.""" + if six.PY2 and not self.binary: + binary = False # pragma: nocover + else: + binary = None + # tuples are expanded to multiple arguments, everything else is sent + # as a single argument + if isinstance(data, tuple): + data = list(data) + else: + data = [data] + self._send_packet(packet.Packet(packet.EVENT, namespace=namespace, + data=[event] + data, id=id, + binary=binary)) + + def _send_packet(self, pkt): + """Send a Socket.IO packet to the server.""" + encoded_packet = pkt.encode() + if isinstance(encoded_packet, list): + binary = False + for ep in encoded_packet: + self.eio.send(ep, binary=binary) + binary = True + else: + self.eio.send(encoded_packet, binary=False) + + def _generate_ack_id(self, namespace, callback): + """Generate a unique identifier for an ACK packet.""" + namespace = namespace or '/' + if namespace not in self.callbacks: + self.callbacks[namespace] = {0: itertools.count(1)} + id = six.next(self.callbacks[namespace][0]) + self.callbacks[namespace][id] = callback + return id + + def _handle_connect(self, namespace): + namespace = namespace or '/' + self.logger.info('namespace {} is connected'.format(namespace)) + self._trigger_event('connect', namespace=namespace) + if namespace == '/': + for n in self.namespaces: + self._send_packet(packet.Packet(packet.CONNECT, namespace=n)) + + def _handle_disconnect(self, namespace): + namespace = namespace or '/' + self._trigger_event('disconnect', namespace=namespace) + if namespace in self.namespaces: + self.namespaces.remove(namespace) + + def _handle_event(self, namespace, id, data): + namespace = namespace or '/' + self.logger.info('received event "%s" [%s]', data[0], namespace) + self._handle_event_internal(data, namespace, id) + + def _handle_event_internal(self, data, namespace, id): + r = self._trigger_event(data[0], namespace, *data[1:]) + if id is not None: + # send ACK packet with the response returned by the handler + # tuples are expanded as multiple arguments + if r is None: + data = [] + elif isinstance(r, tuple): + data = list(r) + else: + data = [r] + if six.PY2 and not self.binary: + binary = False # pragma: nocover + else: + binary = None + self._send_packet(packet.Packet(packet.ACK, namespace=namespace, + id=id, data=data, binary=binary)) + + def _handle_ack(self, namespace, id, data): + namespace = namespace or '/' + self.logger.info('received ack [%s]', namespace) + callback = None + try: + callback = self.callbacks[namespace][id] + except KeyError: + # if we get an unknown callback we just ignore it + self.logger.warning('Unknown callback received, ignoring.') + else: + del self.callbacks[namespace][id] + if callback is not None: + callback(*data) + + def _handle_error(self, namespace, data): + namespace = namespace or '/' + self.logger.info('connection to namespace {} was rejected'.format( + namespace)) + if namespace in self.namespaces: + self.namespaces.remove(namespace) + + def _trigger_event(self, event, namespace, *args): + """Invoke an application event handler.""" + # first see if we have an explicit handler for the event + if namespace in self.handlers and event in self.handlers[namespace]: + return self.handlers[namespace][event](*args) + + # or else, forward the event to a namepsace handler if one exists + elif namespace in self.namespace_handlers: + return self.namespace_handlers[namespace].trigger_event( + event, *args) + + def _handle_eio_connect(self): + """Handle the Engine.IO connection event.""" + self.logger.info('engine.io connection established') + + def _handle_eio_message(self, data): + """Dispatch Engine.IO messages.""" + if self._binary_packet: + pkt = self._binary_packet + if pkt.add_attachment(data): + self._binary_packet = None + if pkt.packet_type == packet.BINARY_EVENT: + self._handle_event(pkt.namespace, pkt.id, pkt.data) + else: + self._handle_ack(pkt.namespace, pkt.id, pkt.data) + else: + pkt = packet.Packet(encoded_packet=data) + if pkt.packet_type == packet.CONNECT: + self._handle_connect(pkt.namespace) + elif pkt.packet_type == packet.DISCONNECT: + self._handle_disconnect(pkt.namespace) + elif pkt.packet_type == packet.EVENT: + self._handle_event(pkt.namespace, pkt.id, pkt.data) + elif pkt.packet_type == packet.ACK: + self._handle_ack(pkt.namespace, pkt.id, pkt.data) + elif pkt.packet_type == packet.BINARY_EVENT or \ + pkt.packet_type == packet.BINARY_ACK: + self._binary_packet = pkt + elif pkt.packet_type == packet.ERROR: + self._handle_error(pkt.namespace, pkt.data) + else: + raise ValueError('Unknown packet type.') + + def _handle_eio_disconnect(self): + """Handle the Engine.IO disconnection event.""" + self.logger.info('engine.io connection dropped') + for n in self.namespaces: + self._trigger_event('disconnect', namespace=n) + self._trigger_event('disconnect', namespace='/') + self.callbacks = {} + self._binary_packet = None + + def _engineio_client_class(self): + return engineio.Client diff --git a/socketio/namespace.py b/socketio/namespace.py index 14a29e1a..9207b6f7 100644 --- a/socketio/namespace.py +++ b/socketio/namespace.py @@ -1,21 +1,6 @@ -class Namespace(object): - """Base class for class-based namespaces. - - A class-based namespace is a class that contains all the event handlers - for a Socket.IO namespace. The event handlers are methods of the class - with the prefix ``on_``, such as ``on_connect``, ``on_disconnect``, - ``on_message``, ``on_json``, and so on. - - :param namespace: The Socket.IO namespace to be used with all the event - handlers defined in this class. If this argument is - omitted, the default namespace is used. - """ +class BaseNamespace(object): def __init__(self, namespace=None): self.namespace = namespace or '/' - self.server = None - - def _set_server(self, server): - self.server = server def is_asyncio_based(self): return False @@ -32,6 +17,26 @@ def trigger_event(self, event, *args): if hasattr(self, handler_name): return getattr(self, handler_name)(*args) + +class Namespace(BaseNamespace): + """Base class for server-side class-based namespaces. + + A class-based namespace is a class that contains all the event handlers + for a Socket.IO namespace. The event handlers are methods of the class + with the prefix ``on_``, such as ``on_connect``, ``on_disconnect``, + ``on_message``, ``on_json``, and so on. + + :param namespace: The Socket.IO namespace to be used with all the event + handlers defined in this class. If this argument is + omitted, the default namespace is used. + """ + def __init__(self, namespace=None): + super(Namespace, self).__init__(namespace=namespace) + self.server = None + + def _set_server(self, server): + self.server = server + def emit(self, event, data=None, room=None, skip_sid=None, namespace=None, callback=None): """Emit a custom event to one or more connected clients. @@ -104,3 +109,54 @@ def disconnect(self, sid, namespace=None): """ return self.server.disconnect(sid, namespace=namespace or self.namespace) + + +class ClientNamespace(BaseNamespace): + """Base class for client-side class-based namespaces. + + A class-based namespace is a class that contains all the event handlers + for a Socket.IO namespace. The event handlers are methods of the class + with the prefix ``on_``, such as ``on_connect``, ``on_disconnect``, + ``on_message``, ``on_json``, and so on. + + :param namespace: The Socket.IO namespace to be used with all the event + handlers defined in this class. If this argument is + omitted, the default namespace is used. + """ + def __init__(self, namespace=None): + super(Namespace, self).__init__(namespace=namespace) + self.client = None + + def _set_client(self, client): + self.client = client + + def emit(self, event, data=None, namespace=None, callback=None): + """Emit a custom event to the server. + + The only difference with the :func:`socketio.Client.emit` method is + that when the ``namespace`` argument is not given the namespace + associated with the class is used. + """ + return self.client.emit(event, data=data, + namespace=namespace or self.namespace, + callback=callback) + + def send(self, data, room=None, skip_sid=None, namespace=None, + callback=None): + """Send a message to the server. + + The only difference with the :func:`socketio.Client.send` method is + that when the ``namespace`` argument is not given the namespace + associated with the class is used. + """ + return self.server.send(data, namespace=namespace or self.namespace, + callback=callback) + + def disconnect(self, namespace=None): + """Disconnect from the server. + + The only difference with the :func:`socketio.Client.disconnect` method + is that when the ``namespace`` argument is not given the namespace + associated with the class is used. + """ + return self.server.disconnect(namespace=namespace or self.namespace) diff --git a/socketio/server.py b/socketio/server.py index f9f9269a..e613a3c0 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -7,7 +7,7 @@ from . import packet from . import namespace -default_logger = logging.getLogger('socketio') +default_logger = logging.getLogger('socketio.server') class Server(object): From 63ddde0fb01afdd9f9708eb7852d6d322d5a46d4 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 16 Dec 2018 14:30:00 +0000 Subject: [PATCH 039/571] client reconnection support --- socketio/client.py | 96 +++++++++++++++++++++++++++++++++++------- socketio/exceptions.py | 6 +++ 2 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 socketio/exceptions.py diff --git a/socketio/client.py b/socketio/client.py index de350b5c..0d21e903 100644 --- a/socketio/client.py +++ b/socketio/client.py @@ -1,9 +1,11 @@ import itertools import logging +import random import engineio import six +from . import exceptions from . import namespace from . import packet @@ -55,6 +57,13 @@ def __init__(self, reconnection=True, reconnection_attempts=0, reconnection_delay=1, reconnection_delay_max=5, randomization_factor=0.5, logger=False, binary=False, json=None, **kwargs): + self.reconnection = reconnection + self.reconnection_attempts = reconnection_attempts + self.reconnection_delay = reconnection_delay + self.reconnection_delay_max = reconnection_delay_max + self.randomization_factor = randomization_factor + self.binary = binary + engineio_options = kwargs engineio_logger = engineio_options.pop('engineio_logger', None) if engineio_logger is not None: @@ -67,12 +76,6 @@ def __init__(self, reconnection=True, reconnection_attempts=0, self.eio.on('connect', self._handle_eio_connect) self.eio.on('message', self._handle_eio_message) self.eio.on('disconnect', self._handle_eio_disconnect) - self.binary = binary - self.namespaces = None - self.handlers = {} - self.namespace_handlers = {} - self.callbacks = {} - self._binary_packet = None if not isinstance(logger, bool): self.logger = logger @@ -86,6 +89,19 @@ def __init__(self, reconnection=True, reconnection_attempts=0, self.logger.setLevel(logging.ERROR) self.logger.addHandler(logging.StreamHandler()) + self.connection_url = None + self.connection_headers = None + self.connection_transports = None + self.connection_namespaces = None + self.socketio_path = None + + self.namespaces = None + self.handlers = {} + self.namespace_handlers = {} + self.callbacks = {} + self._binary_packet = None + self._reconnect_task = None + def is_asyncio_based(self): return False @@ -174,12 +190,21 @@ def connect(self, url, headers={}, transports=None, sio = socketio.Client() sio.connect('http://localhost:5000') """ + self.connection_url = url + self.connection_headers = headers + self.connection_transports = transports + self.connection_namespaces = namespaces + self.socketio_path = socketio_path + if namespaces is None: namespaces = set(self.handlers.keys()).union( set(self.namespace_handlers.keys())) self.namespaces = [n for n in namespaces if n != '/'] - self.eio.connect(url, headers=headers, transports=transports, - engineio_path=socketio_path) + try: + self.eio.connect(url, headers=headers, transports=transports, + engineio_path=socketio_path) + except engineio.exceptions.ConnectionError as exc: + six.raise_from(exceptions.ConnectionError(exc.args[0]), None) def wait(self): """Wait until the connection with the server ends. @@ -187,7 +212,13 @@ def wait(self): Client applications can use this function to block the main thread during the life of the connection. """ - self.eio.wait() + while True: + self.eio.wait() + if not self._reconnect_task: + break + self._reconnect_task.join() + if self.eio.state != 'connected': + break def emit(self, event, data=None, namespace=None, callback=None): """Emit a custom event to one or more connected clients. @@ -208,7 +239,7 @@ def emit(self, event, data=None, namespace=None, callback=None): when addressing an individual client. """ namespace = namespace or '/' - self.logger.info('emitting event "%s" [%s]', event, namespace) + self.logger.info('Emitting event "%s" [%s]', event, namespace) if callback is not None: id = self._generate_ack_id(namespace, callback) else: @@ -317,7 +348,7 @@ def _generate_ack_id(self, namespace, callback): def _handle_connect(self, namespace): namespace = namespace or '/' - self.logger.info('namespace {} is connected'.format(namespace)) + self.logger.info('Namespace {} is connected'.format(namespace)) self._trigger_event('connect', namespace=namespace) if namespace == '/': for n in self.namespaces: @@ -331,7 +362,7 @@ def _handle_disconnect(self, namespace): def _handle_event(self, namespace, id, data): namespace = namespace or '/' - self.logger.info('received event "%s" [%s]', data[0], namespace) + self.logger.info('Received event "%s" [%s]', data[0], namespace) self._handle_event_internal(data, namespace, id) def _handle_event_internal(self, data, namespace, id): @@ -354,7 +385,7 @@ def _handle_event_internal(self, data, namespace, id): def _handle_ack(self, namespace, id, data): namespace = namespace or '/' - self.logger.info('received ack [%s]', namespace) + self.logger.info('Received ack [%s]', namespace) callback = None try: callback = self.callbacks[namespace][id] @@ -368,7 +399,7 @@ def _handle_ack(self, namespace, id, data): def _handle_error(self, namespace, data): namespace = namespace or '/' - self.logger.info('connection to namespace {} was rejected'.format( + self.logger.info('Connection to namespace {} was rejected'.format( namespace)) if namespace in self.namespaces: self.namespaces.remove(namespace) @@ -384,9 +415,39 @@ def _trigger_event(self, event, namespace, *args): return self.namespace_handlers[namespace].trigger_event( event, *args) + def _handle_reconnect(self): + attempt_count = 0 + current_delay = self.reconnection_delay + while True: + delay = current_delay + current_delay *= 2 + if delay > self.reconnection_delay_max: + delay = self.reconnection_delay_max + delay += self.randomization_factor * (2 * random.random() - 1) + self.logger.info( + 'Connection failed, new attempt in {:.02f} seconds'.format( + delay)) + self.sleep(delay) + attempt_count += 1 + try: + self.connect(self.connection_url, + headers=self.connection_headers, + transports=self.connection_transports, + socketio_path=self.socketio_path) + except (exceptions.ConnectionError, ValueError): + pass + else: + self.logger.info('Reconnection successful') + break + if self.reconnection_attempts and \ + attempt_count >= self.reconnection_attempts: + self.logger.info( + 'Maximum reconnection attempts reached, giving up') + break + def _handle_eio_connect(self): """Handle the Engine.IO connection event.""" - self.logger.info('engine.io connection established') + self.logger.info('Engine.IO connection established') def _handle_eio_message(self, data): """Dispatch Engine.IO messages.""" @@ -418,12 +479,15 @@ def _handle_eio_message(self, data): def _handle_eio_disconnect(self): """Handle the Engine.IO disconnection event.""" - self.logger.info('engine.io connection dropped') + self.logger.info('Engine.IO connection dropped') for n in self.namespaces: self._trigger_event('disconnect', namespace=n) self._trigger_event('disconnect', namespace='/') self.callbacks = {} self._binary_packet = None + if self.eio.state == 'connected' and self.reconnection: + self._reconnect_task = self.start_background_task( + self._handle_reconnect) def _engineio_client_class(self): return engineio.Client diff --git a/socketio/exceptions.py b/socketio/exceptions.py new file mode 100644 index 00000000..5bd86979 --- /dev/null +++ b/socketio/exceptions.py @@ -0,0 +1,6 @@ +class SocketIOError(Exception): + pass + + +class ConnectionError(SocketIOError): + pass From 2360aa4985b2d5b5afddc2df969983eadc0661b5 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 16 Dec 2018 20:19:03 +0000 Subject: [PATCH 040/571] asyncio client --- socketio/__init__.py | 7 +- socketio/asyncio_client.py | 393 +++++++++++++++++++++++++++++++++++++ socketio/client.py | 3 +- 3 files changed, 399 insertions(+), 4 deletions(-) create mode 100644 socketio/asyncio_client.py diff --git a/socketio/__init__.py b/socketio/__init__.py index f18c6b41..f050d447 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -12,6 +12,7 @@ from .tornado import get_tornado_handler if sys.version_info >= (3, 5): # pragma: no cover # from .asyncio_client import AsyncClient + from .asyncio_client import AsyncClient from .asyncio_server import AsyncServer from .asyncio_manager import AsyncManager from .asyncio_namespace import AsyncNamespace, AsyncClientNamespace @@ -29,6 +30,6 @@ 'KombuManager', 'RedisManager', 'ZmqManager', 'Namespace', 'ClientNamespace', 'WSGIApp', 'Middleware'] if AsyncServer is not None: # pragma: no cover - __all__ += ['AsyncServer', 'AsyncNamespace', 'AsyncClientNamespace', - 'AsyncManager', 'AsyncRedisManager', 'ASGIApp', - 'get_tornado_handler'] + __all__ += ['AsyncClient', 'AsyncServer', 'AsyncNamespace', + 'AsyncClientNamespace', 'AsyncManager', 'AsyncRedisManager', + 'ASGIApp', 'get_tornado_handler'] diff --git a/socketio/asyncio_client.py b/socketio/asyncio_client.py new file mode 100644 index 00000000..fff17a30 --- /dev/null +++ b/socketio/asyncio_client.py @@ -0,0 +1,393 @@ +import asyncio +import logging +import random + +import engineio +import six + +from . import client +from . import exceptions +from . import packet + +default_logger = logging.getLogger('socketio.client') + + +class AsyncClient(client.Client): + """An Socket.IO client for asyncio. + + This class implements a fully compliant Engine.IO web client with support + for websocket and long-polling transports. + + :param reconnection: ``True`` if the client should automatically attempt to + reconnect to the server after an interruption, or + ``False`` to not reconnect. The default is ``True``. + :param reconnection_attempts: How many reconnection attempts to issue + before giving up, or 0 for infinity attempts. + The default is 0. + :param reconnection_delay: How long to wait in seconds before the first + reconnection attempt. Each successive attempt + doubles this delay. + :param reconnection_delay_max: The maximum delay between reconnection + attempts. + :param randomization_factor: Randomization amount for each delay between + reconnection attempts. The default is 0.5, + which means that each delay is randomly + adjusted by +/- 50%. + :param logger: To enable logging set to ``True`` or pass a logger object to + use. To disable logging set to ``False``. The default is + ``False``. + :param binary: ``True`` to support binary payloads, ``False`` to treat all + payloads as text. On Python 2, if this is set to ``True``, + ``unicode`` values are treated as text, and ``str`` and + ``bytes`` values are treated as binary. This option has no + effect on Python 3, where text and binary payloads are + always automatically discovered. + :param json: An alternative json module to use for encoding and decoding + packets. Custom json modules must have ``dumps`` and ``loads`` + functions that are compatible with the standard library + versions. + + The Engine.IO configuration supports the following settings: + + :param engineio_logger: To enable Engine.IO logging set to ``True`` or pass + a logger object to use. To disable logging set to + ``False``. The default is ``False``. + """ + def is_asyncio_based(self): + return True + + async def connect(self, url, headers={}, transports=None, + namespaces=None, socketio_path='socket.io'): + """Connect to a Socket.IO server. + + :param url: The URL of the Socket.IO server. It can include custom + query string parameters if required by the server. + :param headers: A dictionary with custom headers to send with the + connection request. + :param transports: The list of allowed transports. Valid transports + are ``'polling'`` and ``'websocket'``. If not + given, the polling transport is connected first, + then an upgrade to websocket is attempted. + :param namespaces: The list of custom namespaces to connect, in + addition to the default namespace. If not given, + the namespace list is obtained from the registered + event handlers. + :param socketio_path: The endpoint where the Socket.IO server is + installed. The default value is appropriate for + most cases. + + Note: this method is a coroutine. + + Example usage:: + + sio = socketio.Client() + sio.connect('http://localhost:5000') + """ + self.connection_url = url + self.connection_headers = headers + self.connection_transports = transports + self.connection_namespaces = namespaces + self.socketio_path = socketio_path + + if namespaces is None: + namespaces = set(self.handlers.keys()).union( + set(self.namespace_handlers.keys())) + self.namespaces = [n for n in namespaces if n != '/'] + try: + await self.eio.connect(url, headers=headers, + transports=transports, + engineio_path=socketio_path) + except engineio.exceptions.ConnectionError as exc: + six.raise_from(exceptions.ConnectionError(exc.args[0]), None) + + async def wait(self): + """Wait until the connection with the server ends. + + Client applications can use this function to block the main thread + during the life of the connection. + + Note: this method is a coroutine. + """ + while True: + await self.eio.wait() + if not self._reconnect_task: + break + await self._reconnect_task + if self.eio.state != 'connected': + break + + async def emit(self, event, data=None, namespace=None, callback=None): + """Emit a custom event to one or more connected clients. + + :param event: The event name. It can be any string. The event names + ``'connect'``, ``'message'`` and ``'disconnect'`` are + reserved and should not be used. + :param data: The data to send to the client or clients. Data can be of + type ``str``, ``bytes``, ``list`` or ``dict``. If a + ``list`` or ``dict``, the data will be serialized as JSON. + :param namespace: The Socket.IO namespace for the event. If this + argument is omitted the event is emitted to the + default namespace. + :param callback: If given, this function will be called to acknowledge + the the client has received the message. The arguments + that will be passed to the function are those provided + by the client. Callback functions can only be used + when addressing an individual client. + + Note: this method is a coroutine. + """ + namespace = namespace or '/' + self.logger.info('Emitting event "%s" [%s]', event, namespace) + if callback is not None: + id = self._generate_ack_id(namespace, callback) + else: + id = None + await self._emit_internal(event, data, namespace, id) + + async def send(self, data, namespace=None, callback=None): + """Send a message to one or more connected clients. + + This function emits an event with the name ``'message'``. Use + :func:`emit` to issue custom event names. + + :param data: The data to send to the client or clients. Data can be of + type ``str``, ``bytes``, ``list`` or ``dict``. If a + ``list`` or ``dict``, the data will be serialized as JSON. + :param namespace: The Socket.IO namespace for the event. If this + argument is omitted the event is emitted to the + default namespace. + :param callback: If given, this function will be called to acknowledge + the the client has received the message. The arguments + that will be passed to the function are those provided + by the client. Callback functions can only be used + when addressing an individual client. + + Note: this method is a coroutine. + """ + await self.emit('message', data, namespace, callback) + + async def disconnect(self): + """Disconnect from the server. + + Note: this method is a coroutine. + """ + for n in self.namespaces: + await self._trigger_event('disconnect', namespace=n) + await self._send_packet(packet.Packet(packet.DISCONNECT, + namespace=n)) + await self._trigger_event('disconnect', namespace='/') + await self._send_packet(packet.Packet( + packet.DISCONNECT, namespace='/')) + + def start_background_task(self, target, *args, **kwargs): + """Start a background task using the appropriate async model. + + This is a utility function that applications can use to start a + background task using the method that is compatible with the + selected async mode. + + :param target: the target function to execute. + :param args: arguments to pass to the function. + :param kwargs: keyword arguments to pass to the function. + + This function returns an object compatible with the `Thread` class in + the Python standard library. The `start()` method on this object is + already called by this function. + """ + return self.eio.start_background_task(target, *args, **kwargs) + + async def sleep(self, seconds=0): + """Sleep for the requested amount of time using the appropriate async + model. + + This is a utility function that applications can use to put a task to + sleep without having to worry about using the correct call for the + selected async mode. + + Note: this method is a coroutine. + """ + return await self.eio.sleep(seconds) + + async def _emit_internal(self, event, data, namespace=None, id=None): + """Send a message to a client.""" + if six.PY2 and not self.binary: + binary = False # pragma: nocover + else: + binary = None + # tuples are expanded to multiple arguments, everything else is sent + # as a single argument + if isinstance(data, tuple): + data = list(data) + else: + data = [data] + await self._send_packet(packet.Packet( + packet.EVENT, namespace=namespace, data=[event] + data, id=id, + binary=binary)) + + async def _send_packet(self, pkt): + """Send a Socket.IO packet to the server.""" + encoded_packet = pkt.encode() + if isinstance(encoded_packet, list): + binary = False + for ep in encoded_packet: + await self.eio.send(ep, binary=binary) + binary = True + else: + await self.eio.send(encoded_packet, binary=False) + + async def _handle_connect(self, namespace): + namespace = namespace or '/' + self.logger.info('Namespace {} is connected'.format(namespace)) + await self._trigger_event('connect', namespace=namespace) + if namespace == '/': + for n in self.namespaces: + await self._send_packet(packet.Packet(packet.CONNECT, + namespace=n)) + + async def _handle_disconnect(self, namespace): + namespace = namespace or '/' + await self._trigger_event('disconnect', namespace=namespace) + if namespace in self.namespaces: + self.namespaces.remove(namespace) + + async def _handle_event(self, namespace, id, data): + namespace = namespace or '/' + self.logger.info('Received event "%s" [%s]', data[0], namespace) + await self._handle_event_internal(data, namespace, id) + + async def _handle_event_internal(self, data, namespace, id): + r = await self._trigger_event(data[0], namespace, *data[1:]) + if id is not None: + # send ACK packet with the response returned by the handler + # tuples are expanded as multiple arguments + if r is None: + data = [] + elif isinstance(r, tuple): + data = list(r) + else: + data = [r] + if six.PY2 and not self.binary: + binary = False # pragma: nocover + else: + binary = None + await self._send_packet(packet.Packet( + packet.ACK, namespace=namespace, id=id, data=data, + binary=binary)) + + async def _handle_ack(self, namespace, id, data): + namespace = namespace or '/' + self.logger.info('Received ack [%s]', namespace) + callback = None + try: + callback = self.callbacks[namespace][id] + except KeyError: + # if we get an unknown callback we just ignore it + self.logger.warning('Unknown callback received, ignoring.') + else: + del self.callbacks[namespace][id] + if callback is not None: + callback(*data) + + def _handle_error(self, namespace, data): + namespace = namespace or '/' + self.logger.info('Connection to namespace {} was rejected'.format( + namespace)) + if namespace in self.namespaces: + self.namespaces.remove(namespace) + + async def _trigger_event(self, event, namespace, *args): + """Invoke an application event handler.""" + # first see if we have an explicit handler for the event + if namespace in self.handlers and event in self.handlers[namespace]: + if asyncio.iscoroutinefunction(self.handlers[namespace][event]) \ + is True: + try: + ret = await self.handlers[namespace][event](*args) + except asyncio.CancelledError: # pragma: no cover + ret = None + else: + ret = self.handlers[namespace][event](*args) + return ret + + # or else, forward the event to a namepsace handler if one exists + elif namespace in self.namespace_handlers: + return await self.namespace_handlers[namespace].trigger_event( + event, *args) + + async def _handle_reconnect(self): + attempt_count = 0 + current_delay = self.reconnection_delay + while True: + delay = current_delay + current_delay *= 2 + if delay > self.reconnection_delay_max: + delay = self.reconnection_delay_max + delay += self.randomization_factor * (2 * random.random() - 1) + self.logger.info( + 'Connection failed, new attempt in {:.02f} seconds'.format( + delay)) + await self.sleep(delay) + attempt_count += 1 + try: + await self.connect(self.connection_url, + headers=self.connection_headers, + transports=self.connection_transports, + socketio_path=self.socketio_path) + except (exceptions.ConnectionError, ValueError): + pass + else: + self.logger.info('Reconnection successful') + self._reconnect_task = None + break + if self.reconnection_attempts and \ + attempt_count >= self.reconnection_attempts: + self.logger.info( + 'Maximum reconnection attempts reached, giving up') + break + + def _handle_eio_connect(self): + """Handle the Engine.IO connection event.""" + self.logger.info('Engine.IO connection established') + + async def _handle_eio_message(self, data): + """Dispatch Engine.IO messages.""" + if self._binary_packet: + pkt = self._binary_packet + if pkt.add_attachment(data): + self._binary_packet = None + if pkt.packet_type == packet.BINARY_EVENT: + await self._handle_event(pkt.namespace, pkt.id, pkt.data) + else: + await self._handle_ack(pkt.namespace, pkt.id, pkt.data) + else: + pkt = packet.Packet(encoded_packet=data) + if pkt.packet_type == packet.CONNECT: + await self._handle_connect(pkt.namespace) + elif pkt.packet_type == packet.DISCONNECT: + await self._handle_disconnect(pkt.namespace) + elif pkt.packet_type == packet.EVENT: + await self._handle_event(pkt.namespace, pkt.id, pkt.data) + elif pkt.packet_type == packet.ACK: + await self._handle_ack(pkt.namespace, pkt.id, pkt.data) + elif pkt.packet_type == packet.BINARY_EVENT or \ + pkt.packet_type == packet.BINARY_ACK: + self._binary_packet = pkt + elif pkt.packet_type == packet.ERROR: + self._handle_error(pkt.namespace, pkt.data) + else: + raise ValueError('Unknown packet type.') + + async def _handle_eio_disconnect(self): + """Handle the Engine.IO disconnection event.""" + self.logger.info('Engine.IO connection dropped') + for n in self.namespaces: + await self._trigger_event('disconnect', namespace=n) + await self._trigger_event('disconnect', namespace='/') + self.callbacks = {} + self._binary_packet = None + if self.eio.state == 'connected' and self.reconnection: + self._reconnect_task = self.start_background_task( + self._handle_reconnect) + + def _engineio_client_class(self): + return engineio.AsyncClient diff --git a/socketio/client.py b/socketio/client.py index 0d21e903..50d500c7 100644 --- a/socketio/client.py +++ b/socketio/client.py @@ -13,7 +13,7 @@ class Client(object): - """An Engine.IO client. + """An Socket.IO client. This class implements a fully compliant Engine.IO web client with support for websocket and long-polling transports. @@ -438,6 +438,7 @@ def _handle_reconnect(self): pass else: self.logger.info('Reconnection successful') + self._reconnect_task = None break if self.reconnection_attempts and \ attempt_count >= self.reconnection_attempts: From 251b5be9dfc5d88a3bbf146ff52197863d5ce109 Mon Sep 17 00:00:00 2001 From: iepngs Date: Mon, 17 Dec 2018 04:29:33 +0800 Subject: [PATCH 041/571] resolve wrong variable and css element (#177) * resolve wrong variable and css element * fixed error variable --- examples/wsgi/templates/latency.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/wsgi/templates/latency.html b/examples/wsgi/templates/latency.html index 56b8c0a6..5df59339 100755 --- a/examples/wsgi/templates/latency.html +++ b/examples/wsgi/templates/latency.html @@ -15,7 +15,7 @@

(connecting)

+ + + diff --git a/examples/server/javascript/fiddle_public/main.js b/examples/server/javascript/fiddle_public/main.js new file mode 100644 index 00000000..3070a03e --- /dev/null +++ b/examples/server/javascript/fiddle_public/main.js @@ -0,0 +1,19 @@ +'use strict'; + +(function() { + + const socket = io(); + + socket.on('connect', () => { + console.log(`connect ${socket.id}`); + }); + + socket.on('disconnect', () => { + console.log(`disconnect ${socket.id}`); + }); + + socket.on('hello', (a, b, c) => { + console.log(a, b, c); + }); + +})(); diff --git a/examples/server/javascript/latency.js b/examples/server/javascript/latency.js new file mode 100644 index 00000000..b374ee6c --- /dev/null +++ b/examples/server/javascript/latency.js @@ -0,0 +1,21 @@ +const express = require('express'); +const app = express(); +const server = require('http').createServer(app); +const io = require('socket.io')(server); +const port = process.env.PORT || 5000; + +app.use(express.static(__dirname + '/latency_public')); + +io.on('connection', socket => { + console.log(`connect ${socket.id}`); + + socket.on('ping_from_client', () => { + socket.emit('pong_from_server'); + }); + + socket.on('disconnect', () => { + console.log(`disconnect ${socket.id}`); + }); +}); + +server.listen(port, () => console.log(`server listening on port ${port}`)); diff --git a/examples/server/javascript/latency_public/index.html b/examples/server/javascript/latency_public/index.html new file mode 100644 index 00000000..c862c898 --- /dev/null +++ b/examples/server/javascript/latency_public/index.html @@ -0,0 +1,16 @@ + + + + Socket.IO Latency + + + +

Socket.IO Latency

+

(connecting)

+ + + + + + + diff --git a/examples/server/javascript/latency_public/index.js b/examples/server/javascript/latency_public/index.js new file mode 100644 index 00000000..a8975663 --- /dev/null +++ b/examples/server/javascript/latency_public/index.js @@ -0,0 +1,48 @@ +// helper +function $ (id) { return document.getElementById(id); } + +// chart +let smoothie; +let time; + +function render () { + if (smoothie) smoothie.stop(); + $('chart').width = document.body.clientWidth; + smoothie = new SmoothieChart(); + smoothie.streamTo($('chart'), 1000); + time = new TimeSeries(); + smoothie.addTimeSeries(time, { + strokeStyle: 'rgb(255, 0, 0)', + fillStyle: 'rgba(255, 0, 0, 0.4)', + lineWidth: 2 + }); +} + +// socket +const socket = io(); +let last; +function send () { + last = new Date(); + socket.emit('ping_from_client'); + $('transport').innerHTML = socket.io.engine.transport.name; +} + +socket.on('connect', () => { + if ($('chart').getContext) { + render(); + window.onresize = render; + } + send(); +}); + +socket.on('disconnect', () => { + if (smoothie) smoothie.stop(); + $('transport').innerHTML = '(disconnected)'; +}); + +socket.on('pong_from_server', () => { + const latency = new Date() - last; + $('latency').innerHTML = latency + 'ms'; + if (time) time.append(+new Date(), latency); + setTimeout(send, 100); +}); diff --git a/examples/server/javascript/latency_public/style.css b/examples/server/javascript/latency_public/style.css new file mode 100644 index 00000000..d20bcad9 --- /dev/null +++ b/examples/server/javascript/latency_public/style.css @@ -0,0 +1,4 @@ +body { margin: 0; padding: 0; font-family: Helvetica Neue; } +h1 { margin: 100px 100px 10px; } +h2 { color: #999; margin: 0 100px 30px; font-weight: normal; } +#latency { color: red; } diff --git a/examples/server/javascript/package-lock.json b/examples/server/javascript/package-lock.json new file mode 100644 index 00000000..d2ec5000 --- /dev/null +++ b/examples/server/javascript/package-lock.json @@ -0,0 +1,1680 @@ +{ + "name": "socketio-examples", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/component-emitter": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", + "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" + }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "requires": { + "@types/node": "*" + } + }, + "@types/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==" + }, + "@types/cors": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.8.tgz", + "integrity": "sha512-fO3gf3DxU2Trcbr75O7obVndW/X5k8rJNZkLXlQWStTHhP71PkRqjwPIEI0yMnJdg9R9OasjU+Bsr+Hr1xy/0w==", + "requires": { + "@types/express": "*" + } + }, + "@types/express": { + "version": "4.17.9", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.9.tgz", + "integrity": "sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.14.tgz", + "integrity": "sha512-uFTLwu94TfUFMToXNgRZikwPuZdOtDgs3syBtAIr/OXorL1kJqUJT9qCLnRZ5KBOWfZQikQ2xKgR2tnDj1OgDA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/mime": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" + }, + "@types/node": { + "version": "14.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.10.tgz", + "integrity": "sha512-J32dgx2hw8vXrSbu4ZlVhn1Nm3GbeCFNw2FWL8S5QKucHGY0cyNwjdQdO+KMBZ4wpmC7KhLCiNsdk1RFRIYUQQ==" + }, + "@types/qs": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", + "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz", + "integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==", + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "Base64": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/Base64/-/Base64-0.2.1.tgz", + "integrity": "sha1-ujpCMHCOGGcFBl5mur3Uw1z2ACg=" + }, + "JSONStream": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.7.4.tgz", + "integrity": "sha1-c0KQ5BUR7qfCz+FR+/mlY6l7l4Y=", + "requires": { + "jsonparse": "0.0.5", + "through": ">=2.2.7 <3" + } + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "assert": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.1.2.tgz", + "integrity": "sha1-raoExGu1jG3R8pTaPrJuYijrbkQ=", + "requires": { + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "astw": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/astw/-/astw-2.2.0.tgz", + "integrity": "sha1-e9QXhNMkk5h66yOba04cV6hzuRc=", + "requires": { + "acorn": "^4.0.3" + } + }, + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, + "base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" + }, + "base64-js": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.7.tgz", + "integrity": "sha1-VEANyR1pbOwyqKR5AvlxUi/uj0g=" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "browser-pack": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-2.0.1.tgz", + "integrity": "sha1-XRxSf1bFgmd0EcTbKhKGSP9r8VA=", + "requires": { + "JSONStream": "~0.6.4", + "combine-source-map": "~0.3.0", + "through": "~2.3.4" + }, + "dependencies": { + "JSONStream": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.6.4.tgz", + "integrity": "sha1-SyyAY/j1Enh7I3X37p22kgj6Lcs=", + "requires": { + "jsonparse": "0.0.5", + "through": "~2.2.7" + }, + "dependencies": { + "through": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/through/-/through-2.2.7.tgz", + "integrity": "sha1-bo4hIAGR1OtqmfbwEN9Gqhxusr0=" + } + } + } + } + }, + "browser-resolve": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.2.4.tgz", + "integrity": "sha1-Wa54IKgpVezTL1+3xGisIcRyOAY=", + "requires": { + "resolve": "0.6.3" + }, + "dependencies": { + "resolve": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", + "integrity": "sha1-3ZV5gufnNt699TtYpN2RdUV13UY=" + } + } + }, + "browserify": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-4.1.9.tgz", + "integrity": "sha1-xKapETse1HgSNQQxTWVunkZpJps=", + "requires": { + "JSONStream": "~0.7.1", + "assert": "~1.1.0", + "browser-pack": "~2.0.0", + "browser-resolve": "~1.2.1", + "browserify-zlib": "~0.1.2", + "buffer": "^2.3.0", + "builtins": "~0.0.3", + "commondir": "0.0.1", + "concat-stream": "~1.4.1", + "console-browserify": "~1.0.1", + "constants-browserify": "~0.0.1", + "crypto-browserify": "~1.0.9", + "deep-equal": "~0.1.0", + "defined": "~0.0.0", + "deps-sort": "~0.1.1", + "derequire": "~0.8.0", + "domain-browser": "~1.1.0", + "duplexer": "~0.1.1", + "events": "~1.0.0", + "glob": "~3.2.8", + "http-browserify": "~1.3.1", + "https-browserify": "~0.0.0", + "inherits": "~2.0.1", + "insert-module-globals": "~6.0.0", + "module-deps": "~2.1.1", + "os-browserify": "~0.1.1", + "parents": "~0.0.1", + "path-browserify": "~0.0.0", + "process": "^0.7.0", + "punycode": "~1.2.3", + "querystring-es3": "~0.2.0", + "readable-stream": "^1.0.27-1", + "resolve": "~0.7.1", + "shallow-copy": "0.0.1", + "shell-quote": "~0.0.1", + "stream-browserify": "^1.0.0", + "stream-combiner": "~0.0.2", + "string_decoder": "~0.0.0", + "subarg": "0.0.1", + "syntax-error": "~1.1.0", + "through2": "~0.4.1", + "timers-browserify": "~1.0.1", + "tty-browserify": "~0.0.0", + "umd": "~2.1.0", + "url": "~0.10.1", + "util": "~0.10.1", + "vm-browserify": "~0.0.1", + "xtend": "^3.0.0" + } + }, + "browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "requires": { + "pako": "~0.2.0" + } + }, + "buffer": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-2.8.3.tgz", + "integrity": "sha512-dyatqxbSWlkhnG5lthQ7TDh2NfShsKesnKiGyt5DmiJfvKJ1zBq1AvC3+neSY565BziAiYwbothV2tizAr2WRg==", + "requires": { + "base64-js": "0.0.7", + "ieee754": "^1.1.4", + "is-array": "^1.0.1" + } + }, + "builtins": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-0.0.7.tgz", + "integrity": "sha1-NVIZzWzxjb58Acx/0tznZc/cVJo=" + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "combine-source-map": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.3.0.tgz", + "integrity": "sha1-2edPWT2c1DgHMSy12EbUUe+qnrc=", + "requires": { + "convert-source-map": "~0.3.0", + "inline-source-map": "~0.3.0", + "source-map": "~0.1.31" + }, + "dependencies": { + "convert-source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz", + "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=" + } + } + }, + "commondir": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-0.0.1.tgz", + "integrity": "sha1-ifAP3NUbUZxXhzP+xWPmptp/W+I=" + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "concat-stream": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.4.11.tgz", + "integrity": "sha512-X3JMh8+4je3U1cQpG87+f9lXHDrqcb2MVLg9L7o8b1UZ0DzhRrUpdn65ttzu10PpJPPI3MQNkis+oha6TSA9Mw==", + "requires": { + "inherits": "~2.0.1", + "readable-stream": "~1.1.9", + "typedarray": "~0.0.5" + } + }, + "console-browserify": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.0.3.tgz", + "integrity": "sha1-04mNLDqTEC82QZf4h0tPkrUoao4=" + }, + "constants-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-0.0.1.tgz", + "integrity": "sha1-kld9tSe6bEzwpFaNhLwDH0QeIfI=" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.1.tgz", + "integrity": "sha1-dOUYJHMFhBOwkN1zd3rLxKD/88w=" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "crypto-browserify": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz", + "integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA=" + }, + "debounce": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz", + "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "deep-equal": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.1.2.tgz", + "integrity": "sha1-skbCuApXCkfBG+HZvRBw7IeLh84=" + }, + "defined": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-0.0.0.tgz", + "integrity": "sha1-817qfXBekzuvE7LwOz+D2SFAOz4=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "deps-sort": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-0.1.2.tgz", + "integrity": "sha1-2qL7YUoXyWN9gB4vVTOa43DzYRo=", + "requires": { + "JSONStream": "~0.6.4", + "minimist": "~0.0.1", + "through": "~2.3.4" + }, + "dependencies": { + "JSONStream": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.6.4.tgz", + "integrity": "sha1-SyyAY/j1Enh7I3X37p22kgj6Lcs=", + "requires": { + "jsonparse": "0.0.5", + "through": "~2.2.7" + }, + "dependencies": { + "through": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/through/-/through-2.2.7.tgz", + "integrity": "sha1-bo4hIAGR1OtqmfbwEN9Gqhxusr0=" + } + } + } + } + }, + "derequire": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/derequire/-/derequire-0.8.0.tgz", + "integrity": "sha1-wffx2izt5Ere3gRzePA/RE6cTA0=", + "requires": { + "esprima-fb": "^3001.1.0-dev-harmony-fb", + "esrefactor": "~0.1.0", + "estraverse": "~1.5.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detective": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-3.1.0.tgz", + "integrity": "sha1-d3gkRKt1K4jKG+Lp0KA5Xx2iXu0=", + "requires": { + "escodegen": "~1.1.0", + "esprima-fb": "3001.1.0-dev-harmony-fb" + } + }, + "domain-browser": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=" + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + }, + "duplexer2": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "requires": { + "readable-stream": "~1.1.9" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "enchilada": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/enchilada/-/enchilada-0.13.0.tgz", + "integrity": "sha1-Sp5ZqJCwWN0qJ3ukozv763xOwQg=", + "requires": { + "browserify": "4.1.9", + "convert-source-map": "1.1.1", + "debug": "2.2.0", + "filewatcher": "3.0.0", + "mime": "1.2.11", + "ready-signal": "1.3.0", + "uglify-js": "2.5.0" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "requires": { + "ms": "0.7.1" + } + }, + "mime": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=" + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" + } + } + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "engine.io": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.0.4.tgz", + "integrity": "sha512-4ggUX5pICZU17OTZNFv5+uFE/ZyoK+TIXv2SvxWWX8lwStllQ6Lvvs4lDBqvKpV9EYXNcvlNOcjKChd/mo+8Tw==", + "requires": { + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.1.0", + "engine.io-parser": "~4.0.0", + "ws": "^7.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "engine.io-client": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-4.0.4.tgz", + "integrity": "sha512-and4JRvjv+BQ4WBLopYUFePxju3ms3aBRk0XjaLdh/t9TKv2LCKtKKWFRoRzIfUZsu3U38FcYqNLuXhfS16vqw==", + "requires": { + "base64-arraybuffer": "0.1.4", + "component-emitter": "~1.3.0", + "debug": "~4.1.0", + "engine.io-parser": "~4.0.1", + "has-cors": "1.1.0", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~7.2.1", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "ws": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.5.tgz", + "integrity": "sha512-C34cIU4+DB2vMyAbmEKossWq2ZQDr6QEyuuCzWrM9zfw1sGc0mYiJ0UnG9zzNykt49C2Fi34hvr2vssFQRS6EA==" + } + } + }, + "engine.io-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.1.tgz", + "integrity": "sha512-v5aZK1hlckcJDGmHz3W8xvI3NUHYc9t8QtTbqdR5OaH3S9iJZilPubauOm+vLWOMMWzpE3hiq92l9lTAHamRCg==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escodegen": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.1.0.tgz", + "integrity": "sha1-xmOSP24gqtSNDA+knzHG1PSTYM8=", + "requires": { + "esprima": "~1.0.4", + "estraverse": "~1.5.0", + "esutils": "~1.0.0", + "source-map": "~0.1.30" + }, + "dependencies": { + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=" + } + } + }, + "escope": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/escope/-/escope-0.0.16.tgz", + "integrity": "sha1-QYx6CvynIdr+ZZGT/Zhig+dGU48=", + "requires": { + "estraverse": ">= 0.0.2" + } + }, + "esprima-fb": { + "version": "3001.1.0-dev-harmony-fb", + "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz", + "integrity": "sha1-t303q8046gt3Qmu4vCkizmtCZBE=" + }, + "esrefactor": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/esrefactor/-/esrefactor-0.1.0.tgz", + "integrity": "sha1-0UJ5WigjOauB6Ta1t6IbEb8ZexM=", + "requires": { + "escope": "~0.0.13", + "esprima": "~1.0.2", + "estraverse": "~0.0.4" + }, + "dependencies": { + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=" + }, + "estraverse": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-0.0.4.tgz", + "integrity": "sha1-AaCTLf7ldGhKWYr1pnw7+bZCjbI=" + } + } + }, + "estraverse": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", + "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=" + }, + "esutils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", + "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "events": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/events/-/events-1.0.2.tgz", + "integrity": "sha1-dYSdz+k9EPsFfDAFWv29UdBqjiQ=" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "filewatcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filewatcher/-/filewatcher-3.0.0.tgz", + "integrity": "sha1-/HYcHLkG9xWHppu8G1vTiQxQ0P8=", + "requires": { + "debounce": "^1.0.0" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "requires": { + "inherits": "2", + "minimatch": "0.3" + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, + "http-browserify": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/http-browserify/-/http-browserify-1.3.2.tgz", + "integrity": "sha1-tWLDRHk0mmkNemWX30la76jGBPU=", + "requires": { + "Base64": "~0.2.0", + "inherits": "~2.0.1" + } + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "https-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", + "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=" + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "inline-source-map": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.3.1.tgz", + "integrity": "sha1-pSi1FOaJ/OkNswiehw2S9Sestes=", + "requires": { + "source-map": "~0.3.0" + }, + "dependencies": { + "source-map": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.3.0.tgz", + "integrity": "sha1-hYb7mloAXltQHiHNGLbyG0V60fk=", + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "insert-module-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-6.0.0.tgz", + "integrity": "sha1-7orrne4WgZ4zqhRYilWIJK8MFdw=", + "requires": { + "JSONStream": "~0.7.1", + "concat-stream": "~1.4.1", + "lexical-scope": "~1.1.0", + "process": "~0.6.0", + "through": "~2.3.4", + "xtend": "^3.0.0" + }, + "dependencies": { + "process": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/process/-/process-0.6.0.tgz", + "integrity": "sha1-fdm+gP+q7dTLYo8YJ/HLq23AkY8=" + } + } + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-array": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-array/-/is-array-1.0.1.tgz", + "integrity": "sha1-6YUMwsyGDDvAl36EzPDdRkWEJ5o=" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "jsonparse": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz", + "integrity": "sha1-MwVCrT8KZUZlt3jz6y2an6UHrGQ=" + }, + "lexical-scope": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/lexical-scope/-/lexical-scope-1.1.1.tgz", + "integrity": "sha1-3rrBBnQ18TWdkPz9npS8su5Hsr8=", + "requires": { + "astw": "^2.0.0" + } + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + }, + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + }, + "module-deps": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-2.1.5.tgz", + "integrity": "sha1-N1qbyATM1kzrs8Yu5kN1Xws8zyk=", + "requires": { + "JSONStream": "~0.7.1", + "browser-resolve": "~1.2.4", + "concat-stream": "~1.4.5", + "detective": "~3.1.0", + "duplexer2": "0.0.2", + "inherits": "~2.0.1", + "minimist": "~0.0.9", + "parents": "0.0.2", + "readable-stream": "^1.0.27-1", + "resolve": "~0.6.3", + "stream-combiner": "~0.1.0", + "subarg": "0.0.1", + "through2": "~0.4.1" + }, + "dependencies": { + "parents": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/parents/-/parents-0.0.2.tgz", + "integrity": "sha1-ZxR4JuSX1AdZqvW6TJllm2A00wI=" + }, + "resolve": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", + "integrity": "sha1-3ZV5gufnNt699TtYpN2RdUV13UY=" + }, + "stream-combiner": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.1.0.tgz", + "integrity": "sha1-DcOJo8ID+PTVY2j5Xd5S65Jptb4=", + "requires": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "optimist": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", + "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", + "requires": { + "wordwrap": "~0.0.2" + } + }, + "os-browserify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.1.2.tgz", + "integrity": "sha1-ScoCk+CxlZCl9d4Qx/JlphfY/lQ=" + }, + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" + }, + "parents": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/parents/-/parents-0.0.3.tgz", + "integrity": "sha1-+iEvAk2fpjGNu2tM5nbIvkk7nEM=", + "requires": { + "path-platform": "^0.0.1" + } + }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" + }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==" + }, + "path-platform": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.0.1.tgz", + "integrity": "sha1-tVhdfDxGPYmqAGDYZhHPGv1hfio=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "process": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/process/-/process-0.7.0.tgz", + "integrity": "sha1-xSIIFho0rfOBI0SuhdPmFQRpOJ0=" + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "punycode": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.2.4.tgz", + "integrity": "sha1-VACKyXKux0F13vnLpt9/qdORh0A=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + }, + "dependencies": { + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "ready-signal": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ready-signal/-/ready-signal-1.3.0.tgz", + "integrity": "sha1-VUA/kg4eHyqLBloii5pj77Cjwts=" + }, + "resolve": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.7.4.tgz", + "integrity": "sha1-OVqe+ehz+/4SvRRAi9kbuTYAPWk=" + }, + "rfile": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rfile/-/rfile-1.0.0.tgz", + "integrity": "sha1-WXCM+Qyh50xUw8/Fw2/bmBBDUmE=", + "requires": { + "callsite": "~1.0.0", + "resolve": "~0.3.0" + }, + "dependencies": { + "resolve": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.3.1.tgz", + "integrity": "sha1-NMY0R8ZkxwWY0cmxJvxDsqJDEKQ=" + } + } + }, + "ruglify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ruglify/-/ruglify-1.0.0.tgz", + "integrity": "sha1-3Ikw4qlUSidDAcyZcldMDQmGtnU=", + "requires": { + "rfile": "~1.0", + "uglify-js": "~2.2" + }, + "dependencies": { + "uglify-js": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.2.5.tgz", + "integrity": "sha1-puAqcNg5eSuXgEiLe4sYTAlcmcc=", + "requires": { + "optimist": "~0.3.5", + "source-map": "~0.1.7" + } + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "shallow-copy": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", + "integrity": "sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=" + }, + "shell-quote": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-0.0.1.tgz", + "integrity": "sha1-GkEZbzwDM8SCMjWT1ohuzxU92YY=" + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" + }, + "smoothie": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/smoothie/-/smoothie-1.19.0.tgz", + "integrity": "sha1-kVZqT9PFES3CrJWAAQT6Ruz7XBA=" + }, + "socket.io": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.0.3.tgz", + "integrity": "sha512-TC1GnSXhDVmd3bHji5aG7AgWB8UL7E6quACbKra8uFXBqlMwEDbrJFK+tjuIY5Pe9N0L+MAPPDv3pycnn0000A==", + "requires": { + "@types/cookie": "^0.4.0", + "@types/cors": "^2.8.8", + "@types/node": "^14.14.7", + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.1.0", + "engine.io": "~4.0.0", + "socket.io-adapter": "~2.0.3", + "socket.io-parser": "~4.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-adapter": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.0.3.tgz", + "integrity": "sha512-2wo4EXgxOGSFueqvHAdnmi5JLZzWqMArjuP4nqC26AtLh5PoCPsaRbRdah2xhcwTAMooZfjYiNVNkkmmSMaxOQ==" + }, + "socket.io-client": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-3.0.3.tgz", + "integrity": "sha512-kwCJAKb6JMqE9ZYXg78Dgt8rYLSwtJ/g/LJqpb/pOTFRZMSr1cKAsCaisHZ+IBwKHBY7DYOOkjtkHqseY3ZLpw==", + "requires": { + "@types/component-emitter": "^1.2.10", + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "~1.3.0", + "debug": "~4.1.0", + "engine.io-client": "~4.0.0", + "parseuri": "0.0.6", + "socket.io-parser": "~4.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.2.tgz", + "integrity": "sha512-Bs3IYHDivwf+bAAuW/8xwJgIiBNtlvnjYRc4PbXgniLmcP1BrakBoq/QhO24rgtgW7VZ7uAaswRGxutUnlAK7g==", + "requires": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.1.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "requires": { + "amdefine": ">=0.0.4" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "stream-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-1.0.0.tgz", + "integrity": "sha1-v5tKv7QrJ011FHnkTg/yZWtvEZM=", + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^1.0.27-1" + } + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "requires": { + "duplexer": "~0.1.1" + } + }, + "string_decoder": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.0.1.tgz", + "integrity": "sha1-9UctCo0WUOyCN1LSTm/WJ7Ob8UE=" + }, + "subarg": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-0.0.1.tgz", + "integrity": "sha1-PVawfaz7xFu7Y/dnK0O2PkY2jjo=", + "requires": { + "minimist": "~0.0.7" + } + }, + "syntax-error": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.1.6.tgz", + "integrity": "sha1-tFSXBtOGzBwdx8JCPxhXm2yt5xA=", + "requires": { + "acorn": "^2.7.0" + }, + "dependencies": { + "acorn": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=" + } + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=", + "requires": { + "readable-stream": "~1.0.17", + "xtend": "~2.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "requires": { + "object-keys": "~0.4.0" + } + } + } + }, + "timers-browserify": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.0.3.tgz", + "integrity": "sha1-/7pwycEu7ZFv1nMY5imsbzIpVVE=", + "requires": { + "process": "~0.5.1" + }, + "dependencies": { + "process": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", + "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" + } + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "uglify-js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.5.0.tgz", + "integrity": "sha1-SrXWWkcw7Lek+2LT9JniBU2Y+6E=", + "requires": { + "async": "~0.2.6", + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.5.4" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=" + }, + "umd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/umd/-/umd-2.1.0.tgz", + "integrity": "sha1-SmMHt2LxfwLSAbX6FU5nM5bCY88=", + "requires": { + "rfile": "~1.0.0", + "ruglify": "~1.0.0", + "through": "~2.3.4", + "uglify-js": "~2.4.0" + }, + "dependencies": { + "source-map": { + "version": "0.1.34", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", + "integrity": "sha1-p8/omux7FoLDsZjQrPtH19CQVms=", + "requires": { + "amdefine": ">=0.0.4" + } + }, + "uglify-js": { + "version": "2.4.24", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", + "integrity": "sha1-+tV1XB4Vd2WLsG/5q25UjJW+vW4=", + "requires": { + "async": "~0.2.6", + "source-map": "0.1.34", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.5.4" + } + } + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "requires": { + "inherits": "2.0.3" + } + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "requires": { + "indexof": "0.0.1" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + }, + "ws": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz", + "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==" + }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, + "xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=" + }, + "yargs": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", + "integrity": "sha1-2K/49mXpTDS9JZvevRv68N3TU2E=", + "requires": { + "camelcase": "^1.0.2", + "decamelize": "^1.0.0", + "window-size": "0.1.0", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + } + } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + } + } +} diff --git a/examples/server/javascript/package.json b/examples/server/javascript/package.json new file mode 100644 index 00000000..c1e2c3bf --- /dev/null +++ b/examples/server/javascript/package.json @@ -0,0 +1,10 @@ +{ + "name": "socketio-examples", + "version": "0.1.0", + "dependencies": { + "socket.io": "^3.0.0", + "socket.io-client": "^3.0.0", + "express": "^4.17.1", + "smoothie": "1.19.0" + } +} From 49822e6919d3de9d52f6dde32c7f04ad62d73990 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 2 Dec 2020 08:46:25 +0000 Subject: [PATCH 183/571] v5 protocol: do not connect the default namespace unless requested explicitly --- .../{latency_client.js => latency-client.js} | 0 socketio/asyncio_client.py | 45 ++++++-------- socketio/asyncio_server.py | 1 - socketio/client.py | 60 +++++++++++-------- socketio/server.py | 1 - 5 files changed, 53 insertions(+), 54 deletions(-) rename examples/client/javascript/{latency_client.js => latency-client.js} (100%) diff --git a/examples/client/javascript/latency_client.js b/examples/client/javascript/latency-client.js similarity index 100% rename from examples/client/javascript/latency_client.js rename to examples/client/javascript/latency-client.js diff --git a/socketio/asyncio_client.py b/socketio/asyncio_client.py index 85ea3c24..615c7c07 100644 --- a/socketio/asyncio_client.py +++ b/socketio/asyncio_client.py @@ -71,16 +71,19 @@ async def connect(self, url, headers={}, transports=None, are ``'polling'`` and ``'websocket'``. If not given, the polling transport is connected first, then an upgrade to websocket is attempted. - :param namespaces: The list of custom namespaces to connect, in - addition to the default namespace. If not given, - the namespace list is obtained from the registered - event handlers. + :param namespaces: The namespaces to connect as a string or list of + strings. If not given, the namespaces that have + registered event handlers are connected. :param socketio_path: The endpoint where the Socket.IO server is installed. The default value is appropriate for most cases. Note: this method is a coroutine. + Note: The connection mechannism occurs in the background and will + complete at some point after this function returns. The connection + will be established when the ``connect`` event is invoked. + Example usage:: sio = socketio.AsyncClient() @@ -97,8 +100,7 @@ async def connect(self, url, headers={}, transports=None, set(self.namespace_handlers.keys())) elif isinstance(namespaces, six.string_types): namespaces = [namespaces] - self.connection_namespaces = namespaces - self.namespaces = [n for n in namespaces if n != '/'] + self.connection_namespaces = namespaces try: await self.eio.connect(url, headers=headers, transports=transports, @@ -154,7 +156,7 @@ async def emit(self, event, data=None, namespace=None, callback=None): Note 2: this method is a coroutine. """ namespace = namespace or '/' - if namespace != '/' and namespace not in self.namespaces: + if namespace not in self.namespaces: raise exceptions.BadNamespaceError( namespace + ' is not a connected namespace.') self.logger.info('Emitting event "%s" [%s]', event, namespace) @@ -248,8 +250,6 @@ async def disconnect(self): for n in self.namespaces: await self._send_packet(packet.Packet(packet.DISCONNECT, namespace=n)) - await self._send_packet(packet.Packet( - packet.DISCONNECT, namespace='/')) self.connected = False await self.eio.disconnect(abort=True) @@ -291,30 +291,22 @@ async def _send_packet(self, pkt): else: await self.eio.send(encoded_packet) - async def _handle_connect(self, namespace): + async def _handle_connect(self, namespace, data): namespace = namespace or '/' self.logger.info('Namespace {} is connected'.format(namespace)) + if namespace not in self.namespaces: + self.namespaces[namespace] = (data or {}).get('sid', self.sid) await self._trigger_event('connect', namespace=namespace) - if namespace == '/': - for n in self.namespaces: - await self._send_packet(packet.Packet(packet.CONNECT, - namespace=n)) - elif namespace not in self.namespaces: - self.namespaces.append(namespace) async def _handle_disconnect(self, namespace): if not self.connected: return namespace = namespace or '/' - if namespace == '/': - for n in self.namespaces: - await self._trigger_event('disconnect', namespace=n) - self.namespaces = [] await self._trigger_event('disconnect', namespace=namespace) if namespace in self.namespaces: - self.namespaces.remove(namespace) - if namespace == '/': - self.connected = False + del self.namespaces[namespace] + if not self.namespaces: + await self.eio.disconnect(abort=True) async def _handle_event(self, namespace, id, data): namespace = namespace or '/' @@ -422,10 +414,12 @@ async def _handle_reconnect(self): break client.reconnecting_clients.remove(self) - def _handle_eio_connect(self): + async def _handle_eio_connect(self): """Handle the Engine.IO connection event.""" self.logger.info('Engine.IO connection established') self.sid = self.eio.sid + for n in self.connection_namespaces: + await self._send_packet(packet.Packet(packet.CONNECT, namespace=n)) async def _handle_eio_message(self, data): """Dispatch Engine.IO messages.""" @@ -440,7 +434,7 @@ async def _handle_eio_message(self, data): else: pkt = packet.Packet(encoded_packet=data) if pkt.packet_type == packet.CONNECT: - await self._handle_connect(pkt.namespace) + await self._handle_connect(pkt.namespace, pkt.data) elif pkt.packet_type == packet.DISCONNECT: await self._handle_disconnect(pkt.namespace) elif pkt.packet_type == packet.EVENT: @@ -462,7 +456,6 @@ async def _handle_eio_disconnect(self): if self.connected: for n in self.namespaces: await self._trigger_event('disconnect', namespace=n) - await self._trigger_event('disconnect', namespace='/') self.namespaces = [] self.connected = False self.callbacks = {} diff --git a/socketio/asyncio_server.py b/socketio/asyncio_server.py index 7a7e324d..2cf5d4c3 100644 --- a/socketio/asyncio_server.py +++ b/socketio/asyncio_server.py @@ -515,7 +515,6 @@ async def _handle_eio_connect(self, sid, environ): self.manager_initialized = True self.manager.initialize() self.environ[sid] = environ - return await self._handle_connect(sid, '/') async def _handle_eio_message(self, sid, data): """Dispatch Engine.IO messages.""" diff --git a/socketio/client.py b/socketio/client.py index 7b263586..5873a5b0 100644 --- a/socketio/client.py +++ b/socketio/client.py @@ -124,7 +124,7 @@ def __init__(self, reconnection=True, reconnection_attempts=0, self.sid = None self.connected = False - self.namespaces = [] + self.namespaces = {} self.handlers = {} self.namespace_handlers = {} self.callbacks = {} @@ -242,14 +242,17 @@ def connect(self, url, headers={}, transports=None, are ``'polling'`` and ``'websocket'``. If not given, the polling transport is connected first, then an upgrade to websocket is attempted. - :param namespaces: The list of custom namespaces to connect, in - addition to the default namespace. If not given, - the namespace list is obtained from the registered - event handlers. + :param namespaces: The namespaces to connect as a string or list of + strings. If not given, the namespaces that have + registered event handlers are connected. :param socketio_path: The endpoint where the Socket.IO server is installed. The default value is appropriate for most cases. + Note: The connection mechannism occurs in the background and will + complete at some point after this function returns. The connection + will be established when the ``connect`` event is invoked. + Example usage:: sio = socketio.Client() @@ -266,8 +269,7 @@ def connect(self, url, headers={}, transports=None, set(self.namespace_handlers.keys())) elif isinstance(namespaces, six.string_types): namespaces = [namespaces] - self.connection_namespaces = namespaces - self.namespaces = [n for n in namespaces if n != '/'] + self.connection_namespaces = namespaces try: self.eio.connect(url, headers=headers, transports=transports, engineio_path=socketio_path) @@ -318,7 +320,7 @@ def emit(self, event, data=None, namespace=None, callback=None): situation. """ namespace = namespace or '/' - if namespace != '/' and namespace not in self.namespaces: + if namespace not in self.namespaces: raise exceptions.BadNamespaceError( namespace + ' is not a connected namespace.') self.logger.info('Emitting event "%s" [%s]', event, namespace) @@ -402,11 +404,23 @@ def disconnect(self): # later in _handle_eio_disconnect we invoke the disconnect handler for n in self.namespaces: self._send_packet(packet.Packet(packet.DISCONNECT, namespace=n)) - self._send_packet(packet.Packet( - packet.DISCONNECT, namespace='/')) self.connected = False self.eio.disconnect(abort=True) + def get_sid(self, namespace=None): + """Return the ``sid`` associated with a connection. + + :param namespace: The Socket.IO namespace. If this argument is omitted + the handler is associated with the default + namespace. Note that unlike previous versions, the + current version of the Socket.IO protocol uses + different ``sid`` values per namespace. + + This method returns the ``sid`` for the requested namespace as a + string. + """ + return self.namespaces.get(namespace or '/') + def transport(self): """Return the name of the transport used by the client. @@ -460,29 +474,22 @@ def _generate_ack_id(self, namespace, callback): self.callbacks[namespace][id] = callback return id - def _handle_connect(self, namespace): + def _handle_connect(self, namespace, data): namespace = namespace or '/' self.logger.info('Namespace {} is connected'.format(namespace)) + if namespace not in self.namespaces: + self.namespaces[namespace] = (data or {}).get('sid', self.sid) self._trigger_event('connect', namespace=namespace) - if namespace == '/': - for n in self.namespaces: - self._send_packet(packet.Packet(packet.CONNECT, namespace=n)) - elif namespace not in self.namespaces: - self.namespaces.append(namespace) def _handle_disconnect(self, namespace): if not self.connected: return namespace = namespace or '/' - if namespace == '/': - for n in self.namespaces: - self._trigger_event('disconnect', namespace=n) - self.namespaces = [] self._trigger_event('disconnect', namespace=namespace) if namespace in self.namespaces: - self.namespaces.remove(namespace) - if namespace == '/': - self.connected = False + del self.namespaces[namespace] + if not self.namespaces: + self.eio.disconnect(abort=True) def _handle_event(self, namespace, id, data): namespace = namespace or '/' @@ -524,7 +531,7 @@ def _handle_error(self, namespace, data): data = (data,) self._trigger_event('connect_error', namespace, *data) if namespace in self.namespaces: - self.namespaces.remove(namespace) + del self.namespaces[namespace] if namespace == '/': self.namespaces = [] self.connected = False @@ -581,6 +588,8 @@ def _handle_eio_connect(self): """Handle the Engine.IO connection event.""" self.logger.info('Engine.IO connection established') self.sid = self.eio.sid + for n in self.connection_namespaces: + self._send_packet(packet.Packet(packet.CONNECT, namespace=n)) def _handle_eio_message(self, data): """Dispatch Engine.IO messages.""" @@ -595,7 +604,7 @@ def _handle_eio_message(self, data): else: pkt = packet.Packet(encoded_packet=data) if pkt.packet_type == packet.CONNECT: - self._handle_connect(pkt.namespace) + self._handle_connect(pkt.namespace, pkt.data) elif pkt.packet_type == packet.DISCONNECT: self._handle_disconnect(pkt.namespace) elif pkt.packet_type == packet.EVENT: @@ -616,7 +625,6 @@ def _handle_eio_disconnect(self): if self.connected: for n in self.namespaces: self._trigger_event('disconnect', namespace=n) - self._trigger_event('disconnect', namespace='/') self.namespaces = [] self.connected = False self.callbacks = {} diff --git a/socketio/server.py b/socketio/server.py index eb8ae420..d6bc7e35 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -705,7 +705,6 @@ def _handle_eio_connect(self, sid, environ): self.manager_initialized = True self.manager.initialize() self.environ[sid] = environ - return self._handle_connect(sid, '/') def _handle_eio_message(self, sid, data): """Dispatch Engine.IO messages.""" From 308b0c8eeb71e1fead35d19088a3291a15ccd50a Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Fri, 4 Dec 2020 00:05:38 +0000 Subject: [PATCH 184/571] v5 protocol: handle per-namespace sids in base manager --- setup.py | 1 + socketio/asyncio_client.py | 4 +- socketio/asyncio_manager.py | 4 +- socketio/asyncio_server.py | 114 +++++++++---------- socketio/base_manager.py | 36 +++--- socketio/client.py | 4 +- socketio/server.py | 113 +++++++++--------- tests/common/test_base_manager.py | 183 +++++++++++++++--------------- 8 files changed, 229 insertions(+), 230 deletions(-) diff --git a/setup.py b/setup.py index f0d2f26c..da83f00e 100755 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ platforms='any', install_requires=[ 'six>=1.9.0', + 'bidict>=0.21.0', 'python-engineio>=3.13.0,<4' ], extras_require={ diff --git a/socketio/asyncio_client.py b/socketio/asyncio_client.py index 615c7c07..029a1ea5 100644 --- a/socketio/asyncio_client.py +++ b/socketio/asyncio_client.py @@ -353,7 +353,7 @@ async def _handle_error(self, namespace, data): if namespace in self.namespaces: self.namespaces.remove(namespace) if namespace == '/': - self.namespaces = [] + self.namespaces = {} self.connected = False async def _trigger_event(self, event, namespace, *args): @@ -456,7 +456,7 @@ async def _handle_eio_disconnect(self): if self.connected: for n in self.namespaces: await self._trigger_event('disconnect', namespace=n) - self.namespaces = [] + self.namespaces = {} self.connected = False self.callbacks = {} self._binary_packet = None diff --git a/socketio/asyncio_manager.py b/socketio/asyncio_manager.py index 55032abc..db9fecf1 100644 --- a/socketio/asyncio_manager.py +++ b/socketio/asyncio_manager.py @@ -20,13 +20,13 @@ async def emit(self, event, data, namespace, room=None, skip_sid=None, tasks = [] if not isinstance(skip_sid, list): skip_sid = [skip_sid] - for sid in self.get_participants(namespace, room): + for sid, eio_sid in self.get_participants(namespace, room): if sid not in skip_sid: if callback is not None: id = self._generate_ack_id(sid, namespace, callback) else: id = None - tasks.append(self.server._emit_internal(sid, event, data, + tasks.append(self.server._emit_internal(eio_sid, event, data, namespace, id)) if tasks == []: # pragma: no cover return diff --git a/socketio/asyncio_server.py b/socketio/asyncio_server.py index 2cf5d4c3..5eaeb3ac 100644 --- a/socketio/asyncio_server.py +++ b/socketio/asyncio_server.py @@ -336,13 +336,11 @@ async def disconnect(self, sid, namespace=None, ignore_queue=False): delete_it = await self.manager.can_disconnect(sid, namespace) if delete_it: self.logger.info('Disconnecting %s [%s]', sid, namespace) - self.manager.pre_disconnect(sid, namespace=namespace) - await self._send_packet(sid, packet.Packet(packet.DISCONNECT, - namespace=namespace)) + eio_sid = self.manager.pre_disconnect(sid, namespace=namespace) + await self._send_packet(eio_sid, packet.Packet( + packet.DISCONNECT, namespace=namespace)) await self._trigger_event('disconnect', namespace, sid) self.manager.disconnect(sid, namespace=namespace) - if namespace == '/': - await self.eio.disconnect(sid) async def handle_request(self, *args, **kwargs): """Handle an HTTP request from the client. @@ -396,26 +394,26 @@ async def _emit_internal(self, sid, event, data, namespace=None, id=None): await self._send_packet(sid, packet.Packet( packet.EVENT, namespace=namespace, data=[event] + data, id=id)) - async def _send_packet(self, sid, pkt): + async def _send_packet(self, eio_sid, pkt): """Send a Socket.IO packet to a client.""" encoded_packet = pkt.encode() if isinstance(encoded_packet, list): for ep in encoded_packet: - await self.eio.send(sid, ep) + await self.eio.send(eio_sid, ep) else: - await self.eio.send(sid, encoded_packet) + await self.eio.send(eio_sid, encoded_packet) - async def _handle_connect(self, sid, namespace): + async def _handle_connect(self, eio_sid, namespace): """Handle a client connection request.""" namespace = namespace or '/' - self.manager.connect(sid, namespace) + sid = self.manager.connect(eio_sid, namespace) if self.always_connect: - await self._send_packet(sid, packet.Packet(packet.CONNECT, - namespace=namespace)) + await self._send_packet(eio_sid, packet.Packet( + packet.CONNECT, {'sid': sid}, namespace=namespace)) fail_reason = None try: success = await self._trigger_event('connect', namespace, sid, - self.environ[sid]) + self.environ[eio_sid]) except exceptions.ConnectionRefusedError as exc: fail_reason = exc.error_args success = False @@ -423,40 +421,34 @@ async def _handle_connect(self, sid, namespace): if success is False: if self.always_connect: self.manager.pre_disconnect(sid, namespace) - await self._send_packet(sid, packet.Packet( + await self._send_packet(eio_sid, packet.Packet( packet.DISCONNECT, data=fail_reason, namespace=namespace)) elif namespace != '/': - await self._send_packet(sid, packet.Packet( + await self._send_packet(eio_sid, packet.Packet( packet.CONNECT_ERROR, data=fail_reason, namespace=namespace)) self.manager.disconnect(sid, namespace) - if namespace == '/' and sid in self.environ: # pragma: no cover - del self.environ[sid] + if namespace == '/' and \ + eio_sid in self.environ: # pragma: no cover + del self.environ[eio_sid] return fail_reason or False elif not self.always_connect: - await self._send_packet(sid, packet.Packet(packet.CONNECT, - namespace=namespace)) + await self._send_packet(eio_sid, packet.Packet( + packet.CONNECT, {'sid': sid}, namespace=namespace)) - async def _handle_disconnect(self, sid, namespace): + async def _handle_disconnect(self, eio_sid, namespace): """Handle a client disconnect.""" namespace = namespace or '/' - if namespace == '/': - namespace_list = list(self.manager.get_namespaces()) - else: - namespace_list = [namespace] - for n in namespace_list: - if n != '/' and self.manager.is_connected(sid, n): - self.manager.pre_disconnect(sid, namespace=n) - await self._trigger_event('disconnect', n, sid) - self.manager.disconnect(sid, n) - if namespace == '/' and self.manager.is_connected(sid, namespace): - self.manager.pre_disconnect(sid, namespace='/') - await self._trigger_event('disconnect', '/', sid) - self.manager.disconnect(sid, '/') - - async def _handle_event(self, sid, namespace, id, data): + sid = self.manager.sid_from_eio_sid(eio_sid, namespace) + if self.manager.is_connected(sid, namespace): + self.manager.pre_disconnect(sid, namespace=namespace) + await self._trigger_event('disconnect', namespace, sid) + self.manager.disconnect(sid, namespace) + + async def _handle_event(self, eio_sid, namespace, id, data): """Handle an incoming client event.""" namespace = namespace or '/' + sid = self.manager.sid_from_eio_sid(eio_sid, namespace) self.logger.info('received event "%s" from %s [%s]', data[0], sid, namespace) if not self.manager.is_connected(sid, namespace): @@ -465,11 +457,13 @@ async def _handle_event(self, sid, namespace, id, data): return if self.async_handlers: self.start_background_task(self._handle_event_internal, self, sid, - data, namespace, id) + eio_sid, data, namespace, id) else: - await self._handle_event_internal(self, sid, data, namespace, id) + await self._handle_event_internal(self, sid, eio_sid, data, + namespace, id) - async def _handle_event_internal(self, server, sid, data, namespace, id): + async def _handle_event_internal(self, server, sid, eio_sid, data, + namespace, id): r = await server._trigger_event(data[0], namespace, sid, *data[1:]) if id is not None: # send ACK packet with the response returned by the handler @@ -480,13 +474,13 @@ async def _handle_event_internal(self, server, sid, data, namespace, id): data = list(r) else: data = [r] - await server._send_packet(sid, packet.Packet(packet.ACK, - namespace=namespace, - id=id, data=data)) + await server._send_packet(eio_sid, packet.Packet( + packet.ACK, namespace=namespace, id=id, data=data)) - async def _handle_ack(self, sid, namespace, id, data): + async def _handle_ack(self, eio_sid, namespace, id, data): """Handle ACK packets from the client.""" namespace = namespace or '/' + sid = self.manager.sid_from_eio_sid(eio_sid, namespace) self.logger.info('received ack from %s [%s]', sid, namespace) await self.manager.trigger_callback(sid, namespace, id, data) @@ -509,48 +503,50 @@ async def _trigger_event(self, event, namespace, *args): return await self.namespace_handlers[namespace].trigger_event( event, *args) - async def _handle_eio_connect(self, sid, environ): + async def _handle_eio_connect(self, eio_sid, environ): """Handle the Engine.IO connection event.""" if not self.manager_initialized: self.manager_initialized = True self.manager.initialize() - self.environ[sid] = environ + self.environ[eio_sid] = environ - async def _handle_eio_message(self, sid, data): + async def _handle_eio_message(self, eio_sid, data): """Dispatch Engine.IO messages.""" - if sid in self._binary_packet: - pkt = self._binary_packet[sid] + if eio_sid in self._binary_packet: + pkt = self._binary_packet[eio_sid] if pkt.add_attachment(data): - del self._binary_packet[sid] + del self._binary_packet[eio_sid] if pkt.packet_type == packet.BINARY_EVENT: - await self._handle_event(sid, pkt.namespace, pkt.id, + await self._handle_event(eio_sid, pkt.namespace, pkt.id, pkt.data) else: - await self._handle_ack(sid, pkt.namespace, pkt.id, + await self._handle_ack(eio_sid, pkt.namespace, pkt.id, pkt.data) else: pkt = packet.Packet(encoded_packet=data) if pkt.packet_type == packet.CONNECT: - await self._handle_connect(sid, pkt.namespace) + await self._handle_connect(eio_sid, pkt.namespace) elif pkt.packet_type == packet.DISCONNECT: - await self._handle_disconnect(sid, pkt.namespace) + await self._handle_disconnect(eio_sid, pkt.namespace) elif pkt.packet_type == packet.EVENT: - await self._handle_event(sid, pkt.namespace, pkt.id, pkt.data) + await self._handle_event(eio_sid, pkt.namespace, pkt.id, + pkt.data) elif pkt.packet_type == packet.ACK: - await self._handle_ack(sid, pkt.namespace, pkt.id, pkt.data) + await self._handle_ack(eio_sid, pkt.namespace, pkt.id, + pkt.data) elif pkt.packet_type == packet.BINARY_EVENT or \ pkt.packet_type == packet.BINARY_ACK: - self._binary_packet[sid] = pkt + self._binary_packet[eio_sid] = pkt elif pkt.packet_type == packet.CONNECT_ERROR: raise ValueError('Unexpected CONNECT_ERROR packet.') else: raise ValueError('Unknown packet type.') - async def _handle_eio_disconnect(self, sid): + async def _handle_eio_disconnect(self, eio_sid): """Handle Engine.IO disconnect event.""" - await self._handle_disconnect(sid, '/') - if sid in self.environ: - del self.environ[sid] + await self._handle_disconnect(eio_sid, '/') + if eio_sid in self.environ: + del self.environ[eio_sid] def _engineio_server_class(self): return engineio.AsyncServer diff --git a/socketio/base_manager.py b/socketio/base_manager.py index 09264623..2053dc1c 100644 --- a/socketio/base_manager.py +++ b/socketio/base_manager.py @@ -1,6 +1,7 @@ import itertools import logging +from bidict import bidict import six default_logger = logging.getLogger('socketio') @@ -18,7 +19,8 @@ class BaseManager(object): def __init__(self): self.logger = None self.server = None - self.rooms = {} + self.rooms = {} # self.rooms[namespace][room][sio_sid] = eio_sid + self.eio_to_sid = {} self.callbacks = {} self.pending_disconnect = {} @@ -37,13 +39,15 @@ def get_namespaces(self): def get_participants(self, namespace, room): """Return an iterable with the active participants in a room.""" - for sid, active in six.iteritems(self.rooms[namespace][room].copy()): - yield sid + for sid, eio_sid in self.rooms[namespace][room].copy().items(): + yield sid, eio_sid - def connect(self, sid, namespace): + def connect(self, eio_sid, namespace): """Register a client connection to a namespace.""" - self.enter_room(sid, namespace, None) - self.enter_room(sid, namespace, sid) + sid = self.server.eio.generate_id() + self.enter_room(sid, namespace, None, eio_sid=eio_sid) + self.enter_room(sid, namespace, sid, eio_sid=eio_sid) + return sid def is_connected(self, sid, namespace): if namespace in self.pending_disconnect and \ @@ -55,6 +59,9 @@ def is_connected(self, sid, namespace): except KeyError: pass + def sid_from_eio_sid(self, eio_sid, namespace): + return self.rooms[namespace][None].inverse.get(eio_sid) + def can_disconnect(self, sid, namespace): return self.is_connected(sid, namespace) @@ -68,6 +75,7 @@ def pre_disconnect(self, sid, namespace): if namespace not in self.pending_disconnect: self.pending_disconnect[namespace] = [] self.pending_disconnect[namespace].append(sid) + return self.rooms[namespace][None].get(sid) def disconnect(self, sid, namespace): """Register a client disconnect from a namespace.""" @@ -89,13 +97,15 @@ def disconnect(self, sid, namespace): if len(self.pending_disconnect[namespace]) == 0: del self.pending_disconnect[namespace] - def enter_room(self, sid, namespace, room): + def enter_room(self, sid, namespace, room, eio_sid=None): """Add a client to a room.""" if namespace not in self.rooms: self.rooms[namespace] = {} if room not in self.rooms[namespace]: - self.rooms[namespace][room] = {} - self.rooms[namespace][room][sid] = True + self.rooms[namespace][room] = bidict() + if eio_sid is None: + eio_sid = self.rooms[namespace][None][sid] + self.rooms[namespace][room][sid] = eio_sid def leave_room(self, sid, namespace, room): """Remove a client from a room.""" @@ -111,7 +121,7 @@ def leave_room(self, sid, namespace, room): def close_room(self, room, namespace): """Remove all participants from a room.""" try: - for sid in self.get_participants(namespace, room): + for sid, _ in self.get_participants(namespace, room): self.leave_room(sid, namespace, room) except KeyError: pass @@ -121,7 +131,7 @@ def get_rooms(self, sid, namespace): r = [] try: for room_name, room in six.iteritems(self.rooms[namespace]): - if room_name is not None and sid in room and room[sid]: + if room_name is not None and sid in room: r.append(room_name) except KeyError: pass @@ -135,13 +145,13 @@ def emit(self, event, data, namespace, room=None, skip_sid=None, return if not isinstance(skip_sid, list): skip_sid = [skip_sid] - for sid in self.get_participants(namespace, room): + for sid, eio_sid in self.get_participants(namespace, room): if sid not in skip_sid: if callback is not None: id = self._generate_ack_id(sid, namespace, callback) else: id = None - self.server._emit_internal(sid, event, data, namespace, id) + self.server._emit_internal(eio_sid, event, data, namespace, id) def trigger_callback(self, sid, namespace, id, data): """Invoke an application callback.""" diff --git a/socketio/client.py b/socketio/client.py index 5873a5b0..9c5aa120 100644 --- a/socketio/client.py +++ b/socketio/client.py @@ -533,7 +533,7 @@ def _handle_error(self, namespace, data): if namespace in self.namespaces: del self.namespaces[namespace] if namespace == '/': - self.namespaces = [] + self.namespaces = {} self.connected = False def _trigger_event(self, event, namespace, *args): @@ -625,7 +625,7 @@ def _handle_eio_disconnect(self): if self.connected: for n in self.namespaces: self._trigger_event('disconnect', namespace=n) - self.namespaces = [] + self.namespaces = {} self.connected = False self.callbacks = {} self._binary_packet = None diff --git a/socketio/server.py b/socketio/server.py index d6bc7e35..b842f404 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -520,13 +520,11 @@ def disconnect(self, sid, namespace=None, ignore_queue=False): delete_it = self.manager.can_disconnect(sid, namespace) if delete_it: self.logger.info('Disconnecting %s [%s]', sid, namespace) - self.manager.pre_disconnect(sid, namespace=namespace) - self._send_packet(sid, packet.Packet(packet.DISCONNECT, - namespace=namespace)) + eio_sid = self.manager.pre_disconnect(sid, namespace=namespace) + self._send_packet(eio_sid, packet.Packet( + packet.DISCONNECT, namespace=namespace)) self._trigger_event('disconnect', namespace, sid) self.manager.disconnect(sid, namespace=namespace) - if namespace == '/': - self.eio.disconnect(sid) def transport(self, sid): """Return the name of the transport used by the client. @@ -594,26 +592,26 @@ def _emit_internal(self, sid, event, data, namespace=None, id=None): self._send_packet(sid, packet.Packet(packet.EVENT, namespace=namespace, data=[event] + data, id=id)) - def _send_packet(self, sid, pkt): + def _send_packet(self, eio_sid, pkt): """Send a Socket.IO packet to a client.""" encoded_packet = pkt.encode() if isinstance(encoded_packet, list): for ep in encoded_packet: - self.eio.send(sid, ep) + self.eio.send(eio_sid, ep) else: - self.eio.send(sid, encoded_packet) + self.eio.send(eio_sid, encoded_packet) - def _handle_connect(self, sid, namespace): + def _handle_connect(self, eio_sid, namespace): """Handle a client connection request.""" namespace = namespace or '/' - self.manager.connect(sid, namespace) + sid = self.manager.connect(eio_sid, namespace) if self.always_connect: - self._send_packet(sid, packet.Packet(packet.CONNECT, - namespace=namespace)) + self._send_packet(eio_sid, packet.Packet( + packet.CONNECT, {'sid': sid}, namespace=namespace)) fail_reason = None try: success = self._trigger_event('connect', namespace, sid, - self.environ[sid]) + self.environ[eio_sid]) except exceptions.ConnectionRefusedError as exc: fail_reason = exc.error_args success = False @@ -621,40 +619,34 @@ def _handle_connect(self, sid, namespace): if success is False: if self.always_connect: self.manager.pre_disconnect(sid, namespace) - self._send_packet(sid, packet.Packet( + self._send_packet(eio_sid, packet.Packet( packet.DISCONNECT, data=fail_reason, namespace=namespace)) elif namespace != '/': - self._send_packet(sid, packet.Packet( + self._send_packet(eio_sid, packet.Packet( packet.CONNECT_ERROR, data=fail_reason, namespace=namespace)) self.manager.disconnect(sid, namespace) - if namespace == '/' and sid in self.environ: # pragma: no cover - del self.environ[sid] + if namespace == '/' and \ + eio_sid in self.environ: # pragma: no cover + del self.environ[eio_sid] return fail_reason or False elif not self.always_connect: - self._send_packet(sid, packet.Packet(packet.CONNECT, - namespace=namespace)) + self._send_packet(eio_sid, packet.Packet( + packet.CONNECT, {'sid': sid}, namespace=namespace)) - def _handle_disconnect(self, sid, namespace): + def _handle_disconnect(self, eio_sid, namespace): """Handle a client disconnect.""" namespace = namespace or '/' - if namespace == '/': - namespace_list = list(self.manager.get_namespaces()) - else: - namespace_list = [namespace] - for n in namespace_list: - if n != '/' and self.manager.is_connected(sid, n): - self.manager.pre_disconnect(sid, namespace=n) - self._trigger_event('disconnect', n, sid) - self.manager.disconnect(sid, n) - if namespace == '/' and self.manager.is_connected(sid, namespace): - self.manager.pre_disconnect(sid, namespace='/') - self._trigger_event('disconnect', '/', sid) - self.manager.disconnect(sid, '/') - - def _handle_event(self, sid, namespace, id, data): + sid = self.manager.sid_from_eio_sid(eio_sid, namespace) + if self.manager.is_connected(sid, namespace): + self.manager.pre_disconnect(sid, namespace=namespace) + self._trigger_event('disconnect', namespace, sid) + self.manager.disconnect(sid, namespace) + + def _handle_event(self, eio_sid, namespace, id, data): """Handle an incoming client event.""" namespace = namespace or '/' + sid = self.manager.sid_from_eio_sid(eio_sid, namespace) self.logger.info('received event "%s" from %s [%s]', data[0], sid, namespace) if not self.manager.is_connected(sid, namespace): @@ -663,11 +655,13 @@ def _handle_event(self, sid, namespace, id, data): return if self.async_handlers: self.start_background_task(self._handle_event_internal, self, sid, - data, namespace, id) + eio_sid, data, namespace, id) else: - self._handle_event_internal(self, sid, data, namespace, id) + self._handle_event_internal(self, sid, eio_sid, data, namespace, + id) - def _handle_event_internal(self, server, sid, data, namespace, id): + def _handle_event_internal(self, server, sid, eio_sid, data, namespace, + id): r = server._trigger_event(data[0], namespace, sid, *data[1:]) if id is not None: # send ACK packet with the response returned by the handler @@ -678,13 +672,13 @@ def _handle_event_internal(self, server, sid, data, namespace, id): data = list(r) else: data = [r] - server._send_packet(sid, packet.Packet(packet.ACK, - namespace=namespace, - id=id, data=data)) + server._send_packet(eio_sid, packet.Packet( + packet.ACK, namespace=namespace, id=id, data=data)) - def _handle_ack(self, sid, namespace, id, data): + def _handle_ack(self, eio_sid, namespace, id, data): """Handle ACK packets from the client.""" namespace = namespace or '/' + sid = self.manager.sid_from_eio_sid(eio_sid, namespace) self.logger.info('received ack from %s [%s]', sid, namespace) self.manager.trigger_callback(sid, namespace, id, data) @@ -699,46 +693,47 @@ def _trigger_event(self, event, namespace, *args): return self.namespace_handlers[namespace].trigger_event( event, *args) - def _handle_eio_connect(self, sid, environ): + def _handle_eio_connect(self, eio_sid, environ): """Handle the Engine.IO connection event.""" if not self.manager_initialized: self.manager_initialized = True self.manager.initialize() - self.environ[sid] = environ + self.environ[eio_sid] = environ - def _handle_eio_message(self, sid, data): + def _handle_eio_message(self, eio_sid, data): """Dispatch Engine.IO messages.""" - if sid in self._binary_packet: - pkt = self._binary_packet[sid] + if eio_sid in self._binary_packet: + pkt = self._binary_packet[eio_sid] if pkt.add_attachment(data): - del self._binary_packet[sid] + del self._binary_packet[eio_sid] if pkt.packet_type == packet.BINARY_EVENT: - self._handle_event(sid, pkt.namespace, pkt.id, pkt.data) + self._handle_event(eio_sid, pkt.namespace, pkt.id, + pkt.data) else: - self._handle_ack(sid, pkt.namespace, pkt.id, pkt.data) + self._handle_ack(eio_sid, pkt.namespace, pkt.id, pkt.data) else: pkt = packet.Packet(encoded_packet=data) if pkt.packet_type == packet.CONNECT: - self._handle_connect(sid, pkt.namespace) + self._handle_connect(eio_sid, pkt.namespace) elif pkt.packet_type == packet.DISCONNECT: - self._handle_disconnect(sid, pkt.namespace) + self._handle_disconnect(eio_sid, pkt.namespace) elif pkt.packet_type == packet.EVENT: - self._handle_event(sid, pkt.namespace, pkt.id, pkt.data) + self._handle_event(eio_sid, pkt.namespace, pkt.id, pkt.data) elif pkt.packet_type == packet.ACK: - self._handle_ack(sid, pkt.namespace, pkt.id, pkt.data) + self._handle_ack(eio_sid, pkt.namespace, pkt.id, pkt.data) elif pkt.packet_type == packet.BINARY_EVENT or \ pkt.packet_type == packet.BINARY_ACK: - self._binary_packet[sid] = pkt + self._binary_packet[eio_sid] = pkt elif pkt.packet_type == packet.CONNECT_ERROR: raise ValueError('Unexpected CONNECT_ERROR packet.') else: raise ValueError('Unknown packet type.') - def _handle_eio_disconnect(self, sid): + def _handle_eio_disconnect(self, eio_sid): """Handle Engine.IO disconnect event.""" - self._handle_disconnect(sid, '/') - if sid in self.environ: - del self.environ[sid] + self._handle_disconnect(eio_sid, '/') + if eio_sid in self.environ: + del self.environ[eio_sid] def _engineio_server_class(self): return engineio.Server diff --git a/tests/common/test_base_manager.py b/tests/common/test_base_manager.py index 4425ad3d..479b80f3 100644 --- a/tests/common/test_base_manager.py +++ b/tests/common/test_base_manager.py @@ -12,21 +12,28 @@ class TestBaseManager(unittest.TestCase): def setUp(self): + id = 0 + + def generate_id(): + nonlocal id + id += 1 + return str(id) + mock_server = mock.MagicMock() + mock_server.eio.generate_id = generate_id self.bm = base_manager.BaseManager() self.bm.set_server(mock_server) self.bm.initialize() def test_connect(self): - self.bm.connect('123', '/foo') + sid = self.bm.connect('123', '/foo') assert None in self.bm.rooms['/foo'] - assert '123' in self.bm.rooms['/foo'] - assert '123' in self.bm.rooms['/foo'][None] - assert '123' in self.bm.rooms['/foo']['123'] - assert self.bm.rooms['/foo'] == { - None: {'123': True}, - '123': {'123': True}, - } + assert sid in self.bm.rooms['/foo'] + assert sid in self.bm.rooms['/foo'][None] + assert sid in self.bm.rooms['/foo'][sid] + assert dict(self.bm.rooms['/foo'][None]) == {sid: '123'} + assert dict(self.bm.rooms['/foo'][sid]) == {sid: '123'} + assert self.bm.sid_from_eio_sid('123', '/foo') == sid def test_pre_disconnect(self): self.bm.connect('123', '/foo') @@ -43,63 +50,53 @@ def test_pre_disconnect(self): assert self.bm.pending_disconnect == {} def test_disconnect(self): - self.bm.connect('123', '/foo') - self.bm.connect('456', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - self.bm.enter_room('456', '/foo', 'baz') - self.bm.disconnect('123', '/foo') - assert self.bm.rooms['/foo'] == { - None: {'456': True}, - '456': {'456': True}, - 'baz': {'456': True}, - } + sid1 = self.bm.connect('123', '/foo') + sid2 = self.bm.connect('456', '/foo') + self.bm.enter_room(sid1, '/foo', 'bar') + self.bm.enter_room(sid2, '/foo', 'baz') + self.bm.disconnect(sid1, '/foo') + assert dict(self.bm.rooms['/foo'][None]) == {sid2: '456'} + assert dict(self.bm.rooms['/foo'][sid2]) == {sid2: '456'} + assert dict(self.bm.rooms['/foo']['baz']) == {sid2: '456'} def test_disconnect_default_namespace(self): - self.bm.connect('123', '/') - self.bm.connect('123', '/foo') - self.bm.connect('456', '/') - self.bm.connect('456', '/foo') - assert self.bm.is_connected('123', '/') - assert self.bm.is_connected('123', '/foo') - self.bm.disconnect('123', '/') - assert not self.bm.is_connected('123', '/') - assert self.bm.is_connected('123', '/foo') - self.bm.disconnect('123', '/foo') - assert not self.bm.is_connected('123', '/foo') - assert self.bm.rooms['/'] == { - None: {'456': True}, - '456': {'456': True}, - } - assert self.bm.rooms['/foo'] == { - None: {'456': True}, - '456': {'456': True}, - } + sid1 = self.bm.connect('123', '/') + sid2 = self.bm.connect('123', '/foo') + sid3 = self.bm.connect('456', '/') + sid4 = self.bm.connect('456', '/foo') + assert self.bm.is_connected(sid1, '/') + assert self.bm.is_connected(sid2, '/foo') + self.bm.disconnect(sid1, '/') + assert not self.bm.is_connected(sid1, '/') + assert self.bm.is_connected(sid2, '/foo') + self.bm.disconnect(sid2, '/foo') + assert not self.bm.is_connected(sid2, '/foo') + assert dict(self.bm.rooms['/'][None]) == {sid3: '456'} + assert dict(self.bm.rooms['/'][sid3]) == {sid3: '456'} + assert dict(self.bm.rooms['/foo'][None]) == {sid4: '456'} + assert dict(self.bm.rooms['/foo'][sid4]) == {sid4: '456'} def test_disconnect_twice(self): - self.bm.connect('123', '/') - self.bm.connect('123', '/foo') - self.bm.connect('456', '/') - self.bm.connect('456', '/foo') - self.bm.disconnect('123', '/') - self.bm.disconnect('123', '/foo') - self.bm.disconnect('123', '/') - self.bm.disconnect('123', '/foo') - assert self.bm.rooms['/'] == { - None: {'456': True}, - '456': {'456': True}, - } - assert self.bm.rooms['/foo'] == { - None: {'456': True}, - '456': {'456': True}, - } + sid1 = self.bm.connect('123', '/') + sid2 = self.bm.connect('123', '/foo') + sid3 = self.bm.connect('456', '/') + sid4 = self.bm.connect('456', '/foo') + self.bm.disconnect(sid1, '/') + self.bm.disconnect(sid2, '/foo') + self.bm.disconnect(sid1, '/') + self.bm.disconnect(sid2, '/foo') + assert dict(self.bm.rooms['/'][None]) == {sid3: '456'} + assert dict(self.bm.rooms['/'][sid3]) == {sid3: '456'} + assert dict(self.bm.rooms['/foo'][None]) == {sid4: '456'} + assert dict(self.bm.rooms['/foo'][sid4]) == {sid4: '456'} def test_disconnect_all(self): - self.bm.connect('123', '/foo') - self.bm.connect('456', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - self.bm.enter_room('456', '/foo', 'baz') - self.bm.disconnect('123', '/foo') - self.bm.disconnect('456', '/foo') + sid1 = self.bm.connect('123', '/foo') + sid2 = self.bm.connect('456', '/foo') + self.bm.enter_room(sid1, '/foo', 'bar') + self.bm.enter_room(sid2, '/foo', 'baz') + self.bm.disconnect(sid1, '/foo') + self.bm.disconnect(sid2, '/foo') assert self.bm.rooms == {} def test_disconnect_with_callbacks(self): @@ -152,12 +149,12 @@ def test_get_namespaces(self): def test_get_participants(self): self.bm.connect('123', '/') self.bm.connect('456', '/') - self.bm.connect('789', '/') - self.bm.disconnect('789', '/') - assert '789' not in self.bm.rooms['/'][None] + sid = self.bm.connect('789', '/') + self.bm.disconnect(sid, '/') + assert sid not in self.bm.rooms['/'][None] participants = list(self.bm.get_participants('/', None)) assert len(participants) == 2 - assert '789' not in participants + assert sid not in participants def test_leave_invalid_room(self): self.bm.connect('123', '/foo') @@ -169,11 +166,11 @@ def test_no_room(self): assert [] == rooms def test_close_room(self): - self.bm.connect('123', '/foo') + sid1 = self.bm.connect('123', '/foo') self.bm.connect('456', '/foo') self.bm.connect('789', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - self.bm.enter_room('123', '/foo', 'bar') + self.bm.enter_room(sid1, '/foo', 'bar') + self.bm.enter_room(sid1, '/foo', 'bar') self.bm.close_room('bar', '/foo') assert 'bar' not in self.bm.rooms['/foo'] @@ -181,26 +178,26 @@ def test_close_invalid_room(self): self.bm.close_room('bar', '/foo') def test_rooms(self): - self.bm.connect('123', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - r = self.bm.get_rooms('123', '/foo') + sid = self.bm.connect('123', '/foo') + self.bm.enter_room(sid, '/foo', 'bar') + r = self.bm.get_rooms(sid, '/foo') assert len(r) == 2 - assert '123' in r + assert sid in r assert 'bar' in r def test_emit_to_sid(self): - self.bm.connect('123', '/foo') + sid = self.bm.connect('123', '/foo') self.bm.connect('456', '/foo') - self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', room='123') + self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', room=sid) self.bm.server._emit_internal.assert_called_once_with( '123', 'my event', {'foo': 'bar'}, '/foo', None ) def test_emit_to_room(self): - self.bm.connect('123', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - self.bm.connect('456', '/foo') - self.bm.enter_room('456', '/foo', 'bar') + sid1 = self.bm.connect('123', '/foo') + self.bm.enter_room(sid1, '/foo', 'bar') + sid2 = self.bm.connect('456', '/foo') + self.bm.enter_room(sid2, '/foo', 'bar') self.bm.connect('789', '/foo') self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', room='bar') assert self.bm.server._emit_internal.call_count == 2 @@ -212,10 +209,10 @@ def test_emit_to_room(self): ) def test_emit_to_all(self): - self.bm.connect('123', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - self.bm.connect('456', '/foo') - self.bm.enter_room('456', '/foo', 'bar') + sid1 = self.bm.connect('123', '/foo') + self.bm.enter_room(sid1, '/foo', 'bar') + sid2 = self.bm.connect('456', '/foo') + self.bm.enter_room(sid2, '/foo', 'bar') self.bm.connect('789', '/foo') self.bm.connect('abc', '/bar') self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo') @@ -231,14 +228,14 @@ def test_emit_to_all(self): ) def test_emit_to_all_skip_one(self): - self.bm.connect('123', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - self.bm.connect('456', '/foo') - self.bm.enter_room('456', '/foo', 'bar') + sid1 = self.bm.connect('123', '/foo') + self.bm.enter_room(sid1, '/foo', 'bar') + sid2 = self.bm.connect('456', '/foo') + self.bm.enter_room(sid2, '/foo', 'bar') self.bm.connect('789', '/foo') self.bm.connect('abc', '/bar') self.bm.emit( - 'my event', {'foo': 'bar'}, namespace='/foo', skip_sid='456' + 'my event', {'foo': 'bar'}, namespace='/foo', skip_sid=sid2 ) assert self.bm.server._emit_internal.call_count == 2 self.bm.server._emit_internal.assert_any_call( @@ -249,17 +246,17 @@ def test_emit_to_all_skip_one(self): ) def test_emit_to_all_skip_two(self): - self.bm.connect('123', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - self.bm.connect('456', '/foo') - self.bm.enter_room('456', '/foo', 'bar') - self.bm.connect('789', '/foo') + sid1 = self.bm.connect('123', '/foo') + self.bm.enter_room(sid1, '/foo', 'bar') + sid2 = self.bm.connect('456', '/foo') + self.bm.enter_room(sid2, '/foo', 'bar') + sid3 = self.bm.connect('789', '/foo') self.bm.connect('abc', '/bar') self.bm.emit( 'my event', {'foo': 'bar'}, namespace='/foo', - skip_sid=['123', '789'], + skip_sid=[sid1, sid3], ) assert self.bm.server._emit_internal.call_count == 1 self.bm.server._emit_internal.assert_any_call( @@ -267,13 +264,13 @@ def test_emit_to_all_skip_two(self): ) def test_emit_with_callback(self): - self.bm.connect('123', '/foo') + sid = self.bm.connect('123', '/foo') self.bm._generate_ack_id = mock.MagicMock() self.bm._generate_ack_id.return_value = 11 self.bm.emit( 'my event', {'foo': 'bar'}, namespace='/foo', callback='cb' ) - self.bm._generate_ack_id.assert_called_once_with('123', '/foo', 'cb') + self.bm._generate_ack_id.assert_called_once_with(sid, '/foo', 'cb') self.bm.server._emit_internal.assert_called_once_with( '123', 'my event', {'foo': 'bar'}, '/foo', 11 ) From a4cc0b2c5b8fcd0fe3790ad629ac3e01d97729bd Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 5 Dec 2020 15:38:07 +0000 Subject: [PATCH 185/571] v5 protocol: client manager unit tests --- socketio/asyncio_manager.py | 8 +- socketio/asyncio_pubsub_manager.py | 5 +- socketio/asyncio_server.py | 2 +- socketio/base_manager.py | 27 +- socketio/pubsub_manager.py | 7 +- socketio/server.py | 8 +- tests/asyncio/test_asyncio_manager.py | 272 ++++++++++--------- tests/asyncio/test_asyncio_pubsub_manager.py | 14 +- tests/common/test_base_manager.py | 92 ++++--- tests/common/test_pubsub_manager.py | 14 +- 10 files changed, 228 insertions(+), 221 deletions(-) diff --git a/socketio/asyncio_manager.py b/socketio/asyncio_manager.py index db9fecf1..f89022c6 100644 --- a/socketio/asyncio_manager.py +++ b/socketio/asyncio_manager.py @@ -23,7 +23,7 @@ async def emit(self, event, data, namespace, room=None, skip_sid=None, for sid, eio_sid in self.get_participants(namespace, room): if sid not in skip_sid: if callback is not None: - id = self._generate_ack_id(sid, namespace, callback) + id = self._generate_ack_id(sid, callback) else: id = None tasks.append(self.server._emit_internal(eio_sid, event, data, @@ -39,19 +39,19 @@ async def close_room(self, room, namespace): """ return super().close_room(room, namespace) - async def trigger_callback(self, sid, namespace, id, data): + async def trigger_callback(self, sid, id, data): """Invoke an application callback. Note: this method is a coroutine. """ callback = None try: - callback = self.callbacks[sid][namespace][id] + callback = self.callbacks[sid][id] except KeyError: # if we get an unknown callback we just ignore it self._get_logger().warning('Unknown callback received, ignoring.') else: - del self.callbacks[sid][namespace][id] + del self.callbacks[sid][id] if callback is not None: ret = callback(*data) if asyncio.iscoroutine(ret): diff --git a/socketio/asyncio_pubsub_manager.py b/socketio/asyncio_pubsub_manager.py index 0e41f25f..fc4e2051 100644 --- a/socketio/asyncio_pubsub_manager.py +++ b/socketio/asyncio_pubsub_manager.py @@ -60,7 +60,7 @@ async def emit(self, event, data, namespace=None, room=None, skip_sid=None, 'context of a server.') if room is None: raise ValueError('Cannot use callback without a room set.') - id = self._generate_ack_id(room, namespace, callback) + id = self._generate_ack_id(room, callback) callback = (room, namespace, id) else: callback = None @@ -122,12 +122,11 @@ async def _handle_callback(self, message): if self.host_id == message.get('host_id'): try: sid = message['sid'] - namespace = message['namespace'] id = message['id'] args = message['args'] except KeyError: return - await self.trigger_callback(sid, namespace, id, args) + await self.trigger_callback(sid, id, args) async def _return_callback(self, host_id, sid, namespace, callback_id, *args): diff --git a/socketio/asyncio_server.py b/socketio/asyncio_server.py index 5eaeb3ac..d35d3558 100644 --- a/socketio/asyncio_server.py +++ b/socketio/asyncio_server.py @@ -482,7 +482,7 @@ async def _handle_ack(self, eio_sid, namespace, id, data): namespace = namespace or '/' sid = self.manager.sid_from_eio_sid(eio_sid, namespace) self.logger.info('received ack from %s [%s]', sid, namespace) - await self.manager.trigger_callback(sid, namespace, id, data) + await self.manager.trigger_callback(sid, id, data) async def _trigger_event(self, event, namespace, *args): """Invoke an application event handler.""" diff --git a/socketio/base_manager.py b/socketio/base_manager.py index 2053dc1c..cec0e918 100644 --- a/socketio/base_manager.py +++ b/socketio/base_manager.py @@ -55,7 +55,7 @@ def is_connected(self, sid, namespace): # the client is in the process of being disconnected return False try: - return self.rooms[namespace][None][sid] + return self.rooms[namespace][None][sid] is not None except KeyError: pass @@ -87,10 +87,8 @@ def disconnect(self, sid, namespace): rooms.append(room_name) for room in rooms: self.leave_room(sid, namespace, room) - if sid in self.callbacks and namespace in self.callbacks[sid]: - del self.callbacks[sid][namespace] - if len(self.callbacks[sid]) == 0: - del self.callbacks[sid] + if sid in self.callbacks: + del self.callbacks[sid] if namespace in self.pending_disconnect and \ sid in self.pending_disconnect[namespace]: self.pending_disconnect[namespace].remove(sid) @@ -148,33 +146,30 @@ def emit(self, event, data, namespace, room=None, skip_sid=None, for sid, eio_sid in self.get_participants(namespace, room): if sid not in skip_sid: if callback is not None: - id = self._generate_ack_id(sid, namespace, callback) + id = self._generate_ack_id(sid, callback) else: id = None self.server._emit_internal(eio_sid, event, data, namespace, id) - def trigger_callback(self, sid, namespace, id, data): + def trigger_callback(self, sid, id, data): """Invoke an application callback.""" callback = None try: - callback = self.callbacks[sid][namespace][id] + callback = self.callbacks[sid][id] except KeyError: # if we get an unknown callback we just ignore it self._get_logger().warning('Unknown callback received, ignoring.') else: - del self.callbacks[sid][namespace][id] + del self.callbacks[sid][id] if callback is not None: callback(*data) - def _generate_ack_id(self, sid, namespace, callback): + def _generate_ack_id(self, sid, callback): """Generate a unique identifier for an ACK packet.""" - namespace = namespace or '/' if sid not in self.callbacks: - self.callbacks[sid] = {} - if namespace not in self.callbacks[sid]: - self.callbacks[sid][namespace] = {0: itertools.count(1)} - id = six.next(self.callbacks[sid][namespace][0]) - self.callbacks[sid][namespace][id] = callback + self.callbacks[sid] = {0: itertools.count(1)} + id = six.next(self.callbacks[sid][0]) + self.callbacks[sid][id] = callback return id def _get_logger(self): diff --git a/socketio/pubsub_manager.py b/socketio/pubsub_manager.py index b0438576..dcbef88c 100644 --- a/socketio/pubsub_manager.py +++ b/socketio/pubsub_manager.py @@ -58,7 +58,7 @@ def emit(self, event, data, namespace=None, room=None, skip_sid=None, 'context of a server.') if room is None: raise ValueError('Cannot use callback without a room set.') - id = self._generate_ack_id(room, namespace, callback) + id = self._generate_ack_id(room, callback) callback = (room, namespace, id) else: callback = None @@ -120,16 +120,15 @@ def _handle_callback(self, message): if self.host_id == message.get('host_id'): try: sid = message['sid'] - namespace = message['namespace'] id = message['id'] args = message['args'] except KeyError: return - self.trigger_callback(sid, namespace, id, args) + self.trigger_callback(sid, id, args) def _return_callback(self, host_id, sid, namespace, callback_id, *args): # When an event callback is received, the callback is returned back - # the sender, which is identified by the host_id + # to the sender, which is identified by the host_id self._publish({'method': 'callback', 'host_id': host_id, 'sid': sid, 'namespace': namespace, 'id': callback_id, 'args': args}) diff --git a/socketio/server.py b/socketio/server.py index b842f404..6418e76b 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -579,7 +579,7 @@ def sleep(self, seconds=0): """ return self.eio.sleep(seconds) - def _emit_internal(self, sid, event, data, namespace=None, id=None): + def _emit_internal(self, eio_sid, event, data, namespace=None, id=None): """Send a message to a client.""" # tuples are expanded to multiple arguments, everything else is sent # as a single argument @@ -589,8 +589,8 @@ def _emit_internal(self, sid, event, data, namespace=None, id=None): data = [data] else: data = [] - self._send_packet(sid, packet.Packet(packet.EVENT, namespace=namespace, - data=[event] + data, id=id)) + self._send_packet(eio_sid, packet.Packet( + packet.EVENT, namespace=namespace, data=[event] + data, id=id)) def _send_packet(self, eio_sid, pkt): """Send a Socket.IO packet to a client.""" @@ -680,7 +680,7 @@ def _handle_ack(self, eio_sid, namespace, id, data): namespace = namespace or '/' sid = self.manager.sid_from_eio_sid(eio_sid, namespace) self.logger.info('received ack from %s [%s]', sid, namespace) - self.manager.trigger_callback(sid, namespace, id, data) + self.manager.trigger_callback(sid, id, data) def _trigger_event(self, event, namespace, *args): """Invoke an application event handler.""" diff --git a/tests/asyncio/test_asyncio_manager.py b/tests/asyncio/test_asyncio_manager.py index ea4abf00..66fe53af 100644 --- a/tests/asyncio/test_asyncio_manager.py +++ b/tests/asyncio/test_asyncio_manager.py @@ -31,140 +31,142 @@ def _run(coro): @unittest.skipIf(sys.version_info < (3, 5), 'only for Python 3.5+') class TestAsyncManager(unittest.TestCase): def setUp(self): + id = 0 + + def generate_id(): + nonlocal id + id += 1 + return str(id) + mock_server = mock.MagicMock() mock_server._emit_internal = AsyncMock() + mock_server.eio.generate_id = generate_id self.bm = asyncio_manager.AsyncManager() self.bm.set_server(mock_server) self.bm.initialize() def test_connect(self): - self.bm.connect('123', '/foo') + sid = self.bm.connect('123', '/foo') assert None in self.bm.rooms['/foo'] - assert '123' in self.bm.rooms['/foo'] - assert '123' in self.bm.rooms['/foo'][None] - assert '123' in self.bm.rooms['/foo']['123'] - assert self.bm.rooms['/foo'] == { - None: {'123': True}, - '123': {'123': True}, - } + assert sid in self.bm.rooms['/foo'] + assert sid in self.bm.rooms['/foo'][None] + assert sid in self.bm.rooms['/foo'][sid] + assert dict(self.bm.rooms['/foo'][None]) == {sid: '123'} + assert dict(self.bm.rooms['/foo'][sid]) == {sid: '123'} + assert self.bm.sid_from_eio_sid('123', '/foo') == sid def test_pre_disconnect(self): - self.bm.connect('123', '/foo') - self.bm.connect('456', '/foo') - self.bm.pre_disconnect('123', '/foo') - assert self.bm.pending_disconnect == {'/foo': ['123']} - assert not self.bm.is_connected('123', '/foo') - self.bm.pre_disconnect('456', '/foo') - assert self.bm.pending_disconnect == {'/foo': ['123', '456']} - assert not self.bm.is_connected('456', '/foo') - self.bm.disconnect('123', '/foo') - assert self.bm.pending_disconnect == {'/foo': ['456']} - self.bm.disconnect('456', '/foo') + sid1 = self.bm.connect('123', '/foo') + sid2 = self.bm.connect('456', '/foo') + assert self.bm.is_connected(sid1, '/foo') + assert self.bm.pre_disconnect(sid1, '/foo') == '123' + assert self.bm.pending_disconnect == {'/foo': [sid1]} + assert not self.bm.is_connected(sid1, '/foo') + assert self.bm.pre_disconnect(sid2, '/foo') == '456' + assert self.bm.pending_disconnect == {'/foo': [sid1, sid2]} + assert not self.bm.is_connected(sid2, '/foo') + self.bm.disconnect(sid1, '/foo') + assert self.bm.pending_disconnect == {'/foo': [sid2]} + self.bm.disconnect(sid2, '/foo') assert self.bm.pending_disconnect == {} def test_disconnect(self): - self.bm.connect('123', '/foo') - self.bm.connect('456', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - self.bm.enter_room('456', '/foo', 'baz') - self.bm.disconnect('123', '/foo') - assert self.bm.rooms['/foo'] == { - None: {'456': True}, - '456': {'456': True}, - 'baz': {'456': True}, - } + sid1 = self.bm.connect('123', '/foo') + sid2 = self.bm.connect('456', '/foo') + self.bm.enter_room(sid1, '/foo', 'bar') + self.bm.enter_room(sid2, '/foo', 'baz') + self.bm.disconnect(sid1, '/foo') + assert dict(self.bm.rooms['/foo'][None]) == {sid2: '456'} + assert dict(self.bm.rooms['/foo'][sid2]) == {sid2: '456'} + assert dict(self.bm.rooms['/foo']['baz']) == {sid2: '456'} def test_disconnect_default_namespace(self): - self.bm.connect('123', '/') - self.bm.connect('123', '/foo') - self.bm.connect('456', '/') - self.bm.connect('456', '/foo') - assert self.bm.is_connected('123', '/') - assert self.bm.is_connected('123', '/foo') - self.bm.disconnect('123', '/') - assert not self.bm.is_connected('123', '/') - assert self.bm.is_connected('123', '/foo') - self.bm.disconnect('123', '/foo') - assert not self.bm.is_connected('123', '/foo') - assert self.bm.rooms['/'] == { - None: {'456': True}, - '456': {'456': True}, - } - assert self.bm.rooms['/foo'] == { - None: {'456': True}, - '456': {'456': True}, - } + sid1 = self.bm.connect('123', '/') + sid2 = self.bm.connect('123', '/foo') + sid3 = self.bm.connect('456', '/') + sid4 = self.bm.connect('456', '/foo') + assert self.bm.is_connected(sid1, '/') + assert self.bm.is_connected(sid2, '/foo') + assert not self.bm.is_connected(sid2, '/') + assert not self.bm.is_connected(sid1, '/foo') + self.bm.disconnect(sid1, '/') + assert not self.bm.is_connected(sid1, '/') + assert self.bm.is_connected(sid2, '/foo') + self.bm.disconnect(sid2, '/foo') + assert not self.bm.is_connected(sid2, '/foo') + assert dict(self.bm.rooms['/'][None]) == {sid3: '456'} + assert dict(self.bm.rooms['/'][sid3]) == {sid3: '456'} + assert dict(self.bm.rooms['/foo'][None]) == {sid4: '456'} + assert dict(self.bm.rooms['/foo'][sid4]) == {sid4: '456'} def test_disconnect_twice(self): - self.bm.connect('123', '/') - self.bm.connect('123', '/foo') - self.bm.connect('456', '/') - self.bm.connect('456', '/foo') - self.bm.disconnect('123', '/') - self.bm.disconnect('123', '/foo') - self.bm.disconnect('123', '/') - self.bm.disconnect('123', '/foo') - assert self.bm.rooms['/'] == { - None: {'456': True}, - '456': {'456': True}, - } - assert self.bm.rooms['/foo'] == { - None: {'456': True}, - '456': {'456': True}, - } + sid1 = self.bm.connect('123', '/') + sid2 = self.bm.connect('123', '/foo') + sid3 = self.bm.connect('456', '/') + sid4 = self.bm.connect('456', '/foo') + self.bm.disconnect(sid1, '/') + self.bm.disconnect(sid2, '/foo') + self.bm.disconnect(sid1, '/') + self.bm.disconnect(sid2, '/foo') + assert dict(self.bm.rooms['/'][None]) == {sid3: '456'} + assert dict(self.bm.rooms['/'][sid3]) == {sid3: '456'} + assert dict(self.bm.rooms['/foo'][None]) == {sid4: '456'} + assert dict(self.bm.rooms['/foo'][sid4]) == {sid4: '456'} def test_disconnect_all(self): - self.bm.connect('123', '/foo') - self.bm.connect('456', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - self.bm.enter_room('456', '/foo', 'baz') - self.bm.disconnect('123', '/foo') - self.bm.disconnect('456', '/foo') + sid1 = self.bm.connect('123', '/foo') + sid2 = self.bm.connect('456', '/foo') + self.bm.enter_room(sid1, '/foo', 'bar') + self.bm.enter_room(sid2, '/foo', 'baz') + self.bm.disconnect(sid1, '/foo') + self.bm.disconnect(sid2, '/foo') assert self.bm.rooms == {} def test_disconnect_with_callbacks(self): - self.bm.connect('123', '/') - self.bm.connect('123', '/foo') - self.bm._generate_ack_id('123', '/', 'f') - self.bm._generate_ack_id('123', '/foo', 'g') - self.bm.disconnect('123', '/foo') - assert '/foo' not in self.bm.callbacks['123'] - self.bm.disconnect('123', '/') - assert '123' not in self.bm.callbacks + sid1 = self.bm.connect('123', '/') + sid2 = self.bm.connect('123', '/foo') + sid3 = self.bm.connect('456', '/foo') + self.bm._generate_ack_id(sid1, 'f') + self.bm._generate_ack_id(sid2, 'g') + self.bm._generate_ack_id(sid3, 'h') + self.bm.disconnect(sid2, '/foo') + assert sid2 not in self.bm.callbacks + self.bm.disconnect(sid1, '/') + assert sid1 not in self.bm.callbacks + assert sid3 in self.bm.callbacks def test_trigger_sync_callback(self): - self.bm.connect('123', '/') - self.bm.connect('123', '/foo') + sid1 = self.bm.connect('123', '/') + sid2 = self.bm.connect('123', '/foo') cb = mock.MagicMock() - id1 = self.bm._generate_ack_id('123', '/', cb) - id2 = self.bm._generate_ack_id('123', '/foo', cb) - _run(self.bm.trigger_callback('123', '/', id1, ['foo'])) - _run(self.bm.trigger_callback('123', '/foo', id2, ['bar', 'baz'])) + id1 = self.bm._generate_ack_id(sid1, cb) + id2 = self.bm._generate_ack_id(sid2, cb) + _run(self.bm.trigger_callback(sid1, id1, ['foo'])) + _run(self.bm.trigger_callback(sid2, id2, ['bar', 'baz'])) assert cb.call_count == 2 cb.assert_any_call('foo') cb.assert_any_call('bar', 'baz') def test_trigger_async_callback(self): - self.bm.connect('123', '/') - self.bm.connect('123', '/foo') + sid1 = self.bm.connect('123', '/') + sid2 = self.bm.connect('123', '/foo') cb = AsyncMock() - id1 = self.bm._generate_ack_id('123', '/', cb) - id2 = self.bm._generate_ack_id('123', '/foo', cb) - _run(self.bm.trigger_callback('123', '/', id1, ['foo'])) - _run(self.bm.trigger_callback('123', '/foo', id2, ['bar', 'baz'])) + id1 = self.bm._generate_ack_id(sid1, cb) + id2 = self.bm._generate_ack_id(sid2, cb) + _run(self.bm.trigger_callback(sid1, id1, ['foo'])) + _run(self.bm.trigger_callback(sid2, id2, ['bar', 'baz'])) assert cb.mock.call_count == 2 cb.mock.assert_any_call('foo') cb.mock.assert_any_call('bar', 'baz') def test_invalid_callback(self): - self.bm.connect('123', '/') + sid = self.bm.connect('123', '/') cb = mock.MagicMock() - id = self.bm._generate_ack_id('123', '/', cb) + id = self.bm._generate_ack_id(sid, cb) # these should not raise an exception - _run(self.bm.trigger_callback('124', '/', id, ['foo'])) - _run(self.bm.trigger_callback('123', '/foo', id, ['foo'])) - _run(self.bm.trigger_callback('123', '/', id + 1, ['foo'])) + _run(self.bm.trigger_callback('xxx', id, ['foo'])) + _run(self.bm.trigger_callback(sid, id + 1, ['foo'])) assert cb.mock.call_count == 0 def test_get_namespaces(self): @@ -177,30 +179,32 @@ def test_get_namespaces(self): assert '/foo' in namespaces def test_get_participants(self): - self.bm.connect('123', '/') - self.bm.connect('456', '/') - self.bm.connect('789', '/') - self.bm.disconnect('789', '/') - assert '789' not in self.bm.rooms['/'][None] + sid1 = self.bm.connect('123', '/') + sid2 = self.bm.connect('456', '/') + sid3 = self.bm.connect('789', '/') + self.bm.disconnect(sid3, '/') + assert sid3 not in self.bm.rooms['/'][None] participants = list(self.bm.get_participants('/', None)) assert len(participants) == 2 - assert '789' not in participants + assert (sid1, '123') in participants + assert (sid2, '456') in participants + assert (sid3, '789') not in participants def test_leave_invalid_room(self): - self.bm.connect('123', '/foo') - self.bm.leave_room('123', '/foo', 'baz') - self.bm.leave_room('123', '/bar', 'baz') + sid = self.bm.connect('123', '/foo') + self.bm.leave_room(sid, '/foo', 'baz') + self.bm.leave_room(sid, '/bar', 'baz') def test_no_room(self): rooms = self.bm.get_rooms('123', '/foo') assert [] == rooms def test_close_room(self): - self.bm.connect('123', '/foo') + sid = self.bm.connect('123', '/foo') self.bm.connect('456', '/foo') self.bm.connect('789', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - self.bm.enter_room('123', '/foo', 'bar') + self.bm.enter_room(sid, '/foo', 'bar') + self.bm.enter_room(sid, '/foo', 'bar') _run(self.bm.close_room('bar', '/foo')) assert 'bar' not in self.bm.rooms['/foo'] @@ -208,19 +212,19 @@ def test_close_invalid_room(self): self.bm.close_room('bar', '/foo') def test_rooms(self): - self.bm.connect('123', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - r = self.bm.get_rooms('123', '/foo') + sid = self.bm.connect('123', '/foo') + self.bm.enter_room(sid, '/foo', 'bar') + r = self.bm.get_rooms(sid, '/foo') assert len(r) == 2 - assert '123' in r + assert sid in r assert 'bar' in r def test_emit_to_sid(self): - self.bm.connect('123', '/foo') + sid = self.bm.connect('123', '/foo') self.bm.connect('456', '/foo') _run( self.bm.emit( - 'my event', {'foo': 'bar'}, namespace='/foo', room='123' + 'my event', {'foo': 'bar'}, namespace='/foo', room=sid ) ) self.bm.server._emit_internal.mock.assert_called_once_with( @@ -228,10 +232,10 @@ def test_emit_to_sid(self): ) def test_emit_to_room(self): - self.bm.connect('123', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - self.bm.connect('456', '/foo') - self.bm.enter_room('456', '/foo', 'bar') + sid1 = self.bm.connect('123', '/foo') + self.bm.enter_room(sid1, '/foo', 'bar') + sid2 = self.bm.connect('456', '/foo') + self.bm.enter_room(sid2, '/foo', 'bar') self.bm.connect('789', '/foo') _run( self.bm.emit( @@ -247,10 +251,10 @@ def test_emit_to_room(self): ) def test_emit_to_all(self): - self.bm.connect('123', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - self.bm.connect('456', '/foo') - self.bm.enter_room('456', '/foo', 'bar') + sid1 = self.bm.connect('123', '/foo') + self.bm.enter_room(sid1, '/foo', 'bar') + sid2 = self.bm.connect('456', '/foo') + self.bm.enter_room(sid2, '/foo', 'bar') self.bm.connect('789', '/foo') self.bm.connect('abc', '/bar') _run(self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo')) @@ -266,15 +270,15 @@ def test_emit_to_all(self): ) def test_emit_to_all_skip_one(self): - self.bm.connect('123', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - self.bm.connect('456', '/foo') - self.bm.enter_room('456', '/foo', 'bar') + sid1 = self.bm.connect('123', '/foo') + self.bm.enter_room(sid1, '/foo', 'bar') + sid2 = self.bm.connect('456', '/foo') + self.bm.enter_room(sid2, '/foo', 'bar') self.bm.connect('789', '/foo') self.bm.connect('abc', '/bar') _run( self.bm.emit( - 'my event', {'foo': 'bar'}, namespace='/foo', skip_sid='456' + 'my event', {'foo': 'bar'}, namespace='/foo', skip_sid=sid2 ) ) assert self.bm.server._emit_internal.mock.call_count == 2 @@ -286,18 +290,18 @@ def test_emit_to_all_skip_one(self): ) def test_emit_to_all_skip_two(self): - self.bm.connect('123', '/foo') - self.bm.enter_room('123', '/foo', 'bar') - self.bm.connect('456', '/foo') - self.bm.enter_room('456', '/foo', 'bar') - self.bm.connect('789', '/foo') + sid1 = self.bm.connect('123', '/foo') + self.bm.enter_room(sid1, '/foo', 'bar') + sid2 = self.bm.connect('456', '/foo') + self.bm.enter_room(sid2, '/foo', 'bar') + sid3 = self.bm.connect('789', '/foo') self.bm.connect('abc', '/bar') _run( self.bm.emit( 'my event', {'foo': 'bar'}, namespace='/foo', - skip_sid=['123', '789'], + skip_sid=[sid1, sid3], ) ) assert self.bm.server._emit_internal.mock.call_count == 1 @@ -306,7 +310,7 @@ def test_emit_to_all_skip_two(self): ) def test_emit_with_callback(self): - self.bm.connect('123', '/foo') + sid = self.bm.connect('123', '/foo') self.bm._generate_ack_id = mock.MagicMock() self.bm._generate_ack_id.return_value = 11 _run( @@ -314,7 +318,7 @@ def test_emit_with_callback(self): 'my event', {'foo': 'bar'}, namespace='/foo', callback='cb' ) ) - self.bm._generate_ack_id.assert_called_once_with('123', '/foo', 'cb') + self.bm._generate_ack_id.assert_called_once_with(sid, 'cb') self.bm.server._emit_internal.mock.assert_called_once_with( '123', 'my event', {'foo': 'bar'}, '/foo', 11 ) diff --git a/tests/asyncio/test_asyncio_pubsub_manager.py b/tests/asyncio/test_asyncio_pubsub_manager.py index ca270fb4..7ca06b41 100644 --- a/tests/asyncio/test_asyncio_pubsub_manager.py +++ b/tests/asyncio/test_asyncio_pubsub_manager.py @@ -154,10 +154,10 @@ def test_emit_with_callback_missing_room(self): _run(self.pm.emit('foo', 'bar', callback='cb')) def test_emit_with_ignore_queue(self): - self.pm.connect('123', '/') + sid = self.pm.connect('123', '/') _run( self.pm.emit( - 'foo', 'bar', room='123', namespace='/', ignore_queue=True + 'foo', 'bar', room=sid, namespace='/', ignore_queue=True ) ) self.pm._publish.mock.assert_not_called() @@ -166,11 +166,11 @@ def test_emit_with_ignore_queue(self): ) def test_can_disconnect(self): - self.pm.connect('123', '/') - assert _run(self.pm.can_disconnect('123', '/')) is True - _run(self.pm.can_disconnect('123', '/foo')) + sid = self.pm.connect('123', '/') + assert _run(self.pm.can_disconnect(sid, '/')) is True + _run(self.pm.can_disconnect(sid, '/foo')) self.pm._publish.mock.assert_called_once_with( - {'method': 'disconnect', 'sid': '123', 'namespace': '/foo'} + {'method': 'disconnect', 'sid': sid, 'namespace': '/foo'} ) def test_close_room(self): @@ -310,7 +310,7 @@ def test_handle_callback(self): } ) ) - trigger.mock.assert_called_once_with('sid', '/', 123, ('one', 2)) + trigger.mock.assert_called_once_with('sid', 123, ('one', 2)) def test_handle_callback_bad_host_id(self): with mock.patch.object( diff --git a/tests/common/test_base_manager.py b/tests/common/test_base_manager.py index 479b80f3..0bcba822 100644 --- a/tests/common/test_base_manager.py +++ b/tests/common/test_base_manager.py @@ -36,17 +36,18 @@ def test_connect(self): assert self.bm.sid_from_eio_sid('123', '/foo') == sid def test_pre_disconnect(self): - self.bm.connect('123', '/foo') - self.bm.connect('456', '/foo') - self.bm.pre_disconnect('123', '/foo') - assert self.bm.pending_disconnect == {'/foo': ['123']} - assert not self.bm.is_connected('123', '/foo') - self.bm.pre_disconnect('456', '/foo') - assert self.bm.pending_disconnect == {'/foo': ['123', '456']} - assert not self.bm.is_connected('456', '/foo') - self.bm.disconnect('123', '/foo') - assert self.bm.pending_disconnect == {'/foo': ['456']} - self.bm.disconnect('456', '/foo') + sid1 = self.bm.connect('123', '/foo') + sid2 = self.bm.connect('456', '/foo') + assert self.bm.is_connected(sid1, '/foo') + assert self.bm.pre_disconnect(sid1, '/foo') == '123' + assert self.bm.pending_disconnect == {'/foo': [sid1]} + assert not self.bm.is_connected(sid1, '/foo') + assert self.bm.pre_disconnect(sid2, '/foo') == '456' + assert self.bm.pending_disconnect == {'/foo': [sid1, sid2]} + assert not self.bm.is_connected(sid2, '/foo') + self.bm.disconnect(sid1, '/foo') + assert self.bm.pending_disconnect == {'/foo': [sid2]} + self.bm.disconnect(sid2, '/foo') assert self.bm.pending_disconnect == {} def test_disconnect(self): @@ -66,6 +67,8 @@ def test_disconnect_default_namespace(self): sid4 = self.bm.connect('456', '/foo') assert self.bm.is_connected(sid1, '/') assert self.bm.is_connected(sid2, '/foo') + assert not self.bm.is_connected(sid2, '/') + assert not self.bm.is_connected(sid1, '/foo') self.bm.disconnect(sid1, '/') assert not self.bm.is_connected(sid1, '/') assert self.bm.is_connected(sid2, '/foo') @@ -100,14 +103,17 @@ def test_disconnect_all(self): assert self.bm.rooms == {} def test_disconnect_with_callbacks(self): - self.bm.connect('123', '/') - self.bm.connect('123', '/foo') - self.bm._generate_ack_id('123', '/', 'f') - self.bm._generate_ack_id('123', '/foo', 'g') - self.bm.disconnect('123', '/foo') - assert '/foo' not in self.bm.callbacks['123'] - self.bm.disconnect('123', '/') - assert '123' not in self.bm.callbacks + sid1 = self.bm.connect('123', '/') + sid2 = self.bm.connect('123', '/foo') + sid3 = self.bm.connect('456', '/foo') + self.bm._generate_ack_id(sid1, 'f') + self.bm._generate_ack_id(sid2, 'g') + self.bm._generate_ack_id(sid3, 'h') + self.bm.disconnect(sid2, '/foo') + assert sid2 not in self.bm.callbacks + self.bm.disconnect(sid1, '/') + assert sid1 not in self.bm.callbacks + assert sid3 in self.bm.callbacks def test_disconnect_bad_namespace(self): self.bm.connect('123', '/') @@ -115,26 +121,28 @@ def test_disconnect_bad_namespace(self): self.bm.disconnect('123', '/bar') # should not assert def test_trigger_callback(self): - self.bm.connect('123', '/') - self.bm.connect('123', '/foo') + sid1 = self.bm.connect('123', '/') + sid2 = self.bm.connect('123', '/foo') cb = mock.MagicMock() - id1 = self.bm._generate_ack_id('123', '/', cb) - id2 = self.bm._generate_ack_id('123', '/foo', cb) - self.bm.trigger_callback('123', '/', id1, ['foo']) - self.bm.trigger_callback('123', '/foo', id2, ['bar', 'baz']) - assert cb.call_count == 2 + id1 = self.bm._generate_ack_id(sid1, cb) + id2 = self.bm._generate_ack_id(sid2, cb) + id3 = self.bm._generate_ack_id(sid1, cb) + self.bm.trigger_callback(sid1, id1, ['foo']) + self.bm.trigger_callback(sid1, id3, ['bar']) + self.bm.trigger_callback(sid2, id2, ['bar', 'baz']) + assert cb.call_count == 3 cb.assert_any_call('foo') + cb.assert_any_call('bar') cb.assert_any_call('bar', 'baz') def test_invalid_callback(self): - self.bm.connect('123', '/') + sid = self.bm.connect('123', '/') cb = mock.MagicMock() - id = self.bm._generate_ack_id('123', '/', cb) + id = self.bm._generate_ack_id(sid, cb) # these should not raise an exception - self.bm.trigger_callback('124', '/', id, ['foo']) - self.bm.trigger_callback('123', '/foo', id, ['foo']) - self.bm.trigger_callback('123', '/', id + 1, ['foo']) + self.bm.trigger_callback('xxx', id, ['foo']) + self.bm.trigger_callback(sid, id + 1, ['foo']) assert cb.call_count == 0 def test_get_namespaces(self): @@ -147,19 +155,21 @@ def test_get_namespaces(self): assert '/foo' in namespaces def test_get_participants(self): - self.bm.connect('123', '/') - self.bm.connect('456', '/') - sid = self.bm.connect('789', '/') - self.bm.disconnect(sid, '/') - assert sid not in self.bm.rooms['/'][None] + sid1 = self.bm.connect('123', '/') + sid2 = self.bm.connect('456', '/') + sid3 = self.bm.connect('789', '/') + self.bm.disconnect(sid3, '/') + assert sid3 not in self.bm.rooms['/'][None] participants = list(self.bm.get_participants('/', None)) assert len(participants) == 2 - assert sid not in participants + assert (sid1, '123') in participants + assert (sid2, '456') in participants + assert (sid3, '789') not in participants def test_leave_invalid_room(self): - self.bm.connect('123', '/foo') - self.bm.leave_room('123', '/foo', 'baz') - self.bm.leave_room('123', '/bar', 'baz') + sid = self.bm.connect('123', '/foo') + self.bm.leave_room(sid, '/foo', 'baz') + self.bm.leave_room(sid, '/bar', 'baz') def test_no_room(self): rooms = self.bm.get_rooms('123', '/foo') @@ -270,7 +280,7 @@ def test_emit_with_callback(self): self.bm.emit( 'my event', {'foo': 'bar'}, namespace='/foo', callback='cb' ) - self.bm._generate_ack_id.assert_called_once_with(sid, '/foo', 'cb') + self.bm._generate_ack_id.assert_called_once_with(sid, 'cb') self.bm.server._emit_internal.assert_called_once_with( '123', 'my event', {'foo': 'bar'}, '/foo', 11 ) diff --git a/tests/common/test_pubsub_manager.py b/tests/common/test_pubsub_manager.py index 7c02b4d2..3347e175 100644 --- a/tests/common/test_pubsub_manager.py +++ b/tests/common/test_pubsub_manager.py @@ -149,9 +149,9 @@ def test_emit_with_callback_missing_room(self): self.pm.emit('foo', 'bar', callback='cb') def test_emit_with_ignore_queue(self): - self.pm.connect('123', '/') + sid = self.pm.connect('123', '/') self.pm.emit( - 'foo', 'bar', room='123', namespace='/', ignore_queue=True + 'foo', 'bar', room=sid, namespace='/', ignore_queue=True ) self.pm._publish.assert_not_called() self.pm.server._emit_internal.assert_called_once_with( @@ -159,11 +159,11 @@ def test_emit_with_ignore_queue(self): ) def test_can_disconnect(self): - self.pm.connect('123', '/') - assert self.pm.can_disconnect('123', '/') - self.pm.can_disconnect('123', '/foo') + sid = self.pm.connect('123', '/') + assert self.pm.can_disconnect(sid, '/') + self.pm.can_disconnect(sid, '/foo') self.pm._publish.assert_called_once_with( - {'method': 'disconnect', 'sid': '123', 'namespace': '/foo'} + {'method': 'disconnect', 'sid': sid, 'namespace': '/foo'} ) def test_close_room(self): @@ -277,7 +277,7 @@ def test_handle_callback(self): 'args': ('one', 2), } ) - trigger.assert_called_once_with('sid', '/', 123, ('one', 2)) + trigger.assert_called_once_with('sid', 123, ('one', 2)) def test_handle_callback_bad_host_id(self): with mock.patch.object(self.pm, 'trigger_callback') as trigger: From 0c91e92eb1b8d8d5b5d8de2ad6b45a608e6507aa Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 5 Dec 2020 18:14:40 +0000 Subject: [PATCH 186/571] v5 protocol: client unit tests --- socketio/asyncio_client.py | 13 +- socketio/client.py | 16 +- tests/asyncio/test_asyncio_client.py | 221 ++++++++++++------------ tests/common/test_client.py | 248 ++++++++++++++------------- 4 files changed, 259 insertions(+), 239 deletions(-) diff --git a/socketio/asyncio_client.py b/socketio/asyncio_client.py index 029a1ea5..e8f30139 100644 --- a/socketio/asyncio_client.py +++ b/socketio/asyncio_client.py @@ -96,8 +96,10 @@ async def connect(self, url, headers={}, transports=None, self.socketio_path = socketio_path if namespaces is None: - namespaces = set(self.handlers.keys()).union( - set(self.namespace_handlers.keys())) + namespaces = list(set(self.handlers.keys()).union( + set(self.namespace_handlers.keys()))) + if len(namespaces) == 0: + namespaces = ['/'] elif isinstance(namespaces, six.string_types): namespaces = [namespaces] self.connection_namespaces = namespaces @@ -293,10 +295,10 @@ async def _send_packet(self, pkt): async def _handle_connect(self, namespace, data): namespace = namespace or '/' - self.logger.info('Namespace {} is connected'.format(namespace)) if namespace not in self.namespaces: + self.logger.info('Namespace {} is connected'.format(namespace)) self.namespaces[namespace] = (data or {}).get('sid', self.sid) - await self._trigger_event('connect', namespace=namespace) + await self._trigger_event('connect', namespace=namespace) async def _handle_disconnect(self, namespace): if not self.connected: @@ -306,6 +308,7 @@ async def _handle_disconnect(self, namespace): if namespace in self.namespaces: del self.namespaces[namespace] if not self.namespaces: + self.connected = False await self.eio.disconnect(abort=True) async def _handle_event(self, namespace, id, data): @@ -351,7 +354,7 @@ async def _handle_error(self, namespace, data): data = (data,) await self._trigger_event('connect_error', namespace, *data) if namespace in self.namespaces: - self.namespaces.remove(namespace) + del self.namespaces[namespace] if namespace == '/': self.namespaces = {} self.connected = False diff --git a/socketio/client.py b/socketio/client.py index 9c5aa120..ab685e8e 100644 --- a/socketio/client.py +++ b/socketio/client.py @@ -108,8 +108,7 @@ def __init__(self, reconnection=True, reconnection_attempts=0, self.logger = logger else: self.logger = default_logger - if not logging.root.handlers and \ - self.logger.level == logging.NOTSET: + if self.logger.level == logging.NOTSET: if logger: self.logger.setLevel(logging.INFO) else: @@ -119,7 +118,7 @@ def __init__(self, reconnection=True, reconnection_attempts=0, self.connection_url = None self.connection_headers = None self.connection_transports = None - self.connection_namespaces = None + self.connection_namespaces = [] self.socketio_path = None self.sid = None @@ -265,8 +264,10 @@ def connect(self, url, headers={}, transports=None, self.socketio_path = socketio_path if namespaces is None: - namespaces = set(self.handlers.keys()).union( - set(self.namespace_handlers.keys())) + namespaces = list(set(self.handlers.keys()).union( + set(self.namespace_handlers.keys()))) + if len(namespaces) == 0: + namespaces = ['/'] elif isinstance(namespaces, six.string_types): namespaces = [namespaces] self.connection_namespaces = namespaces @@ -476,10 +477,10 @@ def _generate_ack_id(self, namespace, callback): def _handle_connect(self, namespace, data): namespace = namespace or '/' - self.logger.info('Namespace {} is connected'.format(namespace)) if namespace not in self.namespaces: + self.logger.info('Namespace {} is connected'.format(namespace)) self.namespaces[namespace] = (data or {}).get('sid', self.sid) - self._trigger_event('connect', namespace=namespace) + self._trigger_event('connect', namespace=namespace) def _handle_disconnect(self, namespace): if not self.connected: @@ -489,6 +490,7 @@ def _handle_disconnect(self, namespace): if namespace in self.namespaces: del self.namespaces[namespace] if not self.namespaces: + self.connected = False self.eio.disconnect(abort=True) def _handle_event(self, namespace, id, data): diff --git a/tests/asyncio/test_asyncio_client.py b/tests/asyncio/test_asyncio_client.py index 4c4d70ec..590b4ea0 100644 --- a/tests/asyncio/test_asyncio_client.py +++ b/tests/asyncio/test_asyncio_client.py @@ -70,7 +70,6 @@ def test_connect(self): assert c.connection_transports == 'transports' assert c.connection_namespaces == ['/foo', '/', '/bar'] assert c.socketio_path == 'path' - assert c.namespaces == ['/foo', '/bar'] c.eio.connect.mock.assert_called_once_with( 'url', headers='headers', @@ -95,7 +94,6 @@ def test_connect_one_namespace(self): assert c.connection_transports == 'transports' assert c.connection_namespaces == ['/foo'] assert c.socketio_path == 'path' - assert c.namespaces == ['/foo'] c.eio.connect.mock.assert_called_once_with( 'url', headers='headers', @@ -119,9 +117,32 @@ def test_connect_default_namespaces(self): assert c.connection_url == 'url' assert c.connection_headers == 'headers' assert c.connection_transports == 'transports' - assert c.connection_namespaces is None + assert c.connection_namespaces == ['/', '/foo'] or \ + c.connection_namespaces == ['/foo', '/'] + assert c.socketio_path == 'path' + c.eio.connect.mock.assert_called_once_with( + 'url', + headers='headers', + transports='transports', + engineio_path='path', + ) + + def test_connect_no_namespaces(self): + c = asyncio_client.AsyncClient() + c.eio.connect = AsyncMock() + _run( + c.connect( + 'url', + headers='headers', + transports='transports', + socketio_path='path', + ) + ) + assert c.connection_url == 'url' + assert c.connection_headers == 'headers' + assert c.connection_transports == 'transports' + assert c.connection_namespaces == ['/'] assert c.socketio_path == 'path' - assert c.namespaces == ['/foo'] c.eio.connect.mock.assert_called_once_with( 'url', headers='headers', @@ -186,11 +207,11 @@ async def fake_wait(): def test_emit_no_arguments(self): c = asyncio_client.AsyncClient() + c.namespaces = {'/': '1'} c._send_packet = AsyncMock() _run(c.emit('foo')) expected_packet = packet.Packet( - packet.EVENT, namespace='/', data=['foo'], id=None, binary=False - ) + packet.EVENT, namespace='/', data=['foo'], id=None) assert c._send_packet.mock.call_count == 1 assert ( c._send_packet.mock.call_args_list[0][0][0].encode() @@ -199,6 +220,7 @@ def test_emit_no_arguments(self): def test_emit_one_argument(self): c = asyncio_client.AsyncClient() + c.namespaces = {'/': '1'} c._send_packet = AsyncMock() _run(c.emit('foo', 'bar')) expected_packet = packet.Packet( @@ -206,7 +228,6 @@ def test_emit_one_argument(self): namespace='/', data=['foo', 'bar'], id=None, - binary=False, ) assert c._send_packet.mock.call_count == 1 assert ( @@ -216,6 +237,7 @@ def test_emit_one_argument(self): def test_emit_one_argument_list(self): c = asyncio_client.AsyncClient() + c.namespaces = {'/': '1'} c._send_packet = AsyncMock() _run(c.emit('foo', ['bar', 'baz'])) expected_packet = packet.Packet( @@ -223,7 +245,6 @@ def test_emit_one_argument_list(self): namespace='/', data=['foo', ['bar', 'baz']], id=None, - binary=False, ) assert c._send_packet.mock.call_count == 1 assert ( @@ -233,6 +254,7 @@ def test_emit_one_argument_list(self): def test_emit_two_arguments(self): c = asyncio_client.AsyncClient() + c.namespaces = {'/': '1'} c._send_packet = AsyncMock() _run(c.emit('foo', ('bar', 'baz'))) expected_packet = packet.Packet( @@ -240,7 +262,6 @@ def test_emit_two_arguments(self): namespace='/', data=['foo', 'bar', 'baz'], id=None, - binary=False, ) assert c._send_packet.mock.call_count == 1 assert ( @@ -250,12 +271,11 @@ def test_emit_two_arguments(self): def test_emit_namespace(self): c = asyncio_client.AsyncClient() - c.namespaces = ['/foo'] + c.namespaces = {'/foo': '1'} c._send_packet = AsyncMock() _run(c.emit('foo', namespace='/foo')) expected_packet = packet.Packet( - packet.EVENT, namespace='/foo', data=['foo'], id=None, binary=False - ) + packet.EVENT, namespace='/foo', data=['foo'], id=None) assert c._send_packet.mock.call_count == 1 assert ( c._send_packet.mock.call_args_list[0][0][0].encode() @@ -264,7 +284,7 @@ def test_emit_namespace(self): def test_emit_unknown_namespace(self): c = asyncio_client.AsyncClient() - c.namespaces = ['/foo'] + c.namespaces = {'/foo': '1'} with pytest.raises(exceptions.BadNamespaceError): _run(c.emit('foo', namespace='/bar')) @@ -272,10 +292,10 @@ def test_emit_with_callback(self): c = asyncio_client.AsyncClient() c._send_packet = AsyncMock() c._generate_ack_id = mock.MagicMock(return_value=123) + c.namespaces = {'/': '1'} _run(c.emit('foo', callback='cb')) expected_packet = packet.Packet( - packet.EVENT, namespace='/', data=['foo'], id=123, binary=False - ) + packet.EVENT, namespace='/', data=['foo'], id=123) assert c._send_packet.mock.call_count == 1 assert ( c._send_packet.mock.call_args_list[0][0][0].encode() @@ -285,13 +305,12 @@ def test_emit_with_callback(self): def test_emit_namespace_with_callback(self): c = asyncio_client.AsyncClient() - c.namespaces = ['/foo'] + c.namespaces = {'/foo': '1'} c._send_packet = AsyncMock() c._generate_ack_id = mock.MagicMock(return_value=123) _run(c.emit('foo', namespace='/foo', callback='cb')) expected_packet = packet.Packet( - packet.EVENT, namespace='/foo', data=['foo'], id=123, binary=False - ) + packet.EVENT, namespace='/foo', data=['foo'], id=123) assert c._send_packet.mock.call_count == 1 assert ( c._send_packet.mock.call_args_list[0][0][0].encode() @@ -300,7 +319,8 @@ def test_emit_namespace_with_callback(self): c._generate_ack_id.assert_called_once_with('/foo', 'cb') def test_emit_binary(self): - c = asyncio_client.AsyncClient(binary=True) + c = asyncio_client.AsyncClient() + c.namespaces = {'/': '1'} c._send_packet = AsyncMock() _run(c.emit('foo', b'bar')) expected_packet = packet.Packet( @@ -308,7 +328,6 @@ def test_emit_binary(self): namespace='/', data=['foo', b'bar'], id=None, - binary=True, ) assert c._send_packet.mock.call_count == 1 assert ( @@ -317,7 +336,8 @@ def test_emit_binary(self): ) def test_emit_not_binary(self): - c = asyncio_client.AsyncClient(binary=False) + c = asyncio_client.AsyncClient() + c.namespaces = {'/': '1'} c._send_packet = AsyncMock() _run(c.emit('foo', 'bar')) expected_packet = packet.Packet( @@ -325,7 +345,6 @@ def test_emit_not_binary(self): namespace='/', data=['foo', 'bar'], id=None, - binary=False, ) assert c._send_packet.mock.call_count == 1 assert ( @@ -351,7 +370,7 @@ def test_send_with_defaults(self): def test_call(self): c = asyncio_client.AsyncClient() - + c.namespaces = {'/': '1'} async def fake_event_wait(): c._generate_ack_id.call_args_list[0][0][1]('foo', 321) @@ -361,8 +380,7 @@ async def fake_event_wait(): c.eio.create_event.return_value.wait = fake_event_wait assert _run(c.call('foo')) == ('foo', 321) expected_packet = packet.Packet( - packet.EVENT, namespace='/', data=['foo'], id=123, binary=False - ) + packet.EVENT, namespace='/', data=['foo'], id=123) assert c._send_packet.mock.call_count == 1 assert ( c._send_packet.mock.call_args_list[0][0][0].encode() @@ -371,6 +389,7 @@ async def fake_event_wait(): def test_call_with_timeout(self): c = asyncio_client.AsyncClient() + c.namespaces = {'/': '1'} async def fake_event_wait(): await asyncio.sleep(1) @@ -382,8 +401,7 @@ async def fake_event_wait(): with pytest.raises(exceptions.TimeoutError): _run(c.call('foo', timeout=0.01)) expected_packet = packet.Packet( - packet.EVENT, namespace='/', data=['foo'], id=123, binary=False - ) + packet.EVENT, namespace='/', data=['foo'], id=123) assert c._send_packet.mock.call_count == 1 assert ( c._send_packet.mock.call_args_list[0][0][0].encode() @@ -392,6 +410,7 @@ async def fake_event_wait(): def test_disconnect(self): c = asyncio_client.AsyncClient() + c.namespaces = {'/': '1'} c._trigger_event = AsyncMock() c._send_packet = AsyncMock() c.eio = mock.MagicMock() @@ -409,7 +428,7 @@ def test_disconnect(self): def test_disconnect_namespaces(self): c = asyncio_client.AsyncClient() - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = AsyncMock() c._send_packet = AsyncMock() c.eio = mock.MagicMock() @@ -417,7 +436,7 @@ def test_disconnect_namespaces(self): c.eio.state = 'connected' _run(c.disconnect()) assert c._trigger_event.mock.call_count == 0 - assert c._send_packet.mock.call_count == 3 + assert c._send_packet.mock.call_count == 2 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/foo') assert ( c._send_packet.mock.call_args_list[0][0][0].encode() @@ -428,11 +447,6 @@ def test_disconnect_namespaces(self): c._send_packet.mock.call_args_list[1][0][0].encode() == expected_packet.encode() ) - expected_packet = packet.Packet(packet.DISCONNECT, namespace='/') - assert ( - c._send_packet.mock.call_args_list[2][0][0].encode() - == expected_packet.encode() - ) def test_start_background_task(self): c = asyncio_client.AsyncClient() @@ -451,67 +465,55 @@ def test_sleep(self): def test_send_packet(self): c = asyncio_client.AsyncClient() c.eio.send = AsyncMock() - _run(c._send_packet(packet.Packet(packet.EVENT, 'foo', binary=False))) - c.eio.send.mock.assert_called_once_with('2"foo"', binary=False) + _run(c._send_packet(packet.Packet(packet.EVENT, 'foo'))) + c.eio.send.mock.assert_called_once_with('2"foo"') def test_send_packet_binary(self): c = asyncio_client.AsyncClient() c.eio.send = AsyncMock() - _run(c._send_packet(packet.Packet(packet.EVENT, b'foo', binary=True))) + _run(c._send_packet(packet.Packet(packet.EVENT, b'foo'))) assert c.eio.send.mock.call_args_list == [ - mock.call('51-{"_placeholder":true,"num":0}', binary=False), - mock.call(b'foo', binary=True), + mock.call('51-{"_placeholder":true,"num":0}'), + mock.call(b'foo'), ] or c.eio.send.mock.call_args_list == [ - mock.call('51-{"num":0,"_placeholder":true}', binary=False), - mock.call(b'foo', binary=True), + mock.call('51-{"num":0,"_placeholder":true}'), + mock.call(b'foo'), ] - def test_send_packet_default_binary_py3(self): + def test_send_packet_default_binary(self): c = asyncio_client.AsyncClient() c.eio.send = AsyncMock() _run(c._send_packet(packet.Packet(packet.EVENT, 'foo'))) - c.eio.send.mock.assert_called_once_with('2"foo"', binary=False) + c.eio.send.mock.assert_called_once_with('2"foo"') def test_handle_connect(self): c = asyncio_client.AsyncClient() c._trigger_event = AsyncMock() c._send_packet = AsyncMock() - _run(c._handle_connect('/')) + _run(c._handle_connect('/', {'sid': '123'})) c._trigger_event.mock.assert_called_once_with('connect', namespace='/') c._send_packet.mock.assert_not_called() def test_handle_connect_with_namespaces(self): c = asyncio_client.AsyncClient() - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = AsyncMock() c._send_packet = AsyncMock() - _run(c._handle_connect('/')) + _run(c._handle_connect('/', {'sid': '3'})) c._trigger_event.mock.assert_called_once_with('connect', namespace='/') - assert c._send_packet.mock.call_count == 2 - expected_packet = packet.Packet(packet.CONNECT, namespace='/foo') - assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() - == expected_packet.encode() - ) - expected_packet = packet.Packet(packet.CONNECT, namespace='/bar') - assert ( - c._send_packet.mock.call_args_list[1][0][0].encode() - == expected_packet.encode() - ) + assert c.namespaces == {'/': '3', '/foo': '1', '/bar': '2'} def test_handle_connect_namespace(self): c = asyncio_client.AsyncClient() - c.namespaces = ['/foo'] + c.namespaces = {'/foo': '1'} c._trigger_event = AsyncMock() c._send_packet = AsyncMock() - _run(c._handle_connect('/foo')) - _run(c._handle_connect('/bar')) - assert c._trigger_event.mock.call_args_list == [ - mock.call('connect', namespace='/foo'), - mock.call('connect', namespace='/bar'), - ] - c._send_packet.mock.assert_not_called() - assert c.namespaces == ['/foo', '/bar'] + _run(c._handle_connect('/foo', {'sid': '123'})) + _run(c._handle_connect('/bar', {'sid': '2'})) + assert c._trigger_event.mock.call_count == 1 + c._trigger_event.mock.assert_called_once_with( + 'connect', namespace='/bar') + assert c.namespaces == {'/foo': '1', '/bar': '2'} def test_handle_disconnect(self): c = asyncio_client.AsyncClient() @@ -528,38 +530,36 @@ def test_handle_disconnect(self): def test_handle_disconnect_namespace(self): c = asyncio_client.AsyncClient() c.connected = True - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = AsyncMock() _run(c._handle_disconnect('/foo')) c._trigger_event.mock.assert_called_once_with( 'disconnect', namespace='/foo' ) - assert c.namespaces == ['/bar'] + assert c.namespaces == {'/bar': '2'} assert c.connected def test_handle_disconnect_unknown_namespace(self): c = asyncio_client.AsyncClient() c.connected = True - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = AsyncMock() _run(c._handle_disconnect('/baz')) c._trigger_event.mock.assert_called_once_with( 'disconnect', namespace='/baz' ) - assert c.namespaces == ['/foo', '/bar'] + assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected - def test_handle_disconnect_all_namespaces(self): + def test_handle_disconnect_default_namespaces(self): c = asyncio_client.AsyncClient() c.connected = True - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = AsyncMock() _run(c._handle_disconnect('/')) - c._trigger_event.mock.assert_any_call('disconnect', namespace='/') - c._trigger_event.mock.assert_any_call('disconnect', namespace='/foo') - c._trigger_event.mock.assert_any_call('disconnect', namespace='/bar') - assert c.namespaces == [] - assert not c.connected + c._trigger_event.mock.assert_called_with('disconnect', namespace='/') + assert c.namespaces == {'/foo': '1', '/bar': '2'} + assert c.connected def test_handle_event(self): c = asyncio_client.AsyncClient() @@ -570,7 +570,7 @@ def test_handle_event(self): ) def test_handle_event_with_id_no_arguments(self): - c = asyncio_client.AsyncClient(binary=True) + c = asyncio_client.AsyncClient() c._trigger_event = AsyncMock(return_value=None) c._send_packet = AsyncMock() _run(c._handle_event('/', 123, ['foo', ('bar', 'baz')])) @@ -579,15 +579,14 @@ def test_handle_event_with_id_no_arguments(self): ) assert c._send_packet.mock.call_count == 1 expected_packet = packet.Packet( - packet.ACK, namespace='/', id=123, data=[], binary=None - ) + packet.ACK, namespace='/', id=123, data=[]) assert ( c._send_packet.mock.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_handle_event_with_id_one_argument(self): - c = asyncio_client.AsyncClient(binary=True) + c = asyncio_client.AsyncClient() c._trigger_event = AsyncMock(return_value='ret') c._send_packet = AsyncMock() _run(c._handle_event('/', 123, ['foo', ('bar', 'baz')])) @@ -596,15 +595,14 @@ def test_handle_event_with_id_one_argument(self): ) assert c._send_packet.mock.call_count == 1 expected_packet = packet.Packet( - packet.ACK, namespace='/', id=123, data=['ret'], binary=None - ) + packet.ACK, namespace='/', id=123, data=['ret']) assert ( c._send_packet.mock.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_handle_event_with_id_one_list_argument(self): - c = asyncio_client.AsyncClient(binary=True) + c = asyncio_client.AsyncClient() c._trigger_event = AsyncMock(return_value=['a', 'b']) c._send_packet = AsyncMock() _run(c._handle_event('/', 123, ['foo', ('bar', 'baz')])) @@ -613,15 +611,14 @@ def test_handle_event_with_id_one_list_argument(self): ) assert c._send_packet.mock.call_count == 1 expected_packet = packet.Packet( - packet.ACK, namespace='/', id=123, data=[['a', 'b']], binary=None - ) + packet.ACK, namespace='/', id=123, data=[['a', 'b']]) assert ( c._send_packet.mock.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_handle_event_with_id_two_arguments(self): - c = asyncio_client.AsyncClient(binary=True) + c = asyncio_client.AsyncClient() c._trigger_event = AsyncMock(return_value=('a', 'b')) c._send_packet = AsyncMock() _run(c._handle_event('/', 123, ['foo', ('bar', 'baz')])) @@ -630,8 +627,7 @@ def test_handle_event_with_id_two_arguments(self): ) assert c._send_packet.mock.call_count == 1 expected_packet = packet.Packet( - packet.ACK, namespace='/', id=123, data=['a', 'b'], binary=None - ) + packet.ACK, namespace='/', id=123, data=['a', 'b']) assert ( c._send_packet.mock.call_args_list[0][0][0].encode() == expected_packet.encode() @@ -665,9 +661,9 @@ def test_handle_error(self): c = asyncio_client.AsyncClient() c.connected = True c._trigger_event = AsyncMock() - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} _run(c._handle_error('/', 'error')) - assert c.namespaces == [] + assert c.namespaces == {} assert not c.connected c._trigger_event.mock.assert_called_once_with( 'connect_error', '/', 'error' @@ -677,19 +673,19 @@ def test_handle_error_with_no_arguments(self): c = asyncio_client.AsyncClient() c.connected = True c._trigger_event = AsyncMock() - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} _run(c._handle_error('/', None)) - assert c.namespaces == [] + assert c.namespaces == {} assert not c.connected c._trigger_event.mock.assert_called_once_with('connect_error', '/') def test_handle_error_namespace(self): c = asyncio_client.AsyncClient() c.connected = True - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = AsyncMock() _run(c._handle_error('/bar', ['error', 'message'])) - assert c.namespaces == ['/foo'] + assert c.namespaces == {'/foo': '1'} assert c.connected c._trigger_event.mock.assert_called_once_with( 'connect_error', '/bar', 'error', 'message' @@ -698,19 +694,19 @@ def test_handle_error_namespace(self): def test_handle_error_namespace_with_no_arguments(self): c = asyncio_client.AsyncClient() c.connected = True - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = AsyncMock() _run(c._handle_error('/bar', None)) - assert c.namespaces == ['/foo'] + assert c.namespaces == {'/foo': '1'} assert c.connected c._trigger_event.mock.assert_called_once_with('connect_error', '/bar') def test_handle_error_unknown_namespace(self): c = asyncio_client.AsyncClient() c.connected = True - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} _run(c._handle_error('/baz', 'error')) - assert c.namespaces == ['/foo', '/bar'] + assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected def test_trigger_event(self): @@ -839,10 +835,23 @@ def test_handle_reconnect_aborted(self, random, wait_for): def test_eio_connect(self): c = asyncio_client.AsyncClient() + c.connection_namespaces = ['/', '/foo'] + c._send_packet = AsyncMock() c.eio.sid = 'foo' assert c.sid is None - c._handle_eio_connect() + _run(c._handle_eio_connect()) assert c.sid == 'foo' + assert c._send_packet.mock.call_count == 2 + expected_packet = packet.Packet(packet.CONNECT, namespace='/') + assert ( + c._send_packet.mock.call_args_list[0][0][0].encode() + == expected_packet.encode() + ) + expected_packet = packet.Packet(packet.CONNECT, namespace='/foo') + assert ( + c._send_packet.mock.call_args_list[1][0][0].encode() + == expected_packet.encode() + ) def test_handle_eio_message(self): c = asyncio_client.AsyncClient() @@ -852,10 +861,10 @@ def test_handle_eio_message(self): c._handle_ack = AsyncMock() c._handle_error = AsyncMock() - _run(c._handle_eio_message('0')) - c._handle_connect.mock.assert_called_with(None) - _run(c._handle_eio_message('0/foo')) - c._handle_connect.mock.assert_called_with('/foo') + _run(c._handle_eio_message('0{"sid":"123"}')) + c._handle_connect.mock.assert_called_with(None, {'sid': '123'}) + _run(c._handle_eio_message('0/foo,{"sid":"123"}')) + c._handle_connect.mock.assert_called_with('/foo', {'sid': '123'}) _run(c._handle_eio_message('1')) c._handle_disconnect.mock.assert_called_with(None) _run(c._handle_eio_message('1/foo')) @@ -895,6 +904,7 @@ def test_handle_eio_message(self): def test_eio_disconnect(self): c = asyncio_client.AsyncClient() + c.namespaces = {'/': '1'} c.connected = True c._trigger_event = AsyncMock() c.sid = 'foo' @@ -908,15 +918,14 @@ def test_eio_disconnect(self): def test_eio_disconnect_namespaces(self): c = asyncio_client.AsyncClient() + c.namespaces = {'/foo': '1', '/bar': '2'} c.connected = True - c.namespaces = ['/foo', '/bar'] c._trigger_event = AsyncMock() c.sid = 'foo' c.eio.state = 'connected' _run(c._handle_eio_disconnect()) c._trigger_event.mock.assert_any_call('disconnect', namespace='/foo') c._trigger_event.mock.assert_any_call('disconnect', namespace='/bar') - c._trigger_event.mock.assert_any_call('disconnect', namespace='/') assert c.sid is None assert not c.connected diff --git a/tests/common/test_client.py b/tests/common/test_client.py index b20ed01d..7e2a037c 100644 --- a/tests/common/test_client.py +++ b/tests/common/test_client.py @@ -37,7 +37,6 @@ def test_create(self, engineio_client_class): reconnection_delay=5, reconnection_delay_max=10, randomization_factor=0.2, - binary=True, foo='bar', ) assert not c.reconnection @@ -45,16 +44,15 @@ def test_create(self, engineio_client_class): assert c.reconnection_delay == 5 assert c.reconnection_delay_max == 10 assert c.randomization_factor == 0.2 - assert c.binary engineio_client_class().assert_called_once_with(foo='bar') assert c.connection_url is None assert c.connection_headers is None assert c.connection_transports is None - assert c.connection_namespaces is None + assert c.connection_namespaces == [] assert c.socketio_path is None assert c.sid is None - assert c.namespaces == [] + assert c.namespaces == {} assert c.handlers == {} assert c.namespace_handlers == {} assert c.callbacks == {} @@ -174,7 +172,6 @@ def test_connect(self): assert c.connection_transports == 'transports' assert c.connection_namespaces == ['/foo', '/', '/bar'] assert c.socketio_path == 'path' - assert c.namespaces == ['/foo', '/bar'] c.eio.connect.assert_called_once_with( 'url', headers='headers', @@ -197,7 +194,6 @@ def test_connect_one_namespace(self): assert c.connection_transports == 'transports' assert c.connection_namespaces == ['/foo'] assert c.socketio_path == 'path' - assert c.namespaces == ['/foo'] c.eio.connect.assert_called_once_with( 'url', headers='headers', @@ -219,9 +215,30 @@ def test_connect_default_namespaces(self): assert c.connection_url == 'url' assert c.connection_headers == 'headers' assert c.connection_transports == 'transports' - assert c.connection_namespaces is None + assert c.connection_namespaces == ['/foo', '/'] or \ + c.connection_namespaces == ['/', '/foo'] + assert c.socketio_path == 'path' + c.eio.connect.assert_called_once_with( + 'url', + headers='headers', + transports='transports', + engineio_path='path', + ) + + def test_connect_no_namespaces(self): + c = client.Client() + c.eio.connect = mock.MagicMock() + c.connect( + 'url', + headers='headers', + transports='transports', + socketio_path='path', + ) + assert c.connection_url == 'url' + assert c.connection_headers == 'headers' + assert c.connection_transports == 'transports' + assert c.connection_namespaces == ['/'] assert c.socketio_path == 'path' - assert c.namespaces == ['/foo'] c.eio.connect.assert_called_once_with( 'url', headers='headers', @@ -283,13 +300,21 @@ def fake_join(): assert c.eio.wait.call_count == 2 assert c.sleep.call_count == 2 + def test_get_sid(self): + c = client.Client() + c.namespaces = {'/': '1', '/foo': '2'} + assert c.get_sid() == '1' + assert c.get_sid('/') == '1' + assert c.get_sid('/foo') == '2' + assert c.get_sid('/bar') is None + def test_emit_no_arguments(self): c = client.Client() + c.namespaces = {'/': '1'} c._send_packet = mock.MagicMock() c.emit('foo') expected_packet = packet.Packet( - packet.EVENT, namespace='/', data=['foo'], id=None, binary=False - ) + packet.EVENT, namespace='/', data=['foo'], id=None) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() @@ -298,6 +323,7 @@ def test_emit_no_arguments(self): def test_emit_one_argument(self): c = client.Client() + c.namespaces = {'/': '1'} c._send_packet = mock.MagicMock() c.emit('foo', 'bar') expected_packet = packet.Packet( @@ -305,7 +331,6 @@ def test_emit_one_argument(self): namespace='/', data=['foo', 'bar'], id=None, - binary=False, ) assert c._send_packet.call_count == 1 assert ( @@ -315,6 +340,7 @@ def test_emit_one_argument(self): def test_emit_one_argument_list(self): c = client.Client() + c.namespaces = {'/': '1'} c._send_packet = mock.MagicMock() c.emit('foo', ['bar', 'baz']) expected_packet = packet.Packet( @@ -322,7 +348,6 @@ def test_emit_one_argument_list(self): namespace='/', data=['foo', ['bar', 'baz']], id=None, - binary=False, ) assert c._send_packet.call_count == 1 assert ( @@ -332,6 +357,7 @@ def test_emit_one_argument_list(self): def test_emit_two_arguments(self): c = client.Client() + c.namespaces = {'/': '1'} c._send_packet = mock.MagicMock() c.emit('foo', ('bar', 'baz')) expected_packet = packet.Packet( @@ -339,7 +365,6 @@ def test_emit_two_arguments(self): namespace='/', data=['foo', 'bar', 'baz'], id=None, - binary=False, ) assert c._send_packet.call_count == 1 assert ( @@ -353,8 +378,7 @@ def test_emit_namespace(self): c._send_packet = mock.MagicMock() c.emit('foo', namespace='/foo') expected_packet = packet.Packet( - packet.EVENT, namespace='/foo', data=['foo'], id=None, binary=False - ) + packet.EVENT, namespace='/foo', data=['foo'], id=None) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() @@ -369,12 +393,12 @@ def test_emit_unknown_namespace(self): def test_emit_with_callback(self): c = client.Client() + c.namespaces = {'/': '1'} c._send_packet = mock.MagicMock() c._generate_ack_id = mock.MagicMock(return_value=123) c.emit('foo', callback='cb') expected_packet = packet.Packet( - packet.EVENT, namespace='/', data=['foo'], id=123, binary=False - ) + packet.EVENT, namespace='/', data=['foo'], id=123) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() @@ -384,13 +408,12 @@ def test_emit_with_callback(self): def test_emit_namespace_with_callback(self): c = client.Client() - c.namespaces = ['/foo'] + c.namespaces = {'/foo': '1'} c._send_packet = mock.MagicMock() c._generate_ack_id = mock.MagicMock(return_value=123) c.emit('foo', namespace='/foo', callback='cb') expected_packet = packet.Packet( - packet.EVENT, namespace='/foo', data=['foo'], id=123, binary=False - ) + packet.EVENT, namespace='/foo', data=['foo'], id=123) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() @@ -399,7 +422,8 @@ def test_emit_namespace_with_callback(self): c._generate_ack_id.assert_called_once_with('/foo', 'cb') def test_emit_binary(self): - c = client.Client(binary=True) + c = client.Client() + c.namespaces = {'/': '1'} c._send_packet = mock.MagicMock() c.emit('foo', b'bar') expected_packet = packet.Packet( @@ -407,7 +431,6 @@ def test_emit_binary(self): namespace='/', data=['foo', b'bar'], id=None, - binary=True, ) assert c._send_packet.call_count == 1 assert ( @@ -416,7 +439,8 @@ def test_emit_binary(self): ) def test_emit_not_binary(self): - c = client.Client(binary=False) + c = client.Client() + c.namespaces = {'/': '1'} c._send_packet = mock.MagicMock() c.emit('foo', 'bar') expected_packet = packet.Packet( @@ -424,7 +448,6 @@ def test_emit_not_binary(self): namespace='/', data=['foo', 'bar'], id=None, - binary=False, ) assert c._send_packet.call_count == 1 assert ( @@ -450,7 +473,7 @@ def test_send_with_defaults(self): def test_call(self): c = client.Client() - + c.namespaces = {'/': '1'} def fake_event_wait(timeout=None): assert timeout == 60 c._generate_ack_id.call_args_list[0][0][1]('foo', 321) @@ -462,8 +485,7 @@ def fake_event_wait(timeout=None): c.eio.create_event.return_value.wait = fake_event_wait assert c.call('foo') == ('foo', 321) expected_packet = packet.Packet( - packet.EVENT, namespace='/', data=['foo'], id=123, binary=False - ) + packet.EVENT, namespace='/', data=['foo'], id=123) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() @@ -472,7 +494,7 @@ def fake_event_wait(timeout=None): def test_call_with_timeout(self): c = client.Client() - + c.namespaces = {'/': '1'} def fake_event_wait(timeout=None): assert timeout == 12 return False @@ -484,8 +506,7 @@ def fake_event_wait(timeout=None): with pytest.raises(exceptions.TimeoutError): c.call('foo', timeout=12) expected_packet = packet.Packet( - packet.EVENT, namespace='/', data=['foo'], id=123, binary=False - ) + packet.EVENT, namespace='/', data=['foo'], id=123) assert c._send_packet.call_count == 1 assert ( c._send_packet.call_args_list[0][0][0].encode() @@ -494,6 +515,7 @@ def fake_event_wait(timeout=None): def test_disconnect(self): c = client.Client() + c.namespaces = {'/': '1'} c._trigger_event = mock.MagicMock() c._send_packet = mock.MagicMock() c.eio = mock.MagicMock() @@ -510,14 +532,14 @@ def test_disconnect(self): def test_disconnect_namespaces(self): c = client.Client() - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._send_packet = mock.MagicMock() c.eio = mock.MagicMock() c.eio.state = 'connected' c.disconnect() assert c._trigger_event.call_count == 0 - assert c._send_packet.call_count == 3 + assert c._send_packet.call_count == 2 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/foo') assert ( c._send_packet.call_args_list[0][0][0].encode() @@ -528,11 +550,6 @@ def test_disconnect_namespaces(self): c._send_packet.call_args_list[1][0][0].encode() == expected_packet.encode() ) - expected_packet = packet.Packet(packet.DISCONNECT, namespace='/') - assert ( - c._send_packet.call_args_list[2][0][0].encode() - == expected_packet.encode() - ) def test_transport(self): c = client.Client() @@ -557,40 +574,26 @@ def test_sleep(self): def test_send_packet(self): c = client.Client() c.eio.send = mock.MagicMock() - c._send_packet(packet.Packet(packet.EVENT, 'foo', binary=False)) - c.eio.send.assert_called_once_with('2"foo"', binary=False) + c._send_packet(packet.Packet(packet.EVENT, 'foo')) + c.eio.send.assert_called_once_with('2"foo"') def test_send_packet_binary(self): c = client.Client() c.eio.send = mock.MagicMock() - c._send_packet(packet.Packet(packet.EVENT, b'foo', binary=True)) + c._send_packet(packet.Packet(packet.EVENT, b'foo')) assert c.eio.send.call_args_list == [ - mock.call('51-{"_placeholder":true,"num":0}', binary=False), - mock.call(b'foo', binary=True), + mock.call('51-{"_placeholder":true,"num":0}'), + mock.call(b'foo'), ] or c.eio.send.call_args_list == [ - mock.call('51-{"num":0,"_placeholder":true}', binary=False), - mock.call(b'foo', binary=True), + mock.call('51-{"num":0,"_placeholder":true}'), + mock.call(b'foo'), ] - @unittest.skipIf(sys.version_info < (3, 0), 'only for Python 3') - def test_send_packet_default_binary_py3(self): - c = client.Client() - c.eio.send = mock.MagicMock() - c._send_packet(packet.Packet(packet.EVENT, 'foo')) - c.eio.send.assert_called_once_with('2"foo"', binary=False) - - @unittest.skipIf(sys.version_info >= (3, 0), 'only for Python 2') - def test_send_packet_default_binary_py2(self): + def test_send_packet_default_binary(self): c = client.Client() c.eio.send = mock.MagicMock() c._send_packet(packet.Packet(packet.EVENT, 'foo')) - assert c.eio.send.call_args_list == [ - mock.call('51-{"_placeholder":true,"num":0}', binary=False), - mock.call(b'foo', binary=True), - ] or c.eio.send.call_args_list == [ - mock.call('51-{"num":0,"_placeholder":true}', binary=False), - mock.call(b'foo', binary=True), - ] + c.eio.send.assert_called_once_with('2"foo"') def test_generate_ack_id(self): c = client.Client() @@ -606,45 +609,34 @@ def test_handle_connect(self): c = client.Client() c._trigger_event = mock.MagicMock() c._send_packet = mock.MagicMock() - c._handle_connect('/') + c._handle_connect('/', {'sid': '123'}) + assert c.namespaces == {'/': '123'} c._trigger_event.assert_called_once_with('connect', namespace='/') c._send_packet.assert_not_called() def test_handle_connect_with_namespaces(self): c = client.Client() - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._send_packet = mock.MagicMock() - c._handle_connect('/') + c._handle_connect('/', {'sid': '3'}) c._trigger_event.assert_called_once_with('connect', namespace='/') - assert c._send_packet.call_count == 2 - expected_packet = packet.Packet(packet.CONNECT, namespace='/foo') - assert ( - c._send_packet.call_args_list[0][0][0].encode() - == expected_packet.encode() - ) - expected_packet = packet.Packet(packet.CONNECT, namespace='/bar') - assert ( - c._send_packet.call_args_list[1][0][0].encode() - == expected_packet.encode() - ) + assert c.namespaces == {'/': '3', '/foo': '1', '/bar': '2'} def test_handle_connect_namespace(self): c = client.Client() - c.namespaces = ['/foo'] + c.namespaces = {'/foo': '1'} c._trigger_event = mock.MagicMock() c._send_packet = mock.MagicMock() - c._handle_connect('/foo') - c._handle_connect('/bar') - assert c._trigger_event.call_args_list == [ - mock.call('connect', namespace='/foo'), - mock.call('connect', namespace='/bar'), - ] - c._send_packet.assert_not_called() - assert c.namespaces == ['/foo', '/bar'] + c._handle_connect('/foo', {'sid': '123'}) + c._handle_connect('/bar', {'sid': '2'}) + assert c._trigger_event.call_count == 1 + c._trigger_event.assert_called_once_with('connect', namespace='/bar') + assert c.namespaces == {'/foo': '1', '/bar': '2'} def test_handle_disconnect(self): c = client.Client() + c.namespace = {'/': '1'} c.connected = True c._trigger_event = mock.MagicMock() c._handle_disconnect('/') @@ -656,38 +648,42 @@ def test_handle_disconnect(self): def test_handle_disconnect_namespace(self): c = client.Client() c.connected = True - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_disconnect('/foo') c._trigger_event.assert_called_once_with( 'disconnect', namespace='/foo' ) - assert c.namespaces == ['/bar'] + assert c.namespaces == {'/bar': '2'} assert c.connected + c._handle_disconnect('/bar') + c._trigger_event.assert_called_with( + 'disconnect', namespace='/bar' + ) + assert c.namespaces == {} + assert not c.connected def test_handle_disconnect_unknown_namespace(self): c = client.Client() c.connected = True - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_disconnect('/baz') c._trigger_event.assert_called_once_with( 'disconnect', namespace='/baz' ) - assert c.namespaces == ['/foo', '/bar'] + assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected - def test_handle_disconnect_all_namespaces(self): + def test_handle_disconnect_default_namespace(self): c = client.Client() c.connected = True - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_disconnect('/') - c._trigger_event.assert_any_call('disconnect', namespace='/') - c._trigger_event.assert_any_call('disconnect', namespace='/foo') - c._trigger_event.assert_any_call('disconnect', namespace='/bar') - assert c.namespaces == [] - assert not c.connected + c._trigger_event.assert_called_with('disconnect', namespace='/') + assert c.namespaces == {'/foo': '1', '/bar': '2'} + assert c.connected def test_handle_event(self): c = client.Client() @@ -696,60 +692,56 @@ def test_handle_event(self): c._trigger_event.assert_called_once_with('foo', '/', ('bar', 'baz')) def test_handle_event_with_id_no_arguments(self): - c = client.Client(binary=True) + c = client.Client() c._trigger_event = mock.MagicMock(return_value=None) c._send_packet = mock.MagicMock() c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_called_once_with('foo', '/', ('bar', 'baz')) assert c._send_packet.call_count == 1 expected_packet = packet.Packet( - packet.ACK, namespace='/', id=123, data=[], binary=None - ) + packet.ACK, namespace='/', id=123, data=[]) assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_handle_event_with_id_one_argument(self): - c = client.Client(binary=True) + c = client.Client() c._trigger_event = mock.MagicMock(return_value='ret') c._send_packet = mock.MagicMock() c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_called_once_with('foo', '/', ('bar', 'baz')) assert c._send_packet.call_count == 1 expected_packet = packet.Packet( - packet.ACK, namespace='/', id=123, data=['ret'], binary=None - ) + packet.ACK, namespace='/', id=123, data=['ret']) assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_handle_event_with_id_one_list_argument(self): - c = client.Client(binary=True) + c = client.Client() c._trigger_event = mock.MagicMock(return_value=['a', 'b']) c._send_packet = mock.MagicMock() c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_called_once_with('foo', '/', ('bar', 'baz')) assert c._send_packet.call_count == 1 expected_packet = packet.Packet( - packet.ACK, namespace='/', id=123, data=[['a', 'b']], binary=None - ) + packet.ACK, namespace='/', id=123, data=[['a', 'b']]) assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) def test_handle_event_with_id_two_arguments(self): - c = client.Client(binary=True) + c = client.Client() c._trigger_event = mock.MagicMock(return_value=('a', 'b')) c._send_packet = mock.MagicMock() c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_called_once_with('foo', '/', ('bar', 'baz')) assert c._send_packet.call_count == 1 expected_packet = packet.Packet( - packet.ACK, namespace='/', id=123, data=['a', 'b'], binary=None - ) + packet.ACK, namespace='/', id=123, data=['a', 'b']) assert ( c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() @@ -774,30 +766,30 @@ def test_handle_ack_not_found(self): def test_handle_error(self): c = client.Client() c.connected = True - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_error('/', 'error') - assert c.namespaces == [] + assert c.namespaces == {} assert not c.connected c._trigger_event.assert_called_once_with('connect_error', '/', 'error') def test_handle_error_with_no_arguments(self): c = client.Client() c.connected = True - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_error('/', None) - assert c.namespaces == [] + assert c.namespaces == {} assert not c.connected c._trigger_event.assert_called_once_with('connect_error', '/') def test_handle_error_namespace(self): c = client.Client() c.connected = True - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_error('/bar', ['error', 'message']) - assert c.namespaces == ['/foo'] + assert c.namespaces == {'/foo': '1'} assert c.connected c._trigger_event.assert_called_once_with( 'connect_error', '/bar', 'error', 'message' @@ -806,19 +798,19 @@ def test_handle_error_namespace(self): def test_handle_error_namespace_with_no_arguments(self): c = client.Client() c.connected = True - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_error('/bar', None) - assert c.namespaces == ['/foo'] + assert c.namespaces == {'/foo': '1'} assert c.connected c._trigger_event.assert_called_once_with('connect_error', '/bar') def test_handle_error_unknown_namespace(self): c = client.Client() c.connected = True - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/foo': '1', '/bar': '2'} c._handle_error('/baz', 'error') - assert c.namespaces == ['/foo', '/bar'] + assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected def test_trigger_event(self): @@ -927,10 +919,23 @@ def test_handle_reconnect_aborted(self, random): def test_handle_eio_connect(self): c = client.Client() + c.connection_namespaces = ['/', '/foo'] + c._send_packet = mock.MagicMock() c.eio.sid = 'foo' assert c.sid is None c._handle_eio_connect() assert c.sid == 'foo' + assert c._send_packet.call_count == 2 + expected_packet = packet.Packet(packet.CONNECT, namespace='/') + assert ( + c._send_packet.call_args_list[0][0][0].encode() + == expected_packet.encode() + ) + expected_packet = packet.Packet(packet.CONNECT, namespace='/foo') + assert ( + c._send_packet.call_args_list[1][0][0].encode() + == expected_packet.encode() + ) def test_handle_eio_message(self): c = client.Client() @@ -940,10 +945,10 @@ def test_handle_eio_message(self): c._handle_ack = mock.MagicMock() c._handle_error = mock.MagicMock() - c._handle_eio_message('0') - c._handle_connect.assert_called_with(None) - c._handle_eio_message('0/foo') - c._handle_connect.assert_called_with('/foo') + c._handle_eio_message('0{"sid":"123"}') + c._handle_connect.assert_called_with(None, {'sid': '123'}) + c._handle_eio_message('0/foo,{"sid":"123"}') + c._handle_connect.assert_called_with('/foo', {'sid': '123'}) c._handle_eio_message('1') c._handle_disconnect.assert_called_with(None) c._handle_eio_message('1/foo') @@ -981,6 +986,7 @@ def test_handle_eio_message(self): def test_eio_disconnect(self): c = client.Client() + c.namespaces = {'/': '1'} c.connected = True c._trigger_event = mock.MagicMock() c.start_background_task = mock.MagicMock() @@ -994,7 +1000,7 @@ def test_eio_disconnect(self): def test_eio_disconnect_namespaces(self): c = client.Client() c.connected = True - c.namespaces = ['/foo', '/bar'] + c.namespaces = {'/': '1', '/foo': '2', '/bar': '3'} c._trigger_event = mock.MagicMock() c.start_background_task = mock.MagicMock() c.sid = 'foo' From 0ecfbf593daaa3da7cfd164b26b6200328001ca3 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 6 Dec 2020 00:50:05 +0000 Subject: [PATCH 187/571] v5 protocol: server unit tests --- socketio/asyncio_server.py | 18 +- socketio/base_manager.py | 3 +- socketio/server.py | 21 +- tests/asyncio/test_asyncio_server.py | 326 ++++++++++++++------------- tests/common/test_server.py | 293 ++++++++++++------------ 5 files changed, 330 insertions(+), 331 deletions(-) diff --git a/socketio/asyncio_server.py b/socketio/asyncio_server.py index d35d3558..20edbd50 100644 --- a/socketio/asyncio_server.py +++ b/socketio/asyncio_server.py @@ -423,15 +423,11 @@ async def _handle_connect(self, eio_sid, namespace): self.manager.pre_disconnect(sid, namespace) await self._send_packet(eio_sid, packet.Packet( packet.DISCONNECT, data=fail_reason, namespace=namespace)) - elif namespace != '/': + else: await self._send_packet(eio_sid, packet.Packet( packet.CONNECT_ERROR, data=fail_reason, namespace=namespace)) self.manager.disconnect(sid, namespace) - if namespace == '/' and \ - eio_sid in self.environ: # pragma: no cover - del self.environ[eio_sid] - return fail_reason or False elif not self.always_connect: await self._send_packet(eio_sid, packet.Packet( packet.CONNECT, {'sid': sid}, namespace=namespace)) @@ -440,10 +436,11 @@ async def _handle_disconnect(self, eio_sid, namespace): """Handle a client disconnect.""" namespace = namespace or '/' sid = self.manager.sid_from_eio_sid(eio_sid, namespace) - if self.manager.is_connected(sid, namespace): - self.manager.pre_disconnect(sid, namespace=namespace) - await self._trigger_event('disconnect', namespace, sid) - self.manager.disconnect(sid, namespace) + if not self.manager.is_connected(sid, namespace): # pragma: no cover + return + self.manager.pre_disconnect(sid, namespace=namespace) + await self._trigger_event('disconnect', namespace, sid) + self.manager.disconnect(sid, namespace) async def _handle_event(self, eio_sid, namespace, id, data): """Handle an incoming client event.""" @@ -544,7 +541,8 @@ async def _handle_eio_message(self, eio_sid, data): async def _handle_eio_disconnect(self, eio_sid): """Handle Engine.IO disconnect event.""" - await self._handle_disconnect(eio_sid, '/') + for n in list(self.manager.get_namespaces()).copy(): + await self._handle_disconnect(eio_sid, n) if eio_sid in self.environ: del self.environ[eio_sid] diff --git a/socketio/base_manager.py b/socketio/base_manager.py index cec0e918..89c755ba 100644 --- a/socketio/base_manager.py +++ b/socketio/base_manager.py @@ -60,7 +60,8 @@ def is_connected(self, sid, namespace): pass def sid_from_eio_sid(self, eio_sid, namespace): - return self.rooms[namespace][None].inverse.get(eio_sid) + if namespace in self.rooms: + return self.rooms[namespace][None].inverse.get(eio_sid) def can_disconnect(self, sid, namespace): return self.is_connected(sid, namespace) diff --git a/socketio/server.py b/socketio/server.py index 6418e76b..68706373 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -116,8 +116,7 @@ def __init__(self, client_manager=None, logger=False, json=None, self.logger = logger else: self.logger = default_logger - if not logging.root.handlers and \ - self.logger.level == logging.NOTSET: + if self.logger.level == logging.NOTSET: if logger: self.logger.setLevel(logging.INFO) else: @@ -621,15 +620,11 @@ def _handle_connect(self, eio_sid, namespace): self.manager.pre_disconnect(sid, namespace) self._send_packet(eio_sid, packet.Packet( packet.DISCONNECT, data=fail_reason, namespace=namespace)) - elif namespace != '/': + else: self._send_packet(eio_sid, packet.Packet( packet.CONNECT_ERROR, data=fail_reason, namespace=namespace)) self.manager.disconnect(sid, namespace) - if namespace == '/' and \ - eio_sid in self.environ: # pragma: no cover - del self.environ[eio_sid] - return fail_reason or False elif not self.always_connect: self._send_packet(eio_sid, packet.Packet( packet.CONNECT, {'sid': sid}, namespace=namespace)) @@ -638,10 +633,11 @@ def _handle_disconnect(self, eio_sid, namespace): """Handle a client disconnect.""" namespace = namespace or '/' sid = self.manager.sid_from_eio_sid(eio_sid, namespace) - if self.manager.is_connected(sid, namespace): - self.manager.pre_disconnect(sid, namespace=namespace) - self._trigger_event('disconnect', namespace, sid) - self.manager.disconnect(sid, namespace) + if not self.manager.is_connected(sid, namespace): # pragma: no cover + return + self.manager.pre_disconnect(sid, namespace=namespace) + self._trigger_event('disconnect', namespace, sid) + self.manager.disconnect(sid, namespace) def _handle_event(self, eio_sid, namespace, id, data): """Handle an incoming client event.""" @@ -731,7 +727,8 @@ def _handle_eio_message(self, eio_sid, data): def _handle_eio_disconnect(self, eio_sid): """Handle Engine.IO disconnect event.""" - self._handle_disconnect(eio_sid, '/') + for n in list(self.manager.get_namespaces()).copy(): + self._handle_disconnect(eio_sid, n) if eio_sid in self.environ: del self.environ[eio_sid] diff --git a/tests/asyncio/test_asyncio_server.py b/tests/asyncio/test_asyncio_server.py index 9b4cbaba..545531f7 100644 --- a/tests/asyncio/test_asyncio_server.py +++ b/tests/asyncio/test_asyncio_server.py @@ -36,7 +36,8 @@ def _run(coro): @unittest.skipIf(sys.version_info < (3, 5), 'only for Python 3.5+') -@mock.patch('socketio.server.engineio.AsyncServer') +@mock.patch('socketio.server.engineio.AsyncServer', **{ + 'return_value.generate_id.side_effect': [str(i) for i in range(1, 10)]}) class TestAsyncServer(unittest.TestCase): def tearDown(self): # restore JSON encoder, in case a test changed it @@ -61,7 +62,6 @@ def test_create(self, eio): eio.assert_called_once_with(**{'foo': 'bar', 'async_handlers': False}) assert s.manager == mgr assert s.eio.on.call_count == 3 - assert not s.binary assert s.async_handlers def test_attach(self, eio): @@ -296,7 +296,7 @@ def test_emit_internal(self, eio): s = asyncio_server.AsyncServer() _run(s._emit_internal('123', 'my event', 'my data', namespace='/foo')) s.eio.send.mock.assert_called_once_with( - '123', '2/foo,["my event","my data"]', binary=False + '123', '2/foo,["my event","my data"]' ) def test_emit_internal_with_tuple(self, eio): @@ -308,7 +308,7 @@ def test_emit_internal_with_tuple(self, eio): ) ) s.eio.send.mock.assert_called_once_with( - '123', '2/foo,["my event","foo","bar"]', binary=False + '123', '2/foo,["my event","foo","bar"]' ) def test_emit_internal_with_list(self, eio): @@ -320,7 +320,7 @@ def test_emit_internal_with_list(self, eio): ) ) s.eio.send.mock.assert_called_once_with( - '123', '2/foo,["my event",["foo","bar"]]', binary=False + '123', '2/foo,["my event",["foo","bar"]]' ) def test_emit_internal_with_none(self, eio): @@ -328,20 +328,20 @@ def test_emit_internal_with_none(self, eio): s = asyncio_server.AsyncServer() _run(s._emit_internal('123', 'my event', None, namespace='/foo')) s.eio.send.mock.assert_called_once_with( - '123', '2/foo,["my event"]', binary=False + '123', '2/foo,["my event"]' ) def test_emit_internal_with_callback(self, eio): eio.return_value.send = AsyncMock() s = asyncio_server.AsyncServer() - id = s.manager._generate_ack_id('123', '/foo', 'cb') + id = s.manager._generate_ack_id('1', 'cb') _run( s._emit_internal( '123', 'my event', 'my data', namespace='/foo', id=id ) ) s.eio.send.mock.assert_called_once_with( - '123', '2/foo,1["my event","my data"]', binary=False + '123', '2/foo,1["my event","my data"]' ) def test_emit_internal_default_namespace(self, eio): @@ -349,7 +349,7 @@ def test_emit_internal_default_namespace(self, eio): s = asyncio_server.AsyncServer() _run(s._emit_internal('123', 'my event', 'my data')) s.eio.send.mock.assert_called_once_with( - '123', '2["my event","my data"]', binary=False + '123', '2["my event","my data"]' ) def test_emit_internal_binary(self, eio): @@ -368,180 +368,186 @@ def test_transport(self, eio): def test_handle_connect(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer(client_manager=mgr) + s = asyncio_server.AsyncServer() + s.manager.initialize = mock.MagicMock() handler = mock.MagicMock() s.on('connect', handler) _run(s._handle_eio_connect('123', 'environ')) - handler.assert_called_once_with('123', 'environ') - s.manager.connect.assert_called_once_with('123', '/') - s.eio.send.mock.assert_called_once_with('123', '0', binary=False) - assert mgr.initialize.call_count == 1 + _run(s._handle_eio_message('123', '0')) + assert s.manager.is_connected('1', '/') + handler.assert_called_once_with('1', 'environ') + s.eio.send.mock.assert_called_once_with('123', '0{"sid":"1"}') + assert s.manager.initialize.call_count == 1 _run(s._handle_eio_connect('456', 'environ')) - assert mgr.initialize.call_count == 1 + _run(s._handle_eio_message('456', '0')) + assert s.manager.initialize.call_count == 1 def test_handle_connect_async(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer(client_manager=mgr) + s = asyncio_server.AsyncServer() + s.manager.initialize = mock.MagicMock() handler = AsyncMock() s.on('connect', handler) _run(s._handle_eio_connect('123', 'environ')) - handler.mock.assert_called_once_with('123', 'environ') - s.manager.connect.assert_called_once_with('123', '/') - s.eio.send.mock.assert_called_once_with('123', '0', binary=False) - assert mgr.initialize.call_count == 1 + _run(s._handle_eio_message('123', '0')) + assert s.manager.is_connected('1', '/') + handler.mock.assert_called_once_with('1', 'environ') + s.eio.send.mock.assert_called_once_with('123', '0{"sid":"1"}') + assert s.manager.initialize.call_count == 1 _run(s._handle_eio_connect('456', 'environ')) - assert mgr.initialize.call_count == 1 + _run(s._handle_eio_message('456', '0')) + assert s.manager.initialize.call_count == 1 def test_handle_connect_namespace(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer(client_manager=mgr) + s = asyncio_server.AsyncServer() handler = mock.MagicMock() s.on('connect', handler, namespace='/foo') _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0/foo')) - handler.assert_called_once_with('123', 'environ') - s.manager.connect.assert_any_call('123', '/') - s.manager.connect.assert_any_call('123', '/foo') - s.eio.send.mock.assert_any_call('123', '0/foo', binary=False) + assert s.manager.is_connected('1', '/foo') + handler.assert_called_once_with('1', 'environ') + s.eio.send.mock.assert_called_once_with('123', '0/foo,{"sid":"1"}') + + def test_handle_connect_always_connect(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer(always_connect=True) + s.manager.initialize = mock.MagicMock() + handler = mock.MagicMock() + s.on('connect', handler) + _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_message('123', '0')) + assert s.manager.is_connected('1', '/') + handler.assert_called_once_with('1', 'environ') + s.eio.send.mock.assert_called_once_with('123', '0{"sid":"1"}') + assert s.manager.initialize.call_count == 1 + _run(s._handle_eio_connect('456', 'environ')) + _run(s._handle_eio_message('456', '0')) + assert s.manager.initialize.call_count == 1 def test_handle_connect_rejected(self, eio): - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer(client_manager=mgr) + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() handler = mock.MagicMock(return_value=False) s.on('connect', handler) - ret = _run(s._handle_eio_connect('123', 'environ')) - assert not ret - handler.assert_called_once_with('123', 'environ') - assert s.manager.connect.call_count == 1 - assert s.manager.disconnect.call_count == 1 - assert s.environ == {} + _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_message('123', '0')) + assert not s.manager.is_connected('1', '/foo') + handler.assert_called_once_with('1', 'environ') + s.eio.send.mock.assert_called_once_with('123', '4') + assert s.environ == {'123': 'environ'} def test_handle_connect_namespace_rejected(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer(client_manager=mgr) + s = asyncio_server.AsyncServer() handler = mock.MagicMock(return_value=False) s.on('connect', handler, namespace='/foo') - ret = _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0/foo')) - assert ret is None - assert s.manager.connect.call_count == 2 - assert s.manager.disconnect.call_count == 1 + assert not s.manager.is_connected('1', '/foo') + handler.assert_called_once_with('1', 'environ') + s.eio.send.mock.assert_any_call('123', '4/foo') assert s.environ == {'123': 'environ'} - s.eio.send.mock.assert_any_call('123', '4/foo', binary=False) def test_handle_connect_rejected_always_connect(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer(client_manager=mgr, always_connect=True) + s = asyncio_server.AsyncServer(always_connect=True) handler = mock.MagicMock(return_value=False) s.on('connect', handler) - ret = _run(s._handle_eio_connect('123', 'environ')) - assert not ret - handler.assert_called_once_with('123', 'environ') - assert s.manager.connect.call_count == 1 - assert s.manager.disconnect.call_count == 1 - assert s.environ == {} - s.eio.send.mock.assert_any_call('123', '0', binary=False) - s.eio.send.mock.assert_any_call('123', '1', binary=False) + _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_message('123', '0')) + assert not s.manager.is_connected('1', '/') + handler.assert_called_once_with('1', 'environ') + s.eio.send.mock.assert_any_call('123', '0{"sid":"1"}') + s.eio.send.mock.assert_any_call('123', '1') + assert s.environ == {'123': 'environ'} def test_handle_connect_namespace_rejected_always_connect(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer(client_manager=mgr, always_connect=True) + s = asyncio_server.AsyncServer(always_connect=True) handler = mock.MagicMock(return_value=False) s.on('connect', handler, namespace='/foo') - ret = _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0/foo')) - assert not ret - assert s.manager.connect.call_count == 2 - assert s.manager.disconnect.call_count == 1 + assert not s.manager.is_connected('1', '/foo') + handler.assert_called_once_with('1', 'environ') + s.eio.send.mock.assert_any_call('123', '0/foo,{"sid":"1"}') + s.eio.send.mock.assert_any_call('123', '1/foo') assert s.environ == {'123': 'environ'} - s.eio.send.mock.assert_any_call('123', '0/foo', binary=False) - s.eio.send.mock.assert_any_call('123', '1/foo', binary=False) def test_handle_connect_rejected_with_exception(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer(client_manager=mgr) + s = asyncio_server.AsyncServer() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError('fail_reason') ) s.on('connect', handler) - ret = _run(s._handle_eio_connect('123', 'environ')) - assert ret == 'fail_reason' - assert s.manager.connect.call_count == 1 - assert s.manager.disconnect.call_count == 1 - assert s.environ == {} + _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_message('123', '0')) + assert not s.manager.is_connected('1', '/') + handler.assert_called_once_with('1', 'environ') + s.eio.send.mock.assert_called_once_with('123', '4"fail_reason"') + assert s.environ == {'123': 'environ'} def test_handle_connect_rejected_with_empty_exception(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer(client_manager=mgr) + s = asyncio_server.AsyncServer() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError() ) s.on('connect', handler) - ret = _run(s._handle_eio_connect('123', 'environ')) - assert not ret - assert s.manager.connect.call_count == 1 - assert s.manager.disconnect.call_count == 1 - assert s.environ == {} + _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_message('123', '0')) + assert not s.manager.is_connected('1', '/') + handler.assert_called_once_with('1', 'environ') + s.eio.send.mock.assert_called_once_with('123', '4') + assert s.environ == {'123': 'environ'} def test_handle_connect_namespace_rejected_with_exception(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer(client_manager=mgr) + s = asyncio_server.AsyncServer() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError('fail_reason', 1) ) s.on('connect', handler, namespace='/foo') - ret = _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0/foo')) - assert ret is None - assert s.manager.connect.call_count == 2 - assert s.manager.disconnect.call_count == 1 + assert not s.manager.is_connected('1', '/foo') + handler.assert_called_once_with('1', 'environ') + s.eio.send.mock.assert_called_once_with('123', + '4/foo,["fail_reason",1]') assert s.environ == {'123': 'environ'} - s.eio.send.mock.assert_any_call( - '123', '4/foo,["fail_reason",1]', binary=False - ) def test_handle_connect_namespace_rejected_with_empty_exception(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer(client_manager=mgr) + s = asyncio_server.AsyncServer() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError() ) s.on('connect', handler, namespace='/foo') - ret = _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0/foo')) - assert ret is None - assert s.manager.connect.call_count == 2 - assert s.manager.disconnect.call_count == 1 + assert not s.manager.is_connected('1', '/foo') + handler.assert_called_once_with('1', 'environ') + s.eio.send.mock.assert_called_once_with('123', '4/foo') assert s.environ == {'123': 'environ'} - s.eio.send.mock.assert_any_call('123', '4/foo', binary=False) def test_handle_disconnect(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer(client_manager=mgr) + s = asyncio_server.AsyncServer() + s.manager.disconnect = mock.MagicMock() handler = mock.MagicMock() s.on('disconnect', handler) _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_message('123', '0')) _run(s._handle_eio_disconnect('123')) - handler.assert_called_once_with('123') - s.manager.disconnect.assert_called_once_with('123', '/') + handler.assert_called_once_with('1') + s.manager.disconnect.assert_called_once_with('1', '/') assert s.environ == {} def test_handle_disconnect_namespace(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer(client_manager=mgr) - s.manager.get_namespaces = mock.MagicMock(return_value=['/', '/foo']) + s = asyncio_server.AsyncServer() handler = mock.MagicMock() s.on('disconnect', handler) handler_namespace = mock.MagicMock() @@ -549,15 +555,13 @@ def test_handle_disconnect_namespace(self, eio): _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0/foo')) _run(s._handle_eio_disconnect('123')) - handler.assert_called_once_with('123') - handler_namespace.assert_called_once_with('123') + handler.assert_not_called() + handler_namespace.assert_called_once_with('1') assert s.environ == {} def test_handle_disconnect_only_namespace(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer(client_manager=mgr) - s.manager.get_namespaces = mock.MagicMock(return_value=['/', '/foo']) + s = asyncio_server.AsyncServer() handler = mock.MagicMock() s.on('disconnect', handler) handler_namespace = mock.MagicMock() @@ -566,7 +570,7 @@ def test_handle_disconnect_only_namespace(self, eio): _run(s._handle_eio_message('123', '0/foo')) _run(s._handle_eio_message('123', '1/foo')) assert handler.call_count == 0 - handler_namespace.assert_called_once_with('123') + handler_namespace.assert_called_once_with('1') assert s.environ == {'123': 'environ'} def test_handle_disconnect_unknown_client(self, eio): @@ -577,20 +581,20 @@ def test_handle_disconnect_unknown_client(self, eio): def test_handle_event(self, eio): eio.return_value.send = AsyncMock() s = asyncio_server.AsyncServer(async_handlers=False) - s.manager.connect('123', '/') + sid = s.manager.connect('123', '/') handler = AsyncMock() s.on('my message', handler) _run(s._handle_eio_message('123', '2["my message","a","b","c"]')) - handler.mock.assert_called_once_with('123', 'a', 'b', 'c') + handler.mock.assert_called_once_with(sid, 'a', 'b', 'c') def test_handle_event_with_namespace(self, eio): eio.return_value.send = AsyncMock() s = asyncio_server.AsyncServer(async_handlers=False) - s.manager.connect('123', '/foo') + sid = s.manager.connect('123', '/foo') handler = mock.MagicMock() s.on('my message', handler, namespace='/foo') _run(s._handle_eio_message('123', '2/foo,["my message","a","b","c"]')) - handler.assert_called_once_with('123', 'a', 'b', 'c') + handler.assert_called_once_with(sid, 'a', 'b', 'c') def test_handle_event_with_disconnected_namespace(self, eio): eio.return_value.send = AsyncMock() @@ -604,7 +608,7 @@ def test_handle_event_with_disconnected_namespace(self, eio): def test_handle_event_binary(self, eio): eio.return_value.send = AsyncMock() s = asyncio_server.AsyncServer(async_handlers=False) - s.manager.connect('123', '/') + sid = s.manager.connect('123', '/') handler = mock.MagicMock() s.on('my message', handler) _run( @@ -617,15 +621,13 @@ def test_handle_event_binary(self, eio): ) _run(s._handle_eio_message('123', b'foo')) _run(s._handle_eio_message('123', b'bar')) - handler.assert_called_once_with('123', 'a', b'bar', b'foo') + handler.assert_called_once_with(sid, 'a', b'bar', b'foo') def test_handle_event_binary_ack(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer( - client_manager=mgr, async_handlers=False - ) - s.manager.initialize(s) + s = asyncio_server.AsyncServer(async_handlers=False) + s.manager.trigger_callback = AsyncMock() + sid = s.manager.connect('123', '/') _run( s._handle_eio_message( '123', @@ -633,70 +635,64 @@ def test_handle_event_binary_ack(self, eio): ) ) _run(s._handle_eio_message('123', b'foo')) - mgr.trigger_callback.mock.assert_called_once_with( - '123', '/', 321, ['my message', 'a', b'foo'] + s.manager.trigger_callback.mock.assert_called_once_with( + sid, 321, ['my message', 'a', b'foo'] ) def test_handle_event_with_ack(self, eio): eio.return_value.send = AsyncMock() s = asyncio_server.AsyncServer(async_handlers=False) - s.manager.connect('123', '/') + sid = s.manager.connect('123', '/') handler = mock.MagicMock(return_value='foo') s.on('my message', handler) _run(s._handle_eio_message('123', '21000["my message","foo"]')) - handler.assert_called_once_with('123', 'foo') + handler.assert_called_once_with(sid, 'foo') s.eio.send.mock.assert_called_once_with( - '123', '31000["foo"]', binary=False + '123', '31000["foo"]' ) def test_handle_event_with_ack_none(self, eio): eio.return_value.send = AsyncMock() s = asyncio_server.AsyncServer(async_handlers=False) - s.manager.connect('123', '/') + sid = s.manager.connect('123', '/') handler = mock.MagicMock(return_value=None) s.on('my message', handler) _run(s._handle_eio_message('123', '21000["my message","foo"]')) - handler.assert_called_once_with('123', 'foo') - s.eio.send.mock.assert_called_once_with('123', '31000[]', binary=False) + handler.assert_called_once_with(sid, 'foo') + s.eio.send.mock.assert_called_once_with('123', '31000[]') def test_handle_event_with_ack_tuple(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer( - client_manager=mgr, async_handlers=False - ) + s = asyncio_server.AsyncServer(async_handlers=False) + sid = s.manager.connect('123', '/') handler = mock.MagicMock(return_value=(1, '2', True)) s.on('my message', handler) _run(s._handle_eio_message('123', '21000["my message","a","b","c"]')) - handler.assert_called_once_with('123', 'a', 'b', 'c') + handler.assert_called_once_with(sid, 'a', 'b', 'c') s.eio.send.mock.assert_called_once_with( - '123', '31000[1,"2",true]', binary=False + '123', '31000[1,"2",true]' ) def test_handle_event_with_ack_list(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer( - client_manager=mgr, async_handlers=False - ) + s = asyncio_server.AsyncServer(async_handlers=False) + sid = s.manager.connect('123', '/') handler = mock.MagicMock(return_value=[1, '2', True]) s.on('my message', handler) _run(s._handle_eio_message('123', '21000["my message","a","b","c"]')) - handler.assert_called_once_with('123', 'a', 'b', 'c') + handler.assert_called_once_with(sid, 'a', 'b', 'c') s.eio.send.mock.assert_called_once_with( - '123', '31000[[1,"2",true]]', binary=False + '123', '31000[[1,"2",true]]' ) def test_handle_event_with_ack_binary(self, eio): eio.return_value.send = AsyncMock() - mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer( - client_manager=mgr, async_handlers=False - ) + s = asyncio_server.AsyncServer(async_handlers=False) + sid = s.manager.connect('123', '/') handler = mock.MagicMock(return_value=b'foo') s.on('my message', handler) _run(s._handle_eio_message('123', '21000["my message","foo"]')) - handler.assert_any_call('123', 'foo') + handler.assert_any_call(sid, 'foo') def test_handle_error_packet(self, eio): s = asyncio_server.AsyncServer() @@ -712,9 +708,10 @@ def test_send_with_ack(self, eio): eio.return_value.send = AsyncMock() s = asyncio_server.AsyncServer() _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_message('123', '0')) cb = mock.MagicMock() - id1 = s.manager._generate_ack_id('123', '/', cb) - id2 = s.manager._generate_ack_id('123', '/', cb) + id1 = s.manager._generate_ack_id('1', cb) + id2 = s.manager._generate_ack_id('1', cb) _run(s._emit_internal('123', 'my event', ['foo'], id=id1)) _run(s._emit_internal('123', 'my event', ['bar'], id=id2)) _run(s._handle_eio_message('123', '31["foo",2]')) @@ -726,7 +723,7 @@ def test_send_with_ack_namespace(self, eio): _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0/foo')) cb = mock.MagicMock() - id = s.manager._generate_ack_id('123', '/foo', cb) + id = s.manager._generate_ack_id('1', cb) _run( s._emit_internal( '123', 'my event', ['foo'], namespace='/foo', id=id @@ -775,18 +772,20 @@ def test_disconnect(self, eio): eio.return_value.disconnect = AsyncMock() s = asyncio_server.AsyncServer() _run(s._handle_eio_connect('123', 'environ')) - _run(s.disconnect('123')) - s.eio.send.mock.assert_any_call('123', '1', binary=False) - s.eio.disconnect.mock.assert_called_once_with('123') + _run(s._handle_eio_message('123', '0')) + _run(s.disconnect('1')) + s.eio.send.mock.assert_any_call('123', '1') + assert not s.manager.is_connected('1', '/') def test_disconnect_ignore_queue(self, eio): eio.return_value.send = AsyncMock() eio.return_value.disconnect = AsyncMock() s = asyncio_server.AsyncServer() _run(s._handle_eio_connect('123', 'environ')) - _run(s.disconnect('123', ignore_queue=True)) - s.eio.send.mock.assert_any_call('123', '1', binary=False) - s.eio.disconnect.mock.assert_called_once_with('123') + _run(s._handle_eio_message('123', '0')) + _run(s.disconnect('1', ignore_queue=True)) + s.eio.send.mock.assert_any_call('123', '1') + assert not s.manager.is_connected('1', '/') def test_disconnect_namespace(self, eio): eio.return_value.send = AsyncMock() @@ -794,29 +793,31 @@ def test_disconnect_namespace(self, eio): s = asyncio_server.AsyncServer() _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0/foo')) - _run(s.disconnect('123', namespace='/foo')) - s.eio.send.mock.assert_any_call('123', '1/foo', binary=False) - s.eio.disconnect.mock.assert_not_called() + _run(s.disconnect('1', namespace='/foo')) + s.eio.send.mock.assert_any_call('123', '1/foo') + assert not s.manager.is_connected('1', '/foo') def test_disconnect_twice(self, eio): eio.return_value.send = AsyncMock() eio.return_value.disconnect = AsyncMock() s = asyncio_server.AsyncServer() _run(s._handle_eio_connect('123', 'environ')) - _run(s.disconnect('123')) + _run(s._handle_eio_message('123', '0')) + _run(s.disconnect('1')) calls = s.eio.send.mock.call_count - _run(s.disconnect('123')) + assert not s.manager.is_connected('1', '/') + _run(s.disconnect('1')) assert calls == s.eio.send.mock.call_count - assert s.eio.disconnect.mock.call_count == 1 def test_disconnect_twice_namespace(self, eio): eio.return_value.send = AsyncMock() s = asyncio_server.AsyncServer() _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0/foo')) - _run(s.disconnect('123', namespace='/foo')) + _run(s.disconnect('1', namespace='/foo')) calls = s.eio.send.mock.call_count - _run(s.disconnect('123', namespace='/foo')) + assert not s.manager.is_connected('1', '/foo') + _run(s.disconnect('1', namespace='/foo')) assert calls == s.eio.send.mock.call_count def test_namespace_handler(self, eio): @@ -843,15 +844,15 @@ async def on_baz(self, sid, data1, data2): s.register_namespace(MyNamespace('/foo')) _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0/foo')) - assert result['result'] == ('123', 'environ') + assert result['result'] == ('1', 'environ') _run(s._handle_eio_message('123', '2/foo,["foo","a"]')) - assert result['result'] == ('123', 'a') + assert result['result'] == ('1', 'a') _run(s._handle_eio_message('123', '2/foo,["bar"]')) assert result['result'] == 'bar' _run(s._handle_eio_message('123', '2/foo,["baz","a","b"]')) assert result['result'] == ('a', 'b') - _run(s.disconnect('123', '/foo')) - assert result['result'] == ('disconnect', '123') + _run(s.disconnect('1', '/foo')) + assert result['result'] == ('disconnect', '1') def test_bad_namespace_handler(self, eio): class Dummy(object): @@ -927,6 +928,7 @@ def test_async_handlers(self, eio): s.eio.start_background_task.assert_called_once_with( s._handle_event_internal, s, + '1', '123', ['my message', 'a', 'b', 'c'], '/', diff --git a/tests/common/test_server.py b/tests/common/test_server.py index 201f974f..4332cba2 100644 --- a/tests/common/test_server.py +++ b/tests/common/test_server.py @@ -16,7 +16,8 @@ import pytest -@mock.patch('engineio.Server') +@mock.patch('socketio.server.engineio.Server', **{ + 'return_value.generate_id.side_effect': [str(i) for i in range(1, 10)]}) class TestServer(unittest.TestCase): def tearDown(self): # restore JSON encoder, in case a test changed it @@ -25,14 +26,13 @@ def tearDown(self): def test_create(self, eio): mgr = mock.MagicMock() s = server.Server( - client_manager=mgr, binary=True, async_handlers=True, foo='bar' + client_manager=mgr, async_handlers=True, foo='bar' ) s.handle_request({}, None) s.handle_request({}, None) eio.assert_called_once_with(**{'foo': 'bar', 'async_handlers': False}) assert s.manager == mgr assert s.eio.on.call_count == 3 - assert s.binary assert s.async_handlers def test_on_event(self, eio): @@ -266,47 +266,47 @@ def test_emit_internal(self, eio): s = server.Server() s._emit_internal('123', 'my event', 'my data', namespace='/foo') s.eio.send.assert_called_once_with( - '123', '2/foo,["my event","my data"]', binary=False + '123', '2/foo,["my event","my data"]' ) def test_emit_internal_with_tuple(self, eio): s = server.Server() s._emit_internal('123', 'my event', ('foo', 'bar'), namespace='/foo') s.eio.send.assert_called_once_with( - '123', '2/foo,["my event","foo","bar"]', binary=False + '123', '2/foo,["my event","foo","bar"]' ) def test_emit_internal_with_list(self, eio): s = server.Server() s._emit_internal('123', 'my event', ['foo', 'bar'], namespace='/foo') s.eio.send.assert_called_once_with( - '123', '2/foo,["my event",["foo","bar"]]', binary=False + '123', '2/foo,["my event",["foo","bar"]]' ) def test_emit_internal_with_none(self, eio): s = server.Server() s._emit_internal('123', 'my event', None, namespace='/foo') s.eio.send.assert_called_once_with( - '123', '2/foo,["my event"]', binary=False + '123', '2/foo,["my event"]' ) def test_emit_internal_with_callback(self, eio): s = server.Server() - id = s.manager._generate_ack_id('123', '/foo', 'cb') + id = s.manager._generate_ack_id('1', 'cb') s._emit_internal('123', 'my event', 'my data', namespace='/foo', id=id) s.eio.send.assert_called_once_with( - '123', '2/foo,1["my event","my data"]', binary=False + '123', '2/foo,1["my event","my data"]' ) def test_emit_internal_default_namespace(self, eio): s = server.Server() s._emit_internal('123', 'my event', 'my data') s.eio.send.assert_called_once_with( - '123', '2["my event","my data"]', binary=False + '123', '2["my event","my data"]' ) def test_emit_internal_binary(self, eio): - s = server.Server(binary=True) + s = server.Server() s._emit_internal('123', u'my event', b'my binary data') assert s.eio.send.call_count == 2 @@ -318,158 +318,157 @@ def test_transport(self, eio): s.eio.transport.assert_called_once_with('foo') def test_handle_connect(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr) + s = server.Server() + s.manager.initialize = mock.MagicMock() handler = mock.MagicMock() s.on('connect', handler) s._handle_eio_connect('123', 'environ') - handler.assert_called_once_with('123', 'environ') - s.manager.connect.assert_called_once_with('123', '/') - s.eio.send.assert_called_once_with('123', '0', binary=False) - assert mgr.initialize.call_count == 1 + s._handle_eio_message('123', '0') + assert s.manager.is_connected('1', '/') + handler.assert_called_once_with('1', 'environ') + s.eio.send.assert_called_once_with('123', '0{"sid":"1"}') + assert s.manager.initialize.call_count == 1 s._handle_eio_connect('456', 'environ') - assert mgr.initialize.call_count == 1 + assert s.manager.initialize.call_count == 1 def test_handle_connect_namespace(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr) + s = server.Server() handler = mock.MagicMock() s.on('connect', handler, namespace='/foo') s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo') - handler.assert_called_once_with('123', 'environ') - s.manager.connect.assert_any_call('123', '/') - s.manager.connect.assert_any_call('123', '/foo') - s.eio.send.assert_any_call('123', '0/foo', binary=False) + assert s.manager.is_connected('1', '/foo') + handler.assert_called_once_with('1', 'environ') + s.eio.send.assert_called_once_with('123', '0/foo,{"sid":"1"}') + + def test_handle_connect_always_connect(self, eio): + s = server.Server(always_connect=True) + s.manager.initialize = mock.MagicMock() + handler = mock.MagicMock() + s.on('connect', handler) + s._handle_eio_connect('123', 'environ') + s._handle_eio_message('123', '0') + assert s.manager.is_connected('1', '/') + handler.assert_called_once_with('1', 'environ') + s.eio.send.assert_called_once_with('123', '0{"sid":"1"}') + assert s.manager.initialize.call_count == 1 + s._handle_eio_connect('456', 'environ') + assert s.manager.initialize.call_count == 1 def test_handle_connect_rejected(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr) + s = server.Server() handler = mock.MagicMock(return_value=False) s.on('connect', handler) - ret = s._handle_eio_connect('123', 'environ') - assert not ret - handler.assert_called_once_with('123', 'environ') - assert s.manager.connect.call_count == 1 - assert s.manager.disconnect.call_count == 1 - assert s.environ == {} + s._handle_eio_connect('123', 'environ') + s._handle_eio_message('123', '0') + assert not s.manager.is_connected('1', '/') + handler.assert_called_once_with('1', 'environ') + assert not s.manager.is_connected('1', '/') + s.eio.send.assert_called_once_with('123', '4') + assert s.environ == {'123': 'environ'} def test_handle_connect_namespace_rejected(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr) + s = server.Server() handler = mock.MagicMock(return_value=False) s.on('connect', handler, namespace='/foo') - ret = s._handle_eio_connect('123', 'environ') + s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo') - assert ret is None - assert s.manager.connect.call_count == 2 - assert s.manager.disconnect.call_count == 1 + assert not s.manager.is_connected('1', '/foo') + handler.assert_called_once_with('1', 'environ') + assert not s.manager.is_connected('1', '/foo') + s.eio.send.assert_called_once_with('123', '4/foo') assert s.environ == {'123': 'environ'} - s.eio.send.assert_any_call('123', '4/foo', binary=False) def test_handle_connect_rejected_always_connect(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr, always_connect=True) + s = server.Server(always_connect=True) handler = mock.MagicMock(return_value=False) s.on('connect', handler) - ret = s._handle_eio_connect('123', 'environ') - assert not ret - handler.assert_called_once_with('123', 'environ') - assert s.manager.connect.call_count == 1 - assert s.manager.disconnect.call_count == 1 - assert s.environ == {} - s.eio.send.assert_any_call('123', '0', binary=False) - s.eio.send.assert_any_call('123', '1', binary=False) + s._handle_eio_connect('123', 'environ') + s._handle_eio_message('123', '0') + assert not s.manager.is_connected('1', '/') + handler.assert_called_once_with('1', 'environ') + s.eio.send.assert_any_call('123', '0{"sid":"1"}') + s.eio.send.assert_any_call('123', '1') + assert s.environ == {'123': 'environ'} def test_handle_connect_namespace_rejected_always_connect(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr, always_connect=True) + s = server.Server(always_connect=True) handler = mock.MagicMock(return_value=False) s.on('connect', handler, namespace='/foo') - ret = s._handle_eio_connect('123', 'environ') + s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo') - assert ret is None - assert s.manager.connect.call_count == 2 - assert s.manager.disconnect.call_count == 1 + assert not s.manager.is_connected('1', '/foo') + handler.assert_called_once_with('1', 'environ') + s.eio.send.assert_any_call('123', '0/foo,{"sid":"1"}') + s.eio.send.assert_any_call('123', '1/foo') assert s.environ == {'123': 'environ'} - s.eio.send.assert_any_call('123', '0/foo', binary=False) - s.eio.send.assert_any_call('123', '1/foo', binary=False) def test_handle_connect_rejected_with_exception(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr) + s = server.Server() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError('fail_reason') ) s.on('connect', handler) - ret = s._handle_eio_connect('123', 'environ') - assert ret == 'fail_reason' - handler.assert_called_once_with('123', 'environ') - assert s.manager.connect.call_count == 1 - assert s.manager.disconnect.call_count == 1 - assert s.environ == {} + s._handle_eio_connect('123', 'environ') + s._handle_eio_message('123', '0') + assert not s.manager.is_connected('1', '/') + handler.assert_called_once_with('1', 'environ') + s.eio.send.assert_called_once_with('123', '4"fail_reason"') + assert s.environ == {'123': 'environ'} def test_handle_connect_rejected_with_empty_exception(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr) + s = server.Server() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError() ) s.on('connect', handler) - ret = s._handle_eio_connect('123', 'environ') - assert not ret - handler.assert_called_once_with('123', 'environ') - assert s.manager.connect.call_count == 1 - assert s.manager.disconnect.call_count == 1 - assert s.environ == {} + s._handle_eio_connect('123', 'environ') + s._handle_eio_message('123', '0') + assert not s.manager.is_connected('1', '/') + handler.assert_called_once_with('1', 'environ') + s.eio.send.assert_called_once_with('123', '4') + assert s.environ == {'123': 'environ'} def test_handle_connect_namespace_rejected_with_exception(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr) + s = server.Server() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError(u'fail_reason', 1) ) s.on('connect', handler, namespace='/foo') - ret = s._handle_eio_connect('123', 'environ') + s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo') - assert ret is None - assert s.manager.connect.call_count == 2 - assert s.manager.disconnect.call_count == 1 - assert s.environ == {'123': 'environ'} - s.eio.send.assert_any_call( - '123', '4/foo,["fail_reason",1]', binary=False + assert not s.manager.is_connected('1', '/foo') + s.eio.send.assert_called_once_with( + '123', '4/foo,["fail_reason",1]' ) + assert s.environ == {'123': 'environ'} def test_handle_connect_namespace_rejected_with_empty_exception(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr) + s = server.Server() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError() ) s.on('connect', handler, namespace='/foo') - ret = s._handle_eio_connect('123', 'environ') + s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo') - assert ret is None - assert s.manager.connect.call_count == 2 - assert s.manager.disconnect.call_count == 1 + assert not s.manager.is_connected('1', '/foo') + s.eio.send.assert_called_once_with('123', '4/foo') assert s.environ == {'123': 'environ'} - s.eio.send.assert_any_call('123', '4/foo', binary=False) def test_handle_disconnect(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr) + s = server.Server() + s.manager.disconnect = mock.MagicMock() handler = mock.MagicMock() s.on('disconnect', handler) s._handle_eio_connect('123', 'environ') + s._handle_eio_message('123', '0') s._handle_eio_disconnect('123') - handler.assert_called_once_with('123') - s.manager.disconnect.assert_called_once_with('123', '/') + handler.assert_called_once_with('1') + s.manager.disconnect.assert_called_once_with('1', '/') assert s.environ == {} def test_handle_disconnect_namespace(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr) - s.manager.get_namespaces = mock.MagicMock(return_value=['/', '/foo']) + s = server.Server() handler = mock.MagicMock() s.on('disconnect', handler) handler_namespace = mock.MagicMock() @@ -477,14 +476,12 @@ def test_handle_disconnect_namespace(self, eio): s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo') s._handle_eio_disconnect('123') - handler.assert_called_once_with('123') - handler_namespace.assert_called_once_with('123') + handler.assert_not_called() + handler_namespace.assert_called_once_with('1') assert s.environ == {} def test_handle_disconnect_only_namespace(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr) - s.manager.get_namespaces = mock.MagicMock(return_value=['/', '/foo']) + s = server.Server() handler = mock.MagicMock() s.on('disconnect', handler) handler_namespace = mock.MagicMock() @@ -493,7 +490,7 @@ def test_handle_disconnect_only_namespace(self, eio): s._handle_eio_message('123', '0/foo') s._handle_eio_message('123', '1/foo') assert handler.call_count == 0 - handler_namespace.assert_called_once_with('123') + handler_namespace.assert_called_once_with('1') assert s.environ == {'123': 'environ'} def test_handle_disconnect_unknown_client(self, eio): @@ -507,7 +504,7 @@ def test_handle_event(self, eio): handler = mock.MagicMock() s.on('my message', handler) s._handle_eio_message('123', '2["my message","a","b","c"]') - handler.assert_called_once_with('123', 'a', 'b', 'c') + handler.assert_called_once_with('1', 'a', 'b', 'c') def test_handle_event_with_namespace(self, eio): s = server.Server(async_handlers=False) @@ -515,7 +512,7 @@ def test_handle_event_with_namespace(self, eio): handler = mock.MagicMock() s.on('my message', handler, namespace='/foo') s._handle_eio_message('123', '2/foo,["my message","a","b","c"]') - handler.assert_called_once_with('123', 'a', 'b', 'c') + handler.assert_called_once_with('1', 'a', 'b', 'c') def test_handle_event_with_disconnected_namespace(self, eio): s = server.Server(async_handlers=False) @@ -538,68 +535,67 @@ def test_handle_event_binary(self, eio): ) s._handle_eio_message('123', b'foo') s._handle_eio_message('123', b'bar') - handler.assert_called_once_with('123', 'a', b'bar', b'foo') + handler.assert_called_once_with('1', 'a', b'bar', b'foo') def test_handle_event_binary_ack(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr) + s = server.Server() + s.manager.trigger_callback = mock.MagicMock() + sid = s.manager.connect('123', '/') s._handle_eio_message( '123', '61-321["my message","a",' '{"_placeholder":true,"num":0}]' ) s._handle_eio_message('123', b'foo') - mgr.trigger_callback.assert_called_once_with( - '123', '/', 321, ['my message', 'a', b'foo'] + s.manager.trigger_callback.assert_called_once_with( + sid, 321, ['my message', 'a', b'foo'] ) def test_handle_event_with_ack(self, eio): s = server.Server(async_handlers=False) - s.manager.connect('123', '/') + sid = s.manager.connect('123', '/') handler = mock.MagicMock(return_value='foo') s.on('my message', handler) s._handle_eio_message('123', '21000["my message","foo"]') - handler.assert_called_once_with('123', 'foo') - s.eio.send.assert_called_once_with('123', '31000["foo"]', binary=False) + handler.assert_called_once_with(sid, 'foo') + s.eio.send.assert_called_once_with('123', '31000["foo"]') def test_handle_event_with_ack_none(self, eio): s = server.Server(async_handlers=False) - s.manager.connect('123', '/') + sid = s.manager.connect('123', '/') handler = mock.MagicMock(return_value=None) s.on('my message', handler) s._handle_eio_message('123', '21000["my message","foo"]') - handler.assert_called_once_with('123', 'foo') - s.eio.send.assert_called_once_with('123', '31000[]', binary=False) + handler.assert_called_once_with(sid, 'foo') + s.eio.send.assert_called_once_with('123', '31000[]') def test_handle_event_with_ack_tuple(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr, async_handlers=False) + s = server.Server(async_handlers=False) handler = mock.MagicMock(return_value=(1, '2', True)) s.on('my message', handler) + sid = s.manager.connect('123', '/') s._handle_eio_message('123', '21000["my message","a","b","c"]') - handler.assert_called_once_with('123', 'a', 'b', 'c') - s.eio.send.assert_called_once_with( - '123', '31000[1,"2",true]', binary=False + handler.assert_called_once_with(sid, 'a', 'b', 'c') + s.eio.send.assert_called_with( + '123', '31000[1,"2",true]' ) def test_handle_event_with_ack_list(self, eio): - mgr = mock.MagicMock() - s = server.Server(client_manager=mgr, async_handlers=False) + s = server.Server(async_handlers=False) handler = mock.MagicMock(return_value=[1, '2', True]) s.on('my message', handler) + sid = s.manager.connect('123', '/') s._handle_eio_message('123', '21000["my message","a","b","c"]') - handler.assert_called_once_with('123', 'a', 'b', 'c') - s.eio.send.assert_called_once_with( - '123', '31000[[1,"2",true]]', binary=False + handler.assert_called_once_with(sid, 'a', 'b', 'c') + s.eio.send.assert_called_with( + '123', '31000[[1,"2",true]]' ) def test_handle_event_with_ack_binary(self, eio): - mgr = mock.MagicMock() - s = server.Server( - client_manager=mgr, binary=True, async_handlers=False - ) + s = server.Server(async_handlers=False) handler = mock.MagicMock(return_value=b'foo') s.on('my message', handler) + sid = s.manager.connect('123', '/') s._handle_eio_message('123', '21000["my message","foo"]') - handler.assert_any_call('123', 'foo') + handler.assert_any_call(sid, 'foo') def test_handle_error_packet(self, eio): s = server.Server() @@ -614,9 +610,10 @@ def test_handle_invalid_packet(self, eio): def test_send_with_ack(self, eio): s = server.Server() s._handle_eio_connect('123', 'environ') + s._handle_eio_message('123', '0') cb = mock.MagicMock() - id1 = s.manager._generate_ack_id('123', '/', cb) - id2 = s.manager._generate_ack_id('123', '/', cb) + id1 = s.manager._generate_ack_id('1', cb) + id2 = s.manager._generate_ack_id('1', cb) s._emit_internal('123', 'my event', ['foo'], id=id1) s._emit_internal('123', 'my event', ['bar'], id=id2) s._handle_eio_message('123', '31["foo",2]') @@ -627,7 +624,7 @@ def test_send_with_ack_namespace(self, eio): s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo') cb = mock.MagicMock() - id = s.manager._generate_ack_id('123', '/foo', cb) + id = s.manager._generate_ack_id('1', cb) s._emit_internal('123', 'my event', ['foo'], namespace='/foo', id=id) s._handle_eio_message('123', '3/foo,1["foo",2]') cb.assert_called_once_with('foo', 2) @@ -665,28 +662,31 @@ def fake_save_session(sid, session): def test_disconnect(self, eio): s = server.Server() s._handle_eio_connect('123', 'environ') - s.disconnect('123') - s.eio.send.assert_any_call('123', '1', binary=False) + s._handle_eio_message('123', '0') + s.disconnect('1') + s.eio.send.assert_any_call('123', '1') def test_disconnect_ignore_queue(self, eio): s = server.Server() s._handle_eio_connect('123', 'environ') - s.disconnect('123', ignore_queue=True) - s.eio.send.assert_any_call('123', '1', binary=False) + s._handle_eio_message('123', '0') + s.disconnect('1', ignore_queue=True) + s.eio.send.assert_any_call('123', '1') def test_disconnect_namespace(self, eio): s = server.Server() s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo') - s.disconnect('123', namespace='/foo') - s.eio.send.assert_any_call('123', '1/foo', binary=False) + s.disconnect('1', namespace='/foo') + s.eio.send.assert_any_call('123', '1/foo') def test_disconnect_twice(self, eio): s = server.Server() s._handle_eio_connect('123', 'environ') - s.disconnect('123') + s._handle_eio_message('123', '0') + s.disconnect('1') calls = s.eio.send.call_count - s.disconnect('123') + s.disconnect('1') assert calls == s.eio.send.call_count def test_disconnect_twice_namespace(self, eio): @@ -721,15 +721,15 @@ def on_baz(self, sid, data1, data2): s.register_namespace(MyNamespace('/foo')) s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo') - assert result['result'] == ('123', 'environ') + assert result['result'] == ('1', 'environ') s._handle_eio_message('123', '2/foo,["foo","a"]') - assert result['result'] == ('123', 'a') + assert result['result'] == ('1', 'a') s._handle_eio_message('123', '2/foo,["bar"]') assert result['result'] == 'bar' s._handle_eio_message('123', '2/foo,["baz","a","b"]') assert result['result'] == ('a', 'b') - s.disconnect('123', '/foo') - assert result['result'] == ('disconnect', '123') + s.disconnect('1', '/foo') + assert result['result'] == ('disconnect', '1') def test_bad_namespace_handler(self, eio): class Dummy(object): @@ -801,11 +801,12 @@ def loads(*args, **kwargs): def test_async_handlers(self, eio): s = server.Server(async_handlers=True) - s.manager.connect('123', '/') + sid = s.manager.connect('123', '/') s._handle_eio_message('123', '2["my message","a","b","c"]') s.eio.start_background_task.assert_called_once_with( s._handle_event_internal, s, + sid, '123', ['my message', 'a', 'b', 'c'], '/', From cc9b3e97bb56489a041ef8503abaabee6ce790f5 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 6 Dec 2020 19:02:18 +0000 Subject: [PATCH 188/571] v5 protocol: updated all examples --- examples/client/asyncio/fiddle_client.py | 28 +++++++++ .../{fiddle-client.js => fiddle_client.js} | 0 .../{latency-client.js => latency_client.js} | 0 examples/client/threads/fiddle_client.py | 23 ++++++++ examples/server/aiohttp/README.rst | 14 ++++- examples/server/aiohttp/app.html | 2 +- examples/server/aiohttp/app.py | 0 examples/server/aiohttp/fiddle.html | 11 ++++ examples/server/aiohttp/fiddle.py | 31 ++++++++++ examples/server/aiohttp/latency.html | 2 +- examples/server/aiohttp/latency.py | 0 examples/server/aiohttp/static/fiddle.js | 19 +++++++ examples/server/asgi/README.rst | 6 ++ examples/server/asgi/app.html | 2 +- examples/server/asgi/app.py | 0 examples/server/asgi/fiddle.html | 11 ++++ examples/server/asgi/fiddle.py | 25 ++++++++ examples/server/asgi/latency.html | 2 +- examples/server/asgi/latency.py | 0 examples/server/asgi/static/fiddle.js | 19 +++++++ examples/server/sanic/README.rst | 14 ++++- examples/server/sanic/app.html | 2 +- examples/server/sanic/app.py | 0 examples/server/sanic/fiddle.html | 11 ++++ examples/server/sanic/fiddle.py | 32 +++++++++++ examples/server/sanic/latency.html | 2 +- examples/server/sanic/latency.py | 0 examples/server/sanic/static/fiddle.js | 19 +++++++ examples/server/tornado/README.rst | 16 +++++- examples/server/tornado/app.py | 2 +- examples/server/tornado/fiddle.py | 47 +++++++++++++++ examples/server/tornado/latency.py | 2 +- examples/server/tornado/static/fiddle.js | 19 +++++++ examples/server/tornado/templates/app.html | 2 +- examples/server/tornado/templates/fiddle.html | 11 ++++ .../server/tornado/templates/latency.html | 2 +- examples/server/wsgi/README.rst | 28 ++++++--- examples/server/wsgi/app.py | 0 .../socketio_app/static/index.html | 2 +- examples/server/wsgi/fiddle.py | 57 +++++++++++++++++++ examples/server/wsgi/latency.py | 0 examples/server/wsgi/static/fiddle.js | 19 +++++++ examples/server/wsgi/templates/fiddle.html | 11 ++++ examples/server/wsgi/templates/index.html | 2 +- examples/server/wsgi/templates/latency.html | 2 +- 45 files changed, 468 insertions(+), 29 deletions(-) create mode 100644 examples/client/asyncio/fiddle_client.py rename examples/client/javascript/{fiddle-client.js => fiddle_client.js} (100%) rename examples/client/javascript/{latency-client.js => latency_client.js} (100%) create mode 100644 examples/client/threads/fiddle_client.py mode change 100755 => 100644 examples/server/aiohttp/app.html mode change 100755 => 100644 examples/server/aiohttp/app.py create mode 100644 examples/server/aiohttp/fiddle.html create mode 100644 examples/server/aiohttp/fiddle.py mode change 100755 => 100644 examples/server/aiohttp/latency.html mode change 100755 => 100644 examples/server/aiohttp/latency.py create mode 100644 examples/server/aiohttp/static/fiddle.js mode change 100755 => 100644 examples/server/asgi/app.py create mode 100644 examples/server/asgi/fiddle.html create mode 100644 examples/server/asgi/fiddle.py mode change 100755 => 100644 examples/server/asgi/latency.py create mode 100644 examples/server/asgi/static/fiddle.js mode change 100755 => 100644 examples/server/sanic/app.html mode change 100755 => 100644 examples/server/sanic/app.py create mode 100644 examples/server/sanic/fiddle.html create mode 100644 examples/server/sanic/fiddle.py mode change 100755 => 100644 examples/server/sanic/latency.html mode change 100755 => 100644 examples/server/sanic/latency.py create mode 100644 examples/server/sanic/static/fiddle.js mode change 100755 => 100644 examples/server/tornado/app.py create mode 100644 examples/server/tornado/fiddle.py mode change 100755 => 100644 examples/server/tornado/latency.py create mode 100644 examples/server/tornado/static/fiddle.js mode change 100755 => 100644 examples/server/tornado/templates/app.html create mode 100644 examples/server/tornado/templates/fiddle.html mode change 100755 => 100644 examples/server/tornado/templates/latency.html mode change 100755 => 100644 examples/server/wsgi/app.py create mode 100644 examples/server/wsgi/fiddle.py mode change 100755 => 100644 examples/server/wsgi/latency.py create mode 100644 examples/server/wsgi/static/fiddle.js create mode 100644 examples/server/wsgi/templates/fiddle.html mode change 100755 => 100644 examples/server/wsgi/templates/index.html mode change 100755 => 100644 examples/server/wsgi/templates/latency.html diff --git a/examples/client/asyncio/fiddle_client.py b/examples/client/asyncio/fiddle_client.py new file mode 100644 index 00000000..b3791674 --- /dev/null +++ b/examples/client/asyncio/fiddle_client.py @@ -0,0 +1,28 @@ +import asyncio +import socketio + +sio = socketio.AsyncClient() + + +@sio.event +async def connect(): + print('connected to server') + + +@sio.event +async def disconnect(): + print('disconnected from server') + + +@sio.event +def hello(a, b, c): + print(a, b, c) + + +async def start_server(): + await sio.connect('http://localhost:5000') + await sio.wait() + + +if __name__ == '__main__': + asyncio.run(start_server()) diff --git a/examples/client/javascript/fiddle-client.js b/examples/client/javascript/fiddle_client.js similarity index 100% rename from examples/client/javascript/fiddle-client.js rename to examples/client/javascript/fiddle_client.js diff --git a/examples/client/javascript/latency-client.js b/examples/client/javascript/latency_client.js similarity index 100% rename from examples/client/javascript/latency-client.js rename to examples/client/javascript/latency_client.js diff --git a/examples/client/threads/fiddle_client.py b/examples/client/threads/fiddle_client.py new file mode 100644 index 00000000..176d083e --- /dev/null +++ b/examples/client/threads/fiddle_client.py @@ -0,0 +1,23 @@ +import socketio + +sio = socketio.Client() + + +@sio.event +def connect(): + print('connected to server') + + +@sio.event +def disconnect(): + print('disconnected from server') + + +@sio.event +def hello(a, b, c): + print(a, b, c) + + +if __name__ == '__main__': + sio.connect('http://localhost:5000') + sio.wait() diff --git a/examples/server/aiohttp/README.rst b/examples/server/aiohttp/README.rst index f1ce6aaf..1b0f4a42 100644 --- a/examples/server/aiohttp/README.rst +++ b/examples/server/aiohttp/README.rst @@ -23,17 +23,27 @@ time to the page. This is an ideal application to measure the performance of the different asynchronous modes supported by the Socket.IO server. +fiddle.py +--------- + +This is a very simple application based on a JavaScript example of the same +name. + Running the Examples -------------------- To run these examples, create a virtual environment, install the requirements -and then run:: +and then run one of the following:: $ python app.py -or:: +:: $ python latency.py +:: + + $ python fiddle.py + You can then access the application from your web browser at ``http://localhost:8080``. diff --git a/examples/server/aiohttp/app.html b/examples/server/aiohttp/app.html old mode 100755 new mode 100644 index ff896b03..5cb854df --- a/examples/server/aiohttp/app.html +++ b/examples/server/aiohttp/app.html @@ -3,7 +3,7 @@ python-socketio test - + + + + diff --git a/examples/server/aiohttp/fiddle.py b/examples/server/aiohttp/fiddle.py new file mode 100644 index 00000000..95b75c20 --- /dev/null +++ b/examples/server/aiohttp/fiddle.py @@ -0,0 +1,31 @@ +from aiohttp import web + +import socketio + +sio = socketio.AsyncServer(async_mode='aiohttp') +app = web.Application() +sio.attach(app) + + +async def index(request): + with open('fiddle.html') as f: + return web.Response(text=f.read(), content_type='text/html') + + +@sio.event +async def connect(sid, environ): + print('connected', sid) + await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) + + +@sio.event +def disconnect(sid): + print('disconnected', sid) + + +app.router.add_static('/static', 'static') +app.router.add_get('/', index) + + +if __name__ == '__main__': + web.run_app(app) diff --git a/examples/server/aiohttp/latency.html b/examples/server/aiohttp/latency.html old mode 100755 new mode 100644 index 85338cc8..9c885eb9 --- a/examples/server/aiohttp/latency.html +++ b/examples/server/aiohttp/latency.html @@ -11,7 +11,7 @@

(connecting)

- + - + + + + diff --git a/examples/server/asgi/fiddle.py b/examples/server/asgi/fiddle.py new file mode 100644 index 00000000..bd9a4c1f --- /dev/null +++ b/examples/server/asgi/fiddle.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +import uvicorn + +import socketio + +sio = socketio.AsyncServer(async_mode='asgi') +app = socketio.ASGIApp(sio, static_files={ + '/': 'fiddle.html', + '/static': 'static', +}) + + +@sio.event +async def connect(sid, environ): + print('connected', sid) + await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) + + +@sio.event +def disconnect(sid): + print('disconnected', sid) + + +if __name__ == '__main__': + uvicorn.run(app, host='127.0.0.1', port=5000) diff --git a/examples/server/asgi/latency.html b/examples/server/asgi/latency.html index 85338cc8..9c885eb9 100644 --- a/examples/server/asgi/latency.html +++ b/examples/server/asgi/latency.html @@ -11,7 +11,7 @@

(connecting)

- + - + + + + diff --git a/examples/server/sanic/fiddle.py b/examples/server/sanic/fiddle.py new file mode 100644 index 00000000..a0f33cbf --- /dev/null +++ b/examples/server/sanic/fiddle.py @@ -0,0 +1,32 @@ +from sanic import Sanic +from sanic.response import html + +import socketio + +sio = socketio.AsyncServer(async_mode='sanic') +app = Sanic() +sio.attach(app) + + +@app.route('/') +def index(request): + with open('fiddle.html') as f: + return html(f.read()) + + +@sio.event +async def connect(sid, environ): + print('connected', sid) + await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) + + +@sio.event +def disconnect(sid): + print('disconnected', sid) + + +app.static('/static', './static') + + +if __name__ == '__main__': + app.run() diff --git a/examples/server/sanic/latency.html b/examples/server/sanic/latency.html old mode 100755 new mode 100644 index 85338cc8..9c885eb9 --- a/examples/server/sanic/latency.html +++ b/examples/server/sanic/latency.html @@ -11,7 +11,7 @@

(connecting)

- + - + + + + diff --git a/examples/server/tornado/templates/latency.html b/examples/server/tornado/templates/latency.html old mode 100755 new mode 100644 index 85338cc8..9c885eb9 --- a/examples/server/tornado/templates/latency.html +++ b/examples/server/tornado/templates/latency.html @@ -11,7 +11,7 @@

(connecting)

- + - + + + + diff --git a/examples/server/wsgi/templates/index.html b/examples/server/wsgi/templates/index.html old mode 100755 new mode 100644 index db8dc2cc..5d18f6bb --- a/examples/server/wsgi/templates/index.html +++ b/examples/server/wsgi/templates/index.html @@ -3,7 +3,7 @@ Flask-SocketIO Test - + - + + diff --git a/examples/server/javascript/latency.js b/examples/server/javascript/latency.js index b374ee6c..a2be9946 100644 --- a/examples/server/javascript/latency.js +++ b/examples/server/javascript/latency.js @@ -1,7 +1,11 @@ const express = require('express'); +const { createServer } = require("http"); +const { Server } = require("socket.io"); + const app = express(); -const server = require('http').createServer(app); -const io = require('socket.io')(server); +const httpServer = createServer(app); +const io = new Server(httpServer); + const port = process.env.PORT || 5000; app.use(express.static(__dirname + '/latency_public')); @@ -18,4 +22,4 @@ io.on('connection', socket => { }); }); -server.listen(port, () => console.log(`server listening on port ${port}`)); +httpServer.listen(port, () => console.log(`server listening on port ${port}`)); diff --git a/examples/server/javascript/latency_public/index.html b/examples/server/javascript/latency_public/index.html index c862c898..857ed6a1 100644 --- a/examples/server/javascript/latency_public/index.html +++ b/examples/server/javascript/latency_public/index.html @@ -9,8 +9,8 @@

Socket.IO Latency

(connecting)

- - + + diff --git a/examples/server/javascript/package-lock.json b/examples/server/javascript/package-lock.json index d167f317..ab3879bd 100644 --- a/examples/server/javascript/package-lock.json +++ b/examples/server/javascript/package-lock.json @@ -8,10 +8,24 @@ "name": "socketio-examples", "version": "0.1.0", "dependencies": { - "express": "^4.17.3", + "@socket.io/admin-ui": "^0.5.1", + "express": "^4.18.2", "smoothie": "1.19.0", - "socket.io": "^4.5.3", - "socket.io-client": "^3.0.3" + "socket.io": "^4.6.1", + "socket.io-client": "^4.6.1" + } + }, + "node_modules/@socket.io/admin-ui": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@socket.io/admin-ui/-/admin-ui-0.5.1.tgz", + "integrity": "sha512-1dlGL2FGm6T+uL1e6iDvbo2eCINwvW7iVbjIblwh5kPPRM1SP8lmZrbFZf4QNJ/cqQ+JLcx49eXGM9WAB4TK7w==", + "dependencies": { + "@types/bcryptjs": "^2.4.2", + "bcryptjs": "^2.4.3", + "debug": "~4.3.1" + }, + "peerDependencies": { + "socket.io": ">=3.1.0" } }, "node_modules/@socket.io/component-emitter": { @@ -19,10 +33,10 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, - "node_modules/@types/component-emitter": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", - "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" + "node_modules/@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==" }, "node_modules/@types/cookie": { "version": "0.4.1", @@ -30,14 +44,17 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "node_modules/@types/cors": { - "version": "2.8.12", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", - "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + "version": "18.15.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz", + "integrity": "sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ==" }, "node_modules/accepts": { "version": "1.3.8", @@ -54,20 +71,7 @@ "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "node_modules/backo2": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" - }, - "node_modules/base64-arraybuffer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", - "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=", - "engines": { - "node": ">= 0.6.0" - } + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, "node_modules/base64id": { "version": "2.0.0", @@ -77,26 +81,47 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "node_modules/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.8.1", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.9.7", - "raw-body": "2.4.3", - "type-is": "~1.6.18" + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -105,10 +130,17 @@ "node": ">= 0.8" } }, - "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/content-disposition": { "version": "0.5.4", @@ -122,17 +154,17 @@ } }, "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "engines": { "node": ">= 0.6" } }, "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", "engines": { "node": ">= 0.6" } @@ -140,7 +172,7 @@ "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "node_modules/cors": { "version": "2.8.5", @@ -155,43 +187,55 @@ } }, "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { - "ms": "2.0.0" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "engines": { "node": ">= 0.8" } }, "node_modules/engine.io": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", - "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz", + "integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -202,114 +246,44 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.0.3", - "ws": "~8.2.3" + "ws": "~8.11.0" }, "engines": { "node": ">=10.0.0" } }, "node_modules/engine.io-client": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-4.1.4.tgz", - "integrity": "sha512-843fqAdKeUMFqKi1sSjnR11tJ4wi8sIefu6+JC1OzkkJBmjtc/gM/rZ53tJfu5Iae/3gApm5veoS+v+gtT0+Fg==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz", + "integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==", "dependencies": { - "base64-arraybuffer": "0.1.4", - "component-emitter": "~1.3.0", + "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", - "engine.io-parser": "~4.0.1", - "has-cors": "1.1.0", - "parseqs": "0.0.6", - "parseuri": "0.0.6", - "ws": "~7.4.2", - "xmlhttprequest-ssl": "~1.6.2", - "yeast": "0.1.2" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "engine.io-parser": "~5.0.3", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" } }, - "node_modules/engine.io-client/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "node_modules/engine.io-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", - "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", - "dependencies": { - "base64-arraybuffer": "0.1.4" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io/node_modules/engine.io-parser": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz", - "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", + "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==", "engines": { "node": ">=10.0.0" } }, - "node_modules/engine.io/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/engine.io/node_modules/ws": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", - "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "node": ">= 0.6" } }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "node_modules/etag": { "version": "1.8.1", @@ -320,37 +294,38 @@ } }, "node_modules/express": { - "version": "4.17.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", - "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.19.2", + "body-parser": "1.20.1", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.4.2", + "cookie": "0.5.0", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "~1.1.2", + "depd": "2.0.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "~1.1.2", + "finalhandler": "1.2.0", "fresh": "0.5.2", + "http-errors": "2.0.0", "merge-descriptors": "1.0.1", "methods": "~1.1.2", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", - "qs": "6.9.7", + "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.17.2", - "serve-static": "1.14.2", + "send": "0.18.0", + "serve-static": "1.15.0", "setprototypeof": "1.2.0", - "statuses": "~1.5.0", + "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -359,23 +334,49 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", - "statuses": "~1.5.0", + "statuses": "2.0.1", "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -392,24 +393,59 @@ "node": ">= 0.6" } }, - "node_modules/has-cors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dependencies": { - "depd": "~1.1.2", + "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", + "statuses": "2.0.1", "toidentifier": "1.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/iconv-lite": { @@ -447,12 +483,12 @@ "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "engines": { "node": ">= 0.6" } @@ -488,9 +524,9 @@ } }, "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/negotiator": { "version": "0.6.3", @@ -508,10 +544,18 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dependencies": { "ee-first": "1.1.1" }, @@ -519,16 +563,6 @@ "node": ">= 0.8" } }, - "node_modules/parseqs": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", - "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" - }, - "node_modules/parseuri": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", - "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -540,7 +574,7 @@ "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -555,9 +589,12 @@ } }, "node_modules/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, "engines": { "node": ">=0.6" }, @@ -574,12 +611,12 @@ } }, "node_modules/raw-body": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", - "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", "dependencies": { "bytes": "3.1.2", - "http-errors": "1.8.1", + "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" }, @@ -612,42 +649,55 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/send": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", - "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "dependencies": { "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", + "depd": "2.0.0", + "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "1.8.1", + "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "range-parser": "~1.2.1", - "statuses": "~1.5.0" + "statuses": "2.0.1" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serve-static": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", - "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.17.2" + "send": "0.18.0" }, "engines": { "node": ">= 0.8.0" @@ -658,129 +708,66 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/smoothie": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/smoothie/-/smoothie-1.19.0.tgz", - "integrity": "sha1-kVZqT9PFES3CrJWAAQT6Ruz7XBA=" + "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" }, "node_modules/socket.io": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz", - "integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", + "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "debug": "~4.3.2", - "engine.io": "~6.2.0", - "socket.io-adapter": "~2.4.0", - "socket.io-parser": "~4.2.0" + "engine.io": "~6.4.1", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.1" }, "engines": { "node": ">=10.0.0" } }, "node_modules/socket.io-adapter": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz", - "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==" - }, - "node_modules/socket.io-client": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-3.1.3.tgz", - "integrity": "sha512-4sIGOGOmCg3AOgGi7EEr6ZkTZRkrXwub70bBB/F0JSkMOUFpA77WsL87o34DffQQ31PkbMUIadGOk+3tx1KGbw==", - "dependencies": { - "@types/component-emitter": "^1.2.10", - "backo2": "~1.0.2", - "component-emitter": "~1.3.0", - "debug": "~4.3.1", - "engine.io-client": "~4.1.0", - "parseuri": "0.0.6", - "socket.io-parser": "~4.0.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "ws": "~8.11.0" } }, - "node_modules/socket.io-client/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/socket.io-parser": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.5.tgz", - "integrity": "sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig==", + "node_modules/socket.io-client": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz", + "integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==", "dependencies": { - "@types/component-emitter": "^1.2.10", - "component-emitter": "~1.3.0", - "debug": "~4.3.1" + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.4.0", + "socket.io-parser": "~4.2.1" }, "engines": { "node": ">=10.0.0" } }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/socket.io/node_modules/socket.io-parser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz", - "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==", + "node_modules/socket.io-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", + "integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -790,11 +777,11 @@ } }, "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/toidentifier": { @@ -820,7 +807,7 @@ "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "engines": { "node": ">= 0.8" } @@ -828,7 +815,7 @@ "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "engines": { "node": ">= 0.4.0" } @@ -836,17 +823,17 @@ "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "engines": { "node": ">= 0.8" } }, "node_modules/ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", @@ -862,29 +849,34 @@ } }, "node_modules/xmlhttprequest-ssl": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", - "integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", "engines": { "node": ">=0.4.0" } - }, - "node_modules/yeast": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", - "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" } }, "dependencies": { + "@socket.io/admin-ui": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@socket.io/admin-ui/-/admin-ui-0.5.1.tgz", + "integrity": "sha512-1dlGL2FGm6T+uL1e6iDvbo2eCINwvW7iVbjIblwh5kPPRM1SP8lmZrbFZf4QNJ/cqQ+JLcx49eXGM9WAB4TK7w==", + "requires": { + "@types/bcryptjs": "^2.4.2", + "bcryptjs": "^2.4.3", + "debug": "~4.3.1" + } + }, "@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, - "@types/component-emitter": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", - "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" + "@types/bcryptjs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==" }, "@types/cookie": { "version": "0.4.1", @@ -892,14 +884,17 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "@types/cors": { - "version": "2.8.12", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", - "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "requires": { + "@types/node": "*" + } }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==" + "version": "18.15.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz", + "integrity": "sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ==" }, "accepts": { "version": "1.3.8", @@ -913,38 +908,50 @@ "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "backo2": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" - }, - "base64-arraybuffer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", - "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, "base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.8.1", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.9.7", - "raw-body": "2.4.3", - "type-is": "~1.6.18" + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } } }, "bytes": { @@ -952,10 +959,14 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } }, "content-disposition": { "version": "0.5.4", @@ -966,19 +977,19 @@ } }, "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" }, "cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "cors": { "version": "2.8.5", @@ -990,37 +1001,37 @@ } }, "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { - "ms": "2.0.0" + "ms": "2.1.2" } }, "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "engine.io": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", - "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz", + "integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==", "requires": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -1031,79 +1042,37 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.0.3", - "ws": "~8.2.3" + "ws": "~8.11.0" }, "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "engine.io-parser": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz", - "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "ws": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", - "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", - "requires": {} + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" } } }, "engine.io-client": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-4.1.4.tgz", - "integrity": "sha512-843fqAdKeUMFqKi1sSjnR11tJ4wi8sIefu6+JC1OzkkJBmjtc/gM/rZ53tJfu5Iae/3gApm5veoS+v+gtT0+Fg==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz", + "integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==", "requires": { - "base64-arraybuffer": "0.1.4", - "component-emitter": "~1.3.0", + "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", - "engine.io-parser": "~4.0.1", - "has-cors": "1.1.0", - "parseqs": "0.0.6", - "parseuri": "0.0.6", - "ws": "~7.4.2", - "xmlhttprequest-ssl": "~1.6.2", - "yeast": "0.1.2" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } + "engine.io-parser": "~5.0.3", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" } }, "engine.io-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", - "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", - "requires": { - "base64-arraybuffer": "0.1.4" - } + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", + "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==" }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "etag": { "version": "1.8.1", @@ -1111,54 +1080,85 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.17.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", - "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.19.2", + "body-parser": "1.20.1", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.4.2", + "cookie": "0.5.0", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "~1.1.2", + "depd": "2.0.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "~1.1.2", + "finalhandler": "1.2.0", "fresh": "0.5.2", + "http-errors": "2.0.0", "merge-descriptors": "1.0.1", "methods": "~1.1.2", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", - "qs": "6.9.7", + "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.17.2", - "serve-static": "1.14.2", + "send": "0.18.0", + "serve-static": "1.15.0", "setprototypeof": "1.2.0", - "statuses": "~1.5.0", + "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } } }, "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", - "statuses": "~1.5.0", + "statuses": "2.0.1", "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } } }, "forwarded": { @@ -1171,20 +1171,43 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, - "has-cors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, "http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "requires": { - "depd": "~1.1.2", + "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", + "statuses": "2.0.1", "toidentifier": "1.0.1" } }, @@ -1214,12 +1237,12 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "mime": { "version": "1.6.0", @@ -1240,9 +1263,9 @@ } }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "negotiator": { "version": "0.6.3", @@ -1254,24 +1277,19 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + }, "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "requires": { "ee-first": "1.1.1" } }, - "parseqs": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", - "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" - }, - "parseuri": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", - "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" - }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1280,7 +1298,7 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, "proxy-addr": { "version": "2.0.7", @@ -1292,9 +1310,12 @@ } }, "qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==" + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } }, "range-parser": { "version": "1.2.1", @@ -1302,12 +1323,12 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", - "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", "requires": { "bytes": "3.1.2", - "http-errors": "1.8.1", + "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } @@ -1323,25 +1344,40 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "send": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", - "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "requires": { "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", + "depd": "2.0.0", + "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "1.8.1", + "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "range-parser": "~1.2.1", - "statuses": "~1.5.0" + "statuses": "2.0.1" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1350,14 +1386,14 @@ } }, "serve-static": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", - "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.17.2" + "send": "0.18.0" } }, "setprototypeof": { @@ -1365,111 +1401,66 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "smoothie": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/smoothie/-/smoothie-1.19.0.tgz", - "integrity": "sha1-kVZqT9PFES3CrJWAAQT6Ruz7XBA=" + "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" }, "socket.io": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz", - "integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", + "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", "requires": { "accepts": "~1.3.4", "base64id": "~2.0.0", "debug": "~4.3.2", - "engine.io": "~6.2.0", - "socket.io-adapter": "~2.4.0", - "socket.io-parser": "~4.2.0" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "socket.io-parser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz", - "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==", - "requires": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - } - } + "engine.io": "~6.4.1", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.1" } }, "socket.io-adapter": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz", - "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==" + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "requires": { + "ws": "~8.11.0" + } }, "socket.io-client": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-3.1.3.tgz", - "integrity": "sha512-4sIGOGOmCg3AOgGi7EEr6ZkTZRkrXwub70bBB/F0JSkMOUFpA77WsL87o34DffQQ31PkbMUIadGOk+3tx1KGbw==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz", + "integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==", "requires": { - "@types/component-emitter": "^1.2.10", - "backo2": "~1.0.2", - "component-emitter": "~1.3.0", - "debug": "~4.3.1", - "engine.io-client": "~4.1.0", - "parseuri": "0.0.6", - "socket.io-parser": "~4.0.4" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.4.0", + "socket.io-parser": "~4.2.1" } }, "socket.io-parser": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.5.tgz", - "integrity": "sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", + "integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", "requires": { - "@types/component-emitter": "^1.2.10", - "component-emitter": "~1.3.0", + "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } } }, "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, "toidentifier": { "version": "1.0.1", @@ -1488,33 +1479,28 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, "ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", "requires": {} }, "xmlhttprequest-ssl": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", - "integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==" - }, - "yeast": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", - "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" } } } diff --git a/examples/server/javascript/package.json b/examples/server/javascript/package.json index 20be6dca..5501986a 100644 --- a/examples/server/javascript/package.json +++ b/examples/server/javascript/package.json @@ -2,9 +2,10 @@ "name": "socketio-examples", "version": "0.1.0", "dependencies": { - "express": "^4.17.3", + "@socket.io/admin-ui": "^0.5.1", + "express": "^4.18.2", "smoothie": "1.19.0", - "socket.io": "^4.5.3", - "socket.io-client": "^3.0.3" + "socket.io": "^4.6.1", + "socket.io-client": "^4.6.1" } } From 72dfdc6c8aff7f4f61c409e771df6a19299ad36f Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 4 Apr 2023 18:29:51 +0100 Subject: [PATCH 367/571] Corrected user session documentation example (Fixes #1170) --- docs/server.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/server.rst b/docs/server.rst index d0dfd3c0..5a6798c2 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -448,13 +448,13 @@ The session can also be manipulated with the `session()` context manager:: For the ``asyncio`` server, an asynchronous context manager is used:: @sio.event - def connect(sid, environ): + async def connect(sid, environ): username = authenticate_user(environ) async with sio.session(sid) as session: session['username'] = username @sio.event - def message(sid, data): + async def message(sid, data): async with sio.session(sid) as session: print('message from ', session['username']) From c66d7a9f1ade0f728840b8ee9079599dd6a3de3f Mon Sep 17 00:00:00 2001 From: zykron1 <85203848+zykron1@users.noreply.github.com> Date: Fri, 14 Apr 2023 02:06:34 -0700 Subject: [PATCH 368/571] Improved grammar in documentation (#1175) --- docs/intro.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/intro.rst b/docs/intro.rst index afd0d937..9cadc6b0 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -188,8 +188,8 @@ Server Features asyncio. - Supports large number of clients even on modest hardware due to being asynchronous. -- Can be hosted on any `WSGI `_ and - `ASGI `_ web servers including +- Can be hosted on any `WSGI `_ or + `ASGI `_ web server including `Gunicorn `_, `Uvicorn `_, `eventlet `_ and `gevent `_. - Can be integrated with WSGI applications written in frameworks such as Flask, Django, From 14b9f4f3600d2406e411d9afce07c7240a7a0545 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Fri, 14 Apr 2023 12:50:44 +0100 Subject: [PATCH 369/571] Upgrade GitHub actions #nolog --- .github/workflows/tests.yml | 19 +++++++++++-------- tox.ini | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9be1c3d3..adb0825a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,8 +11,8 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 - run: python -m pip install --upgrade pip wheel - run: pip install tox tox-gh-actions - run: tox -eflake8 @@ -30,8 +30,8 @@ jobs: fail-fast: false runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 with: python-version: ${{ matrix.python }} - run: python -m pip install --upgrade pip wheel @@ -41,9 +41,12 @@ jobs: name: coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 - run: python -m pip install --upgrade pip wheel - - run: pip install tox tox-gh-actions codecov + - run: pip install tox tox-gh-actions - run: tox - - run: codecov + - uses: codecov/codecov-action@v3 + with: + files: ./coverage.xml + fail_ci_if_error: true diff --git a/tox.ini b/tox.ini index e8f671b7..12d28915 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ python = [testenv] commands= pip install -e . - pytest -p no:logging --cov=socketio --cov-branch --cov-report=term-missing + pytest -p no:logging --cov=socketio --cov-branch --cov-report=term-missing --cov-report=xml deps= msgpack pytest From e9599795bd30ba647f579eb3471a3b6eaf6ec0b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 23:26:25 +0100 Subject: [PATCH 370/571] Bump sqlparse in /examples/server/wsgi/django_socketio (#1176) #nolog Bumps [sqlparse](https://github.com/andialbrecht/sqlparse) from 0.4.2 to 0.4.4. - [Release notes](https://github.com/andialbrecht/sqlparse/releases) - [Changelog](https://github.com/andialbrecht/sqlparse/blob/master/CHANGELOG) - [Commits](https://github.com/andialbrecht/sqlparse/compare/0.4.2...0.4.4) --- updated-dependencies: - dependency-name: sqlparse dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/django_socketio/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/django_socketio/requirements.txt b/examples/server/wsgi/django_socketio/requirements.txt index 3252f005..9fb8e455 100644 --- a/examples/server/wsgi/django_socketio/requirements.txt +++ b/examples/server/wsgi/django_socketio/requirements.txt @@ -4,4 +4,4 @@ bidict==0.22.0 Django==4.1.7 python-engineio==4.3.2 python-socketio==5.6.0 -sqlparse==0.4.2 +sqlparse==0.4.4 From cab8c578a9a0df3acb6c1788c89fcd30b1b5a3bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 May 2023 10:26:07 +0100 Subject: [PATCH 371/571] Bump engine.io from 6.4.1 to 6.4.2 in /examples/server/javascript (#1183) #nolog Bumps [engine.io](https://github.com/socketio/engine.io) from 6.4.1 to 6.4.2. - [Release notes](https://github.com/socketio/engine.io/releases) - [Changelog](https://github.com/socketio/engine.io/blob/main/CHANGELOG.md) - [Commits](https://github.com/socketio/engine.io/compare/6.4.1...6.4.2) --- updated-dependencies: - dependency-name: engine.io dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/javascript/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/server/javascript/package-lock.json b/examples/server/javascript/package-lock.json index ab3879bd..a5a38333 100644 --- a/examples/server/javascript/package-lock.json +++ b/examples/server/javascript/package-lock.json @@ -233,9 +233,9 @@ } }, "node_modules/engine.io": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz", - "integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz", + "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -1029,9 +1029,9 @@ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "engine.io": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz", - "integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz", + "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==", "requires": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", From 2851256c5484e520bf2dab40d4a83caa8cb4198e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 May 2023 10:27:07 +0100 Subject: [PATCH 372/571] Bump engine.io from 6.4.1 to 6.4.2 in /examples/client/javascript (#1184) #nolog Bumps [engine.io](https://github.com/socketio/engine.io) from 6.4.1 to 6.4.2. - [Release notes](https://github.com/socketio/engine.io/releases) - [Changelog](https://github.com/socketio/engine.io/blob/main/CHANGELOG.md) - [Commits](https://github.com/socketio/engine.io/compare/6.4.1...6.4.2) --- updated-dependencies: - dependency-name: engine.io dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/client/javascript/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/client/javascript/package-lock.json b/examples/client/javascript/package-lock.json index b3979cf6..f700e357 100644 --- a/examples/client/javascript/package-lock.json +++ b/examples/client/javascript/package-lock.json @@ -188,9 +188,9 @@ } }, "node_modules/engine.io": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz", - "integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz", + "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -1015,9 +1015,9 @@ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "engine.io": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz", - "integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz", + "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==", "requires": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", From b068fd6e8cc121c3ec7f417996cbeb5b393373fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 May 2023 23:48:57 +0100 Subject: [PATCH 373/571] Bump django from 4.1.7 to 4.1.9 in /examples/server/wsgi/django_socketio (#1185) #nolog Bumps [django](https://github.com/django/django) from 4.1.7 to 4.1.9. - [Commits](https://github.com/django/django/compare/4.1.7...4.1.9) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/django_socketio/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/django_socketio/requirements.txt b/examples/server/wsgi/django_socketio/requirements.txt index 9fb8e455..0ed6894f 100644 --- a/examples/server/wsgi/django_socketio/requirements.txt +++ b/examples/server/wsgi/django_socketio/requirements.txt @@ -1,7 +1,7 @@ asgiref==3.5.2 backports.zoneinfo==0.2.1 bidict==0.22.0 -Django==4.1.7 +Django==4.1.9 python-engineio==4.3.2 python-socketio==5.6.0 sqlparse==0.4.4 From 00b31663c29968bd50282d22fca9b95c09e4becf Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 14 May 2023 11:00:44 +0100 Subject: [PATCH 374/571] Upgrade dependencies in Django server example --- examples/server/wsgi/django_socketio/README.md | 18 ++++++++++++++---- .../wsgi/django_socketio/requirements.txt | 14 +++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/examples/server/wsgi/django_socketio/README.md b/examples/server/wsgi/django_socketio/README.md index 9fcbabeb..b4659f47 100644 --- a/examples/server/wsgi/django_socketio/README.md +++ b/examples/server/wsgi/django_socketio/README.md @@ -9,8 +9,18 @@ You can run it with the Django development web server: python manage.py runserver ``` -When running in this mode, you will see a warning indicating that the WebSocket -transport is not available, which is not supported by this web server. +When running in this mode, you will get an error message: -See the documentation for information on supported deployment methods that you -can use to add support for WebSocket. + RuntimeError: Cannot obtain socket from WSGI environment. + +This is expected, and it happens because the Django web server does not support +the WebSocket protocol. You can ignore the error, as the server will still work +using long-polling. + +To run the application with WebSocket enabled, you can use the Gunicorn web +server as follows: + + gunicorn -b :8000 --threads 100 --access-logfile - django_socketio.wsgi:application + +See the documentation for information on other supported deployment methods +that you can use to add support for WebSocket. diff --git a/examples/server/wsgi/django_socketio/requirements.txt b/examples/server/wsgi/django_socketio/requirements.txt index 0ed6894f..e58991b7 100644 --- a/examples/server/wsgi/django_socketio/requirements.txt +++ b/examples/server/wsgi/django_socketio/requirements.txt @@ -1,7 +1,11 @@ -asgiref==3.5.2 +asgiref==3.6.0 backports.zoneinfo==0.2.1 -bidict==0.22.0 -Django==4.1.9 -python-engineio==4.3.2 -python-socketio==5.6.0 +bidict==0.22.1 +Django==4.2.1 +gunicorn==20.1.0 +h11==0.14.0 +python-engineio==4.4.1 +python-socketio==5.8.0 +simple-websocket==0.10.0 sqlparse==0.4.4 +wsproto==1.2.0 From 4503f9710d9bf8865331febe279974c100bbe43c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 10:00:33 +0100 Subject: [PATCH 375/571] Bump socket.io-parser from 4.2.2 to 4.2.3 in /examples/client/javascript (#1194) #nolog Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 4.2.2 to 4.2.3. - [Release notes](https://github.com/socketio/socket.io-parser/releases) - [Changelog](https://github.com/socketio/socket.io-parser/blob/main/CHANGELOG.md) - [Commits](https://github.com/socketio/socket.io-parser/compare/4.2.2...4.2.3) --- updated-dependencies: - dependency-name: socket.io-parser dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/client/javascript/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/client/javascript/package-lock.json b/examples/client/javascript/package-lock.json index f700e357..27ab1964 100644 --- a/examples/client/javascript/package-lock.json +++ b/examples/client/javascript/package-lock.json @@ -744,9 +744,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/socket.io-parser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", - "integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz", + "integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -1448,9 +1448,9 @@ } }, "socket.io-parser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", - "integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz", + "integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==", "requires": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" From 47d117e2710f0eaae42a8de481ab4e934a197afb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 10:00:58 +0100 Subject: [PATCH 376/571] Bump socket.io-parser from 4.2.2 to 4.2.3 in /examples/server/javascript (#1195) #nolog Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 4.2.2 to 4.2.3. - [Release notes](https://github.com/socketio/socket.io-parser/releases) - [Changelog](https://github.com/socketio/socket.io-parser/blob/main/CHANGELOG.md) - [Commits](https://github.com/socketio/socket.io-parser/compare/4.2.2...4.2.3) --- updated-dependencies: - dependency-name: socket.io-parser dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/javascript/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/server/javascript/package-lock.json b/examples/server/javascript/package-lock.json index a5a38333..4b21a1ed 100644 --- a/examples/server/javascript/package-lock.json +++ b/examples/server/javascript/package-lock.json @@ -765,9 +765,9 @@ } }, "node_modules/socket.io-parser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", - "integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz", + "integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -1449,9 +1449,9 @@ } }, "socket.io-parser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", - "integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz", + "integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==", "requires": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" From 02b48a30e94f0e3a729b4e1a7a89918608f314c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 May 2023 19:24:52 +0100 Subject: [PATCH 377/571] Bump tornado from 5.0.2 to 6.3.2 in /examples/server/tornado (#1196) #nolog Bumps [tornado](https://github.com/tornadoweb/tornado) from 5.0.2 to 6.3.2. - [Changelog](https://github.com/tornadoweb/tornado/blob/master/docs/releases.rst) - [Commits](https://github.com/tornadoweb/tornado/compare/v5.0.2...v6.3.2) --- updated-dependencies: - dependency-name: tornado dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/tornado/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/tornado/requirements.txt b/examples/server/tornado/requirements.txt index 354f4a7a..ac27ff4d 100644 --- a/examples/server/tornado/requirements.txt +++ b/examples/server/tornado/requirements.txt @@ -1,4 +1,4 @@ -tornado==5.0.2 +tornado==6.3.2 python-engineio python_socketio six==1.10.0 From 81aee59b542adef380c43f4362daf205e61eb573 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 20 Jun 2023 12:24:54 +0100 Subject: [PATCH 378/571] Add readthedocs config file --- .readthedocs.yaml | 16 ++++++++++++++++ setup.cfg | 2 ++ 2 files changed, 18 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..26768c7d --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: docs/conf.py + +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/setup.cfg b/setup.cfg index 2cc839c9..860e9f99 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,3 +36,5 @@ client = websocket-client >= 0.54.0 asyncio_client = aiohttp >= 3.4 +docs = + sphinx From 4295de7cda2e60468b0606b5370c0adf468b4e1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Jul 2023 00:19:20 +0100 Subject: [PATCH 379/571] Bump django from 4.2.1 to 4.2.3 in /examples/server/wsgi/django_socketio (#1210) #nolog Bumps [django](https://github.com/django/django) from 4.2.1 to 4.2.3. - [Commits](https://github.com/django/django/compare/4.2.1...4.2.3) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/django_socketio/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/django_socketio/requirements.txt b/examples/server/wsgi/django_socketio/requirements.txt index e58991b7..4ddd7488 100644 --- a/examples/server/wsgi/django_socketio/requirements.txt +++ b/examples/server/wsgi/django_socketio/requirements.txt @@ -1,7 +1,7 @@ asgiref==3.6.0 backports.zoneinfo==0.2.1 bidict==0.22.1 -Django==4.2.1 +Django==4.2.3 gunicorn==20.1.0 h11==0.14.0 python-engineio==4.4.1 From c5064110fc4b3d4158437da6703ba331ba5a7cf8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Aug 2023 19:02:17 +0100 Subject: [PATCH 380/571] Bump tornado from 6.3.2 to 6.3.3 in /examples/server/tornado (#1225) #nolog Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.3.2 to 6.3.3. - [Changelog](https://github.com/tornadoweb/tornado/blob/master/docs/releases.rst) - [Commits](https://github.com/tornadoweb/tornado/compare/v6.3.2...v6.3.3) --- updated-dependencies: - dependency-name: tornado dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/tornado/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/tornado/requirements.txt b/examples/server/tornado/requirements.txt index ac27ff4d..1a7ff9d7 100644 --- a/examples/server/tornado/requirements.txt +++ b/examples/server/tornado/requirements.txt @@ -1,4 +1,4 @@ -tornado==6.3.2 +tornado==6.3.3 python-engineio python_socketio six==1.10.0 From 1b7ed682e06d7c8a758a2f89391ddc941f70ee98 Mon Sep 17 00:00:00 2001 From: Alex Kuhrt Date: Wed, 30 Aug 2023 17:41:49 +0200 Subject: [PATCH 381/571] Fix typos in the documentation (#1230) --- docs/client.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/client.rst b/docs/client.rst index e3dcc552..07d120e6 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -158,7 +158,7 @@ that is configured with the desired TLS/SSL options. The following example disables server certificate verification, which can be useful when connecting to a server that uses a self-signed certificate:: - http_session = request.Session() + http_session = requests.Session() http_session.verify = False sio = socketio.Client(http_session=http_session) sio.connect('https://example.com') @@ -173,7 +173,7 @@ And when using ``asyncio``:: Instead of disabling certificate verification, you can provide a custom certificate authority bundle to verify the certificate against:: - http_session = request.Session() + http_session = requests.Session() http_session.verify = '/path/to/ca.pem' sio = socketio.Client(http_session=http_session) sio.connect('https://example.com') @@ -190,7 +190,7 @@ And for ``asyncio``:: Below you can see how to use a client certificate to authenticate against the server:: - http_session = request.Session() + http_session = requests.Session() http_session.cert = ('/path/to/client/cert.pem', '/path/to/client/key.pem') sio = socketio.Client(http_session=http_session) sio.connect('https://example.com') From f49d65a0c3ff55f92a817994898c4182efa5ba69 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 3 Sep 2023 16:24:15 +0100 Subject: [PATCH 382/571] Upgrade to pypy-3.9 in unit tests --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index adb0825a..af896fb4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python: ['3.7', '3.8', '3.9', '3.10', '3.11', 'pypy-3.8'] + python: ['3.7', '3.8', '3.9', '3.10', '3.11', 'pypy-3.9'] exclude: # pypy3 currently fails to run on Windows - os: windows-latest From bf11ad36aebeebbc0b7a74d88d4b9a8fad113456 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 3 Sep 2023 16:39:44 +0100 Subject: [PATCH 383/571] Optimize memory usage during broadcasts (#1233) --- setup.cfg | 2 +- src/socketio/asyncio_manager.py | 47 +++++- src/socketio/asyncio_server.py | 17 +- src/socketio/base_manager.py | 41 ++++- src/socketio/server.py | 21 +-- tests/asyncio/test_asyncio_manager.py | 164 ++++++++++++++----- tests/asyncio/test_asyncio_pubsub_manager.py | 13 +- tests/asyncio/test_asyncio_server.py | 83 +++------- tests/common/test_base_manager.py | 121 +++++++++----- tests/common/test_pubsub_manager.py | 9 +- tests/common/test_server.py | 62 ++----- tests/performance/run.sh | 1 + tests/performance/server_send.py | 3 + tests/performance/server_send_broadcast.py | 30 ++++ 14 files changed, 366 insertions(+), 248 deletions(-) create mode 100644 tests/performance/server_send_broadcast.py diff --git a/setup.cfg b/setup.cfg index 860e9f99..712d015a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ packages = find: python_requires = >=3.6 install_requires = bidict >= 0.21.0 - python-engineio >= 4.3.0 + python-engineio >= 4.7.0 [options.packages.find] where = src diff --git a/src/socketio/asyncio_manager.py b/src/socketio/asyncio_manager.py index 47013dc6..2bf90011 100644 --- a/src/socketio/asyncio_manager.py +++ b/src/socketio/asyncio_manager.py @@ -1,5 +1,7 @@ import asyncio +from engineio import packet as eio_packet +from socketio import packet from .base_manager import BaseManager @@ -17,18 +19,45 @@ async def emit(self, event, data, namespace, room=None, skip_sid=None, """ if namespace not in self.rooms: return - tasks = [] + if isinstance(data, tuple): + # tuples are expanded to multiple arguments, everything else is + # sent as a single argument + data = list(data) + elif data is not None: + data = [data] + else: + data = [] if not isinstance(skip_sid, list): skip_sid = [skip_sid] - for sid, eio_sid in self.get_participants(namespace, room): - if sid not in skip_sid: - if callback is not None: + tasks = [] + if not callback: + # when callbacks aren't used the packets sent to each recipient are + # identical, so they can be generated once and reused + pkt = self.server.packet_class( + packet.EVENT, namespace=namespace, data=[event] + data) + encoded_packet = pkt.encode() + if not isinstance(encoded_packet, list): + encoded_packet = [encoded_packet] + eio_pkt = [eio_packet.Packet(eio_packet.MESSAGE, p) + for p in encoded_packet] + for sid, eio_sid in self.get_participants(namespace, room): + if sid not in skip_sid: + for p in eio_pkt: + tasks.append(asyncio.create_task( + self.server._send_eio_packet(eio_sid, p))) + else: + # callbacks are used, so each recipient must be sent a packet that + # contains a unique callback id + # note that callbacks when addressing a group of people are + # implemented but not tested or supported + for sid, eio_sid in self.get_participants(namespace, room): + if sid not in skip_sid: # pragma: no branch id = self._generate_ack_id(sid, callback) - else: - id = None - tasks.append(asyncio.create_task( - self.server._emit_internal(eio_sid, event, data, - namespace, id))) + pkt = self.server.packet_class( + packet.EVENT, namespace=namespace, data=[event] + data, + id=id) + tasks.append(asyncio.create_task( + self.server._send_packet(eio_sid, pkt))) if tasks == []: # pragma: no cover return await asyncio.wait(tasks) diff --git a/src/socketio/asyncio_server.py b/src/socketio/asyncio_server.py index e5df2a9b..aff8812a 100644 --- a/src/socketio/asyncio_server.py +++ b/src/socketio/asyncio_server.py @@ -424,19 +424,6 @@ async def sleep(self, seconds=0): """ return await self.eio.sleep(seconds) - async def _emit_internal(self, sid, event, data, namespace=None, id=None): - """Send a message to a client.""" - # tuples are expanded to multiple arguments, everything else is sent - # as a single argument - if isinstance(data, tuple): - data = list(data) - elif data is not None: - data = [data] - else: - data = [] - await self._send_packet(sid, self.packet_class( - packet.EVENT, namespace=namespace, data=[event] + data, id=id)) - async def _send_packet(self, eio_sid, pkt): """Send a Socket.IO packet to a client.""" encoded_packet = pkt.encode() @@ -446,6 +433,10 @@ async def _send_packet(self, eio_sid, pkt): else: await self.eio.send(eio_sid, encoded_packet) + async def _send_eio_packet(self, eio_sid, eio_pkt): + """Send a raw Engine.IO packet to a client.""" + await self.eio.send_packet(eio_sid, eio_pkt) + async def _handle_connect(self, eio_sid, namespace, data): """Handle a client connection request.""" namespace = namespace or '/' diff --git a/src/socketio/base_manager.py b/src/socketio/base_manager.py index 87d23879..4330bac4 100644 --- a/src/socketio/base_manager.py +++ b/src/socketio/base_manager.py @@ -2,6 +2,8 @@ import logging from bidict import bidict, ValueDuplicationError +from engineio import packet as eio_packet +from socketio import packet default_logger = logging.getLogger('socketio') @@ -161,15 +163,42 @@ def emit(self, event, data, namespace, room=None, skip_sid=None, connected to the namespace.""" if namespace not in self.rooms: return + if isinstance(data, tuple): + # tuples are expanded to multiple arguments, everything else is + # sent as a single argument + data = list(data) + elif data is not None: + data = [data] + else: + data = [] if not isinstance(skip_sid, list): skip_sid = [skip_sid] - for sid, eio_sid in self.get_participants(namespace, room): - if sid not in skip_sid: - if callback is not None: + if not callback: + # when callbacks aren't used the packets sent to each recipient are + # identical, so they can be generated once and reused + pkt = self.server.packet_class( + packet.EVENT, namespace=namespace, data=[event] + data) + encoded_packet = pkt.encode() + if not isinstance(encoded_packet, list): + encoded_packet = [encoded_packet] + eio_pkt = [eio_packet.Packet(eio_packet.MESSAGE, p) + for p in encoded_packet] + for sid, eio_sid in self.get_participants(namespace, room): + if sid not in skip_sid: + for p in eio_pkt: + self.server._send_eio_packet(eio_sid, p) + else: + # callbacks are used, so each recipient must be sent a packet that + # contains a unique callback id + # note that callbacks when addressing a group of people are + # implemented but not tested or supported + for sid, eio_sid in self.get_participants(namespace, room): + if sid not in skip_sid: # pragma: no branch id = self._generate_ack_id(sid, callback) - else: - id = None - self.server._emit_internal(eio_sid, event, data, namespace, id) + pkt = self.server.packet_class( + packet.EVENT, namespace=namespace, data=[event] + data, + id=id) + self.server._send_packet(eio_sid, pkt) def trigger_callback(self, sid, id, data): """Invoke an application callback.""" diff --git a/src/socketio/server.py b/src/socketio/server.py index e8231c31..48d55983 100644 --- a/src/socketio/server.py +++ b/src/socketio/server.py @@ -185,7 +185,7 @@ def on(self, event, handler=None, namespace=None): Example usage:: # as a decorator: - @socket_io.on('connect', namespace='/chat') + @sio.on('connect', namespace='/chat') def connect_handler(sid, environ): print('Connection request') if environ['REMOTE_ADDR'] in blacklisted: @@ -194,7 +194,7 @@ def connect_handler(sid, environ): # as a method: def message_handler(sid, msg): print('Received message: ', msg) - eio.send(sid, 'response') + sio.send(sid, 'response') socket_io.on('message', namespace='/chat', handler=message_handler) The handler function receives the ``sid`` (session ID) for the @@ -633,19 +633,6 @@ def sleep(self, seconds=0): """ return self.eio.sleep(seconds) - def _emit_internal(self, eio_sid, event, data, namespace=None, id=None): - """Send a message to a client.""" - # tuples are expanded to multiple arguments, everything else is sent - # as a single argument - if isinstance(data, tuple): - data = list(data) - elif data is not None: - data = [data] - else: - data = [] - self._send_packet(eio_sid, self.packet_class( - packet.EVENT, namespace=namespace, data=[event] + data, id=id)) - def _send_packet(self, eio_sid, pkt): """Send a Socket.IO packet to a client.""" encoded_packet = pkt.encode() @@ -655,6 +642,10 @@ def _send_packet(self, eio_sid, pkt): else: self.eio.send(eio_sid, encoded_packet) + def _send_eio_packet(self, eio_sid, eio_pkt): + """Send a raw Engine.IO packet to a client.""" + self.eio.send_packet(eio_sid, eio_pkt) + def _handle_connect(self, eio_sid, namespace, data): """Handle a client connection request.""" namespace = namespace or '/' diff --git a/tests/asyncio/test_asyncio_manager.py b/tests/asyncio/test_asyncio_manager.py index 9d9c849b..1fd7b0c4 100644 --- a/tests/asyncio/test_asyncio_manager.py +++ b/tests/asyncio/test_asyncio_manager.py @@ -4,6 +4,7 @@ from unittest import mock from socketio import asyncio_manager +from socketio import packet def AsyncMock(*args, **kwargs): @@ -33,8 +34,10 @@ def generate_id(): return str(id) mock_server = mock.MagicMock() - mock_server._emit_internal = AsyncMock() + mock_server._send_packet = AsyncMock() + mock_server._send_eio_packet = AsyncMock() mock_server.eio.generate_id = generate_id + mock_server.packet_class = packet.Packet self.bm = asyncio_manager.AsyncManager() self.bm.set_server(mock_server) self.bm.initialize() @@ -221,9 +224,11 @@ def test_emit_to_sid(self): 'my event', {'foo': 'bar'}, namespace='/foo', room=sid ) ) - self.bm.server._emit_internal.mock.assert_called_once_with( - '123', 'my event', {'foo': 'bar'}, '/foo', None - ) + assert self.bm.server._send_eio_packet.mock.call_count == 1 + assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + == '123' + pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_room(self): sid1 = self.bm.connect('123', '/foo') @@ -236,13 +241,15 @@ def test_emit_to_room(self): 'my event', {'foo': 'bar'}, namespace='/foo', room='bar' ) ) - assert self.bm.server._emit_internal.mock.call_count == 2 - self.bm.server._emit_internal.mock.assert_any_call( - '123', 'my event', {'foo': 'bar'}, '/foo', None - ) - self.bm.server._emit_internal.mock.assert_any_call( - '456', 'my event', {'foo': 'bar'}, '/foo', None - ) + assert self.bm.server._send_eio_packet.mock.call_count == 2 + assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + == '123' + assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][0] \ + == '456' + pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][1] \ + == pkt + assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_rooms(self): sid1 = self.bm.connect('123', '/foo') @@ -256,16 +263,19 @@ def test_emit_to_rooms(self): self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', room=['bar', 'baz']) ) - assert self.bm.server._emit_internal.mock.call_count == 3 - self.bm.server._emit_internal.mock.assert_any_call( - '123', 'my event', {'foo': 'bar'}, '/foo', None - ) - self.bm.server._emit_internal.mock.assert_any_call( - '456', 'my event', {'foo': 'bar'}, '/foo', None - ) - self.bm.server._emit_internal.mock.assert_any_call( - '789', 'my event', {'foo': 'bar'}, '/foo', None - ) + assert self.bm.server._send_eio_packet.mock.call_count == 3 + assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + == '123' + assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][0] \ + == '456' + assert self.bm.server._send_eio_packet.mock.call_args_list[2][0][0] \ + == '789' + pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][1] \ + == pkt + assert self.bm.server._send_eio_packet.mock.call_args_list[2][0][1] \ + == pkt + assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_all(self): sid1 = self.bm.connect('123', '/foo') @@ -275,16 +285,19 @@ def test_emit_to_all(self): self.bm.connect('789', '/foo') self.bm.connect('abc', '/bar') _run(self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo')) - assert self.bm.server._emit_internal.mock.call_count == 3 - self.bm.server._emit_internal.mock.assert_any_call( - '123', 'my event', {'foo': 'bar'}, '/foo', None - ) - self.bm.server._emit_internal.mock.assert_any_call( - '456', 'my event', {'foo': 'bar'}, '/foo', None - ) - self.bm.server._emit_internal.mock.assert_any_call( - '789', 'my event', {'foo': 'bar'}, '/foo', None - ) + assert self.bm.server._send_eio_packet.mock.call_count == 3 + assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + == '123' + assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][0] \ + == '456' + assert self.bm.server._send_eio_packet.mock.call_args_list[2][0][0] \ + == '789' + pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][1] \ + == pkt + assert self.bm.server._send_eio_packet.mock.call_args_list[2][0][1] \ + == pkt + assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_all_skip_one(self): sid1 = self.bm.connect('123', '/foo') @@ -298,13 +311,15 @@ def test_emit_to_all_skip_one(self): 'my event', {'foo': 'bar'}, namespace='/foo', skip_sid=sid2 ) ) - assert self.bm.server._emit_internal.mock.call_count == 2 - self.bm.server._emit_internal.mock.assert_any_call( - '123', 'my event', {'foo': 'bar'}, '/foo', None - ) - self.bm.server._emit_internal.mock.assert_any_call( - '789', 'my event', {'foo': 'bar'}, '/foo', None - ) + assert self.bm.server._send_eio_packet.mock.call_count == 2 + assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + == '123' + assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][0] \ + == '789' + pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][1] \ + == pkt + assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_all_skip_two(self): sid1 = self.bm.connect('123', '/foo') @@ -321,10 +336,11 @@ def test_emit_to_all_skip_two(self): skip_sid=[sid1, sid3], ) ) - assert self.bm.server._emit_internal.mock.call_count == 1 - self.bm.server._emit_internal.mock.assert_any_call( - '456', 'my event', {'foo': 'bar'}, '/foo', None - ) + assert self.bm.server._send_eio_packet.mock.call_count == 1 + assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + == '456' + pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_with_callback(self): sid = self.bm.connect('123', '/foo') @@ -336,9 +352,11 @@ def test_emit_with_callback(self): ) ) self.bm._generate_ack_id.assert_called_once_with(sid, 'cb') - self.bm.server._emit_internal.mock.assert_called_once_with( - '123', 'my event', {'foo': 'bar'}, '/foo', 11 - ) + assert self.bm.server._send_packet.mock.call_count == 1 + assert self.bm.server._send_packet.mock.call_args_list[0][0][0] \ + == '123' + pkt = self.bm.server._send_packet.mock.call_args_list[0][0][1] + assert pkt.encode() == '2/foo,11["my event",{"foo":"bar"}]' def test_emit_to_invalid_room(self): _run( @@ -347,3 +365,59 @@ def test_emit_to_invalid_room(self): def test_emit_to_invalid_namespace(self): _run(self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo')) + + def test_emit_with_tuple(self): + sid = self.bm.connect('123', '/foo') + _run( + self.bm.emit( + 'my event', ('foo', 'bar'), namespace='/foo', room=sid + ) + ) + assert self.bm.server._send_eio_packet.mock.call_count == 1 + assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + == '123' + pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + assert pkt.encode() == '42/foo,["my event","foo","bar"]' + + def test_emit_with_list(self): + sid = self.bm.connect('123', '/foo') + _run( + self.bm.emit( + 'my event', ['foo', 'bar'], namespace='/foo', room=sid + ) + ) + assert self.bm.server._send_eio_packet.mock.call_count == 1 + assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + == '123' + pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + assert pkt.encode() == '42/foo,["my event",["foo","bar"]]' + + def test_emit_with_none(self): + sid = self.bm.connect('123', '/foo') + _run( + self.bm.emit( + 'my event', None, namespace='/foo', room=sid + ) + ) + assert self.bm.server._send_eio_packet.mock.call_count == 1 + assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + == '123' + pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + assert pkt.encode() == '42/foo,["my event"]' + + def test_emit_binary(self): + sid = self.bm.connect('123', '/') + _run( + self.bm.emit( + u'my event', b'my binary data', namespace='/', room=sid + ) + ) + assert self.bm.server._send_eio_packet.mock.call_count == 2 + assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + == '123' + pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + assert pkt.encode() == '451-["my event",{"_placeholder":true,"num":0}]' + assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][0] \ + == '123' + pkt = self.bm.server._send_eio_packet.mock.call_args_list[1][0][1] + assert pkt.encode() == b'my binary data' diff --git a/tests/asyncio/test_asyncio_pubsub_manager.py b/tests/asyncio/test_asyncio_pubsub_manager.py index 80a821b4..5541bbda 100644 --- a/tests/asyncio/test_asyncio_pubsub_manager.py +++ b/tests/asyncio/test_asyncio_pubsub_manager.py @@ -8,6 +8,7 @@ from socketio import asyncio_manager from socketio import asyncio_pubsub_manager +from socketio import packet def AsyncMock(*args, **kwargs): @@ -38,7 +39,9 @@ def generate_id(): mock_server = mock.MagicMock() mock_server.eio.generate_id = generate_id - mock_server._emit_internal = AsyncMock() + mock_server.packet_class = packet.Packet + mock_server._send_packet = AsyncMock() + mock_server._send_eio_packet = AsyncMock() mock_server.disconnect = AsyncMock() self.pm = asyncio_pubsub_manager.AsyncPubSubManager() self.pm._publish = AsyncMock() @@ -164,9 +167,11 @@ def test_emit_with_ignore_queue(self): ) ) self.pm._publish.mock.assert_not_called() - self.pm.server._emit_internal.mock.assert_called_once_with( - '123', 'foo', 'bar', '/', None - ) + assert self.pm.server._send_eio_packet.mock.call_count == 1 + assert self.pm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + == '123' + pkt = self.pm.server._send_eio_packet.mock.call_args_list[0][0][1] + assert pkt.encode() == '42["foo","bar"]' def test_can_disconnect(self): sid = self.pm.connect('123', '/') diff --git a/tests/asyncio/test_asyncio_server.py b/tests/asyncio/test_asyncio_server.py index 27d8b378..617aadaf 100644 --- a/tests/asyncio/test_asyncio_server.py +++ b/tests/asyncio/test_asyncio_server.py @@ -5,6 +5,7 @@ from unittest import mock from engineio import json +from engineio import packet as eio_packet import pytest from socketio import asyncio_server @@ -32,7 +33,8 @@ def _run(coro): @unittest.skipIf(sys.version_info < (3, 5), 'only for Python 3.5+') @mock.patch('socketio.server.engineio.AsyncServer', **{ - 'return_value.generate_id.side_effect': [str(i) for i in range(1, 10)]}) + 'return_value.generate_id.side_effect': [str(i) for i in range(1, 10)], + 'return_value.send_packet': AsyncMock()}) class TestAsyncServer(unittest.TestCase): def tearDown(self): # restore JSON encoder, in case a test changed it @@ -295,72 +297,24 @@ def test_handle_request(self, eio): _run(s.handle_request('environ')) s.eio.handle_request.mock.assert_called_once_with('environ') - def test_emit_internal(self, eio): + def test_send_packet(self, eio): eio.return_value.send = AsyncMock() s = asyncio_server.AsyncServer() - _run(s._emit_internal('123', 'my event', 'my data', namespace='/foo')) + _run(s._send_packet('123', packet.Packet( + packet.EVENT, ['my event', 'my data'], namespace='/foo'))) s.eio.send.mock.assert_called_once_with( '123', '2/foo,["my event","my data"]' ) - def test_emit_internal_with_tuple(self, eio): + def test_send_eio_packet(self, eio): eio.return_value.send = AsyncMock() s = asyncio_server.AsyncServer() - _run( - s._emit_internal( - '123', 'my event', ('foo', 'bar'), namespace='/foo' - ) - ) - s.eio.send.mock.assert_called_once_with( - '123', '2/foo,["my event","foo","bar"]' - ) - - def test_emit_internal_with_list(self, eio): - eio.return_value.send = AsyncMock() - s = asyncio_server.AsyncServer() - _run( - s._emit_internal( - '123', 'my event', ['foo', 'bar'], namespace='/foo' - ) - ) - s.eio.send.mock.assert_called_once_with( - '123', '2/foo,["my event",["foo","bar"]]' - ) - - def test_emit_internal_with_none(self, eio): - eio.return_value.send = AsyncMock() - s = asyncio_server.AsyncServer() - _run(s._emit_internal('123', 'my event', None, namespace='/foo')) - s.eio.send.mock.assert_called_once_with( - '123', '2/foo,["my event"]' - ) - - def test_emit_internal_with_callback(self, eio): - eio.return_value.send = AsyncMock() - s = asyncio_server.AsyncServer() - id = s.manager._generate_ack_id('1', 'cb') - _run( - s._emit_internal( - '123', 'my event', 'my data', namespace='/foo', id=id - ) - ) - s.eio.send.mock.assert_called_once_with( - '123', '2/foo,1["my event","my data"]' - ) - - def test_emit_internal_default_namespace(self, eio): - eio.return_value.send = AsyncMock() - s = asyncio_server.AsyncServer() - _run(s._emit_internal('123', 'my event', 'my data')) - s.eio.send.mock.assert_called_once_with( - '123', '2["my event","my data"]' - ) - - def test_emit_internal_binary(self, eio): - eio.return_value.send = AsyncMock() - s = asyncio_server.AsyncServer() - _run(s._emit_internal('123', u'my event', b'my binary data')) - assert s.eio.send.mock.call_count == 2 + _run(s._send_eio_packet('123', eio_packet.Packet( + eio_packet.MESSAGE, 'hello'))) + assert s.eio.send_packet.mock.call_count == 1 + assert s.eio.send_packet.mock.call_args_list[0][0][0] == '123' + pkt = s.eio.send_packet.mock.call_args_list[0][0][1] + assert pkt.encode() == '4hello' def test_transport(self, eio): eio.return_value.send = AsyncMock() @@ -804,8 +758,10 @@ def test_send_with_ack(self, eio): cb = mock.MagicMock() id1 = s.manager._generate_ack_id('1', cb) id2 = s.manager._generate_ack_id('1', cb) - _run(s._emit_internal('123', 'my event', ['foo'], id=id1)) - _run(s._emit_internal('123', 'my event', ['bar'], id=id2)) + _run(s._send_packet('123', packet.Packet( + packet.EVENT, ['my event', 'foo'], id=id1))) + _run(s._send_packet('123', packet.Packet( + packet.EVENT, ['my event', 'bar'], id=id2))) _run(s._handle_eio_message('123', '31["foo",2]')) cb.assert_called_once_with('foo', 2) @@ -818,8 +774,9 @@ def test_send_with_ack_namespace(self, eio): cb = mock.MagicMock() id = s.manager._generate_ack_id('1', cb) _run( - s._emit_internal( - '123', 'my event', ['foo'], namespace='/foo', id=id + s._send_packet( + '123', packet.Packet(packet.EVENT, ['my event', 'foo'], + namespace='/foo', id=id) ) ) _run(s._handle_eio_message('123', '3/foo,1["foo",2]')) diff --git a/tests/common/test_base_manager.py b/tests/common/test_base_manager.py index b9337452..ae1490c8 100644 --- a/tests/common/test_base_manager.py +++ b/tests/common/test_base_manager.py @@ -4,6 +4,7 @@ import pytest from socketio import base_manager +from socketio import packet class TestBaseManager(unittest.TestCase): @@ -17,6 +18,7 @@ def generate_id(): mock_server = mock.MagicMock() mock_server.eio.generate_id = generate_id + mock_server.packet_class = packet.Packet self.bm = base_manager.BaseManager() self.bm.set_server(mock_server) self.bm.initialize() @@ -205,9 +207,10 @@ def test_emit_to_sid(self): sid = self.bm.connect('123', '/foo') self.bm.connect('456', '/foo') self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', room=sid) - self.bm.server._emit_internal.assert_called_once_with( - '123', 'my event', {'foo': 'bar'}, '/foo', None - ) + assert self.bm.server._send_eio_packet.call_count == 1 + assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' + pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] + assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_room(self): sid1 = self.bm.connect('123', '/foo') @@ -216,13 +219,12 @@ def test_emit_to_room(self): self.bm.enter_room(sid2, '/foo', 'bar') self.bm.connect('789', '/foo') self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', room='bar') - assert self.bm.server._emit_internal.call_count == 2 - self.bm.server._emit_internal.assert_any_call( - '123', 'my event', {'foo': 'bar'}, '/foo', None - ) - self.bm.server._emit_internal.assert_any_call( - '456', 'my event', {'foo': 'bar'}, '/foo', None - ) + assert self.bm.server._send_eio_packet.call_count == 2 + assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' + assert self.bm.server._send_eio_packet.call_args_list[1][0][0] == '456' + pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] + assert pkt == self.bm.server._send_eio_packet.call_args_list[1][0][1] + assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_rooms(self): sid1 = self.bm.connect('123', '/foo') @@ -234,16 +236,14 @@ def test_emit_to_rooms(self): self.bm.enter_room(sid3, '/foo', 'baz') self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', room=['bar', 'baz']) - assert self.bm.server._emit_internal.call_count == 3 - self.bm.server._emit_internal.assert_any_call( - '123', 'my event', {'foo': 'bar'}, '/foo', None - ) - self.bm.server._emit_internal.assert_any_call( - '456', 'my event', {'foo': 'bar'}, '/foo', None - ) - self.bm.server._emit_internal.assert_any_call( - '789', 'my event', {'foo': 'bar'}, '/foo', None - ) + assert self.bm.server._send_eio_packet.call_count == 3 + assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' + assert self.bm.server._send_eio_packet.call_args_list[1][0][0] == '456' + assert self.bm.server._send_eio_packet.call_args_list[2][0][0] == '789' + pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] + assert pkt == self.bm.server._send_eio_packet.call_args_list[1][0][1] + assert pkt == self.bm.server._send_eio_packet.call_args_list[2][0][1] + assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_all(self): sid1 = self.bm.connect('123', '/foo') @@ -253,16 +253,14 @@ def test_emit_to_all(self): self.bm.connect('789', '/foo') self.bm.connect('abc', '/bar') self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo') - assert self.bm.server._emit_internal.call_count == 3 - self.bm.server._emit_internal.assert_any_call( - '123', 'my event', {'foo': 'bar'}, '/foo', None - ) - self.bm.server._emit_internal.assert_any_call( - '456', 'my event', {'foo': 'bar'}, '/foo', None - ) - self.bm.server._emit_internal.assert_any_call( - '789', 'my event', {'foo': 'bar'}, '/foo', None - ) + assert self.bm.server._send_eio_packet.call_count == 3 + assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' + assert self.bm.server._send_eio_packet.call_args_list[1][0][0] == '456' + assert self.bm.server._send_eio_packet.call_args_list[2][0][0] == '789' + pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] + assert pkt == self.bm.server._send_eio_packet.call_args_list[1][0][1] + assert pkt == self.bm.server._send_eio_packet.call_args_list[2][0][1] + assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_all_skip_one(self): sid1 = self.bm.connect('123', '/foo') @@ -274,13 +272,12 @@ def test_emit_to_all_skip_one(self): self.bm.emit( 'my event', {'foo': 'bar'}, namespace='/foo', skip_sid=sid2 ) - assert self.bm.server._emit_internal.call_count == 2 - self.bm.server._emit_internal.assert_any_call( - '123', 'my event', {'foo': 'bar'}, '/foo', None - ) - self.bm.server._emit_internal.assert_any_call( - '789', 'my event', {'foo': 'bar'}, '/foo', None - ) + assert self.bm.server._send_eio_packet.call_count == 2 + assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' + assert self.bm.server._send_eio_packet.call_args_list[1][0][0] == '789' + pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] + assert pkt == self.bm.server._send_eio_packet.call_args_list[1][0][1] + assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_all_skip_two(self): sid1 = self.bm.connect('123', '/foo') @@ -295,10 +292,10 @@ def test_emit_to_all_skip_two(self): namespace='/foo', skip_sid=[sid1, sid3], ) - assert self.bm.server._emit_internal.call_count == 1 - self.bm.server._emit_internal.assert_any_call( - '456', 'my event', {'foo': 'bar'}, '/foo', None - ) + assert self.bm.server._send_eio_packet.call_count == 1 + assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '456' + pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] + assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_with_callback(self): sid = self.bm.connect('123', '/foo') @@ -308,12 +305,48 @@ def test_emit_with_callback(self): 'my event', {'foo': 'bar'}, namespace='/foo', callback='cb' ) self.bm._generate_ack_id.assert_called_once_with(sid, 'cb') - self.bm.server._emit_internal.assert_called_once_with( - '123', 'my event', {'foo': 'bar'}, '/foo', 11 - ) + assert self.bm.server._send_packet.call_count == 1 + assert self.bm.server._send_packet.call_args_list[0][0][0] == '123' + pkt = self.bm.server._send_packet.call_args_list[0][0][1] + assert pkt.encode() == '2/foo,11["my event",{"foo":"bar"}]' def test_emit_to_invalid_room(self): self.bm.emit('my event', {'foo': 'bar'}, namespace='/', room='123') def test_emit_to_invalid_namespace(self): self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo') + + def test_emit_with_tuple(self): + sid = self.bm.connect('123', '/foo') + self.bm.emit('my event', ('foo', 'bar'), namespace='/foo', room=sid) + assert self.bm.server._send_eio_packet.call_count == 1 + assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' + pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] + assert pkt.encode() == '42/foo,["my event","foo","bar"]' + + def test_emit_with_list(self): + sid = self.bm.connect('123', '/foo') + self.bm.emit('my event', ['foo', 'bar'], namespace='/foo', room=sid) + assert self.bm.server._send_eio_packet.call_count == 1 + assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' + pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] + assert pkt.encode() == '42/foo,["my event",["foo","bar"]]' + + def test_emit_with_none(self): + sid = self.bm.connect('123', '/foo') + self.bm.emit('my event', None, namespace='/foo', room=sid) + assert self.bm.server._send_eio_packet.call_count == 1 + assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' + pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] + assert pkt.encode() == '42/foo,["my event"]' + + def test_emit_binary(self): + sid = self.bm.connect('123', '/') + self.bm.emit(u'my event', b'my binary data', namespace='/', room=sid) + assert self.bm.server._send_eio_packet.call_count == 2 + assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' + pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] + assert pkt.encode() == '451-["my event",{"_placeholder":true,"num":0}]' + assert self.bm.server._send_eio_packet.call_args_list[1][0][0] == '123' + pkt = self.bm.server._send_eio_packet.call_args_list[1][0][1] + assert pkt.encode() == b'my binary data' diff --git a/tests/common/test_pubsub_manager.py b/tests/common/test_pubsub_manager.py index b1bb53b3..51e1f921 100644 --- a/tests/common/test_pubsub_manager.py +++ b/tests/common/test_pubsub_manager.py @@ -7,6 +7,7 @@ from socketio import base_manager from socketio import pubsub_manager +from socketio import packet class TestPubSubManager(unittest.TestCase): @@ -20,6 +21,7 @@ def generate_id(): mock_server = mock.MagicMock() mock_server.eio.generate_id = generate_id + mock_server.packet_class = packet.Packet self.pm = pubsub_manager.PubSubManager() self.pm._publish = mock.MagicMock() self.pm.set_server(mock_server) @@ -157,9 +159,10 @@ def test_emit_with_ignore_queue(self): 'foo', 'bar', room=sid, namespace='/', ignore_queue=True ) self.pm._publish.assert_not_called() - self.pm.server._emit_internal.assert_called_once_with( - '123', 'foo', 'bar', '/', None - ) + assert self.pm.server._send_eio_packet.call_count == 1 + assert self.pm.server._send_eio_packet.call_args_list[0][0][0] == '123' + pkt = self.pm.server._send_eio_packet.call_args_list[0][0][1] + assert pkt.encode() == '42["foo","bar"]' def test_can_disconnect(self): sid = self.pm.connect('123', '/') diff --git a/tests/common/test_server.py b/tests/common/test_server.py index 7d06c934..9285e570 100644 --- a/tests/common/test_server.py +++ b/tests/common/test_server.py @@ -3,6 +3,7 @@ from unittest import mock from engineio import json +from engineio import packet as eio_packet import pytest from socketio import exceptions @@ -271,53 +272,22 @@ def test_handle_request(self, eio): 'environ', 'start_response' ) - def test_emit_internal(self, eio): + def test_send_packet(self, eio): s = server.Server() - s._emit_internal('123', 'my event', 'my data', namespace='/foo') + s._send_packet('123', packet.Packet( + packet.EVENT, ['my event', 'my data'], namespace='/foo')) s.eio.send.assert_called_once_with( '123', '2/foo,["my event","my data"]' ) - def test_emit_internal_with_tuple(self, eio): + def test_send_eio_packet(self, eio): s = server.Server() - s._emit_internal('123', 'my event', ('foo', 'bar'), namespace='/foo') - s.eio.send.assert_called_once_with( - '123', '2/foo,["my event","foo","bar"]' - ) - - def test_emit_internal_with_list(self, eio): - s = server.Server() - s._emit_internal('123', 'my event', ['foo', 'bar'], namespace='/foo') - s.eio.send.assert_called_once_with( - '123', '2/foo,["my event",["foo","bar"]]' - ) - - def test_emit_internal_with_none(self, eio): - s = server.Server() - s._emit_internal('123', 'my event', None, namespace='/foo') - s.eio.send.assert_called_once_with( - '123', '2/foo,["my event"]' - ) - - def test_emit_internal_with_callback(self, eio): - s = server.Server() - id = s.manager._generate_ack_id('1', 'cb') - s._emit_internal('123', 'my event', 'my data', namespace='/foo', id=id) - s.eio.send.assert_called_once_with( - '123', '2/foo,1["my event","my data"]' - ) - - def test_emit_internal_default_namespace(self, eio): - s = server.Server() - s._emit_internal('123', 'my event', 'my data') - s.eio.send.assert_called_once_with( - '123', '2["my event","my data"]' - ) - - def test_emit_internal_binary(self, eio): - s = server.Server() - s._emit_internal('123', u'my event', b'my binary data') - assert s.eio.send.call_count == 2 + s._send_eio_packet('123', eio_packet.Packet( + eio_packet.MESSAGE, 'hello')) + assert s.eio.send_packet.call_count == 1 + assert s.eio.send_packet.call_args_list[0][0][0] == '123' + pkt = s.eio.send_packet.call_args_list[0][0][1] + assert pkt.encode() == '4hello' def test_transport(self, eio): s = server.Server() @@ -412,7 +382,6 @@ def test_handle_connect_namespace_twice(self, eio): assert s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_any_call('123', '0/foo,{"sid":"1"}') - print(s.eio.send.call_args_list) s.eio.send.assert_any_call('123', '4/foo,"Unable to connect"') def test_handle_connect_always_connect(self, eio): @@ -714,8 +683,10 @@ def test_send_with_ack(self, eio): cb = mock.MagicMock() id1 = s.manager._generate_ack_id('1', cb) id2 = s.manager._generate_ack_id('1', cb) - s._emit_internal('123', 'my event', ['foo'], id=id1) - s._emit_internal('123', 'my event', ['bar'], id=id2) + s._send_packet('123', packet.Packet( + packet.EVENT, ['my event', 'foo'], id=id1)) + s._send_packet('123', packet.Packet( + packet.EVENT, ['my event', 'bar'], id=id2)) s._handle_eio_message('123', '31["foo",2]') cb.assert_called_once_with('foo', 2) @@ -726,7 +697,8 @@ def test_send_with_ack_namespace(self, eio): s._handle_eio_message('123', '0/foo,') cb = mock.MagicMock() id = s.manager._generate_ack_id('1', cb) - s._emit_internal('123', 'my event', ['foo'], namespace='/foo', id=id) + s._send_packet('123', packet.Packet( + packet.EVENT, ['my event', 'foo'], namespace='/foo', id=id)) s._handle_eio_message('123', '3/foo,1["foo",2]') cb.assert_called_once_with('foo', 2) diff --git a/tests/performance/run.sh b/tests/performance/run.sh index 328e6ae1..ed78fc0d 100755 --- a/tests/performance/run.sh +++ b/tests/performance/run.sh @@ -5,3 +5,4 @@ python json_packet.py python namespace_packet.py python server_receive.py python server_send.py +python server_send_broadcast.py diff --git a/tests/performance/server_send.py b/tests/performance/server_send.py index 6c899118..b6b2d70e 100644 --- a/tests/performance/server_send.py +++ b/tests/performance/server_send.py @@ -6,6 +6,9 @@ class Server(socketio.Server): def _send_packet(self, eio_sid, pkt): pass + def _send_eio_packet(self, eio_sid, eio_pkt): + pass + def test(): s = Server() diff --git a/tests/performance/server_send_broadcast.py b/tests/performance/server_send_broadcast.py new file mode 100644 index 00000000..ce99d0a4 --- /dev/null +++ b/tests/performance/server_send_broadcast.py @@ -0,0 +1,30 @@ +import time +import socketio + + +class Server(socketio.Server): + def _send_packet(self, eio_sid, pkt): + pass + + def _send_eio_packet(self, eio_sid, eio_pkt): + pass + + +def test(): + s = Server() + start = time.time() + count = 0 + for i in range(100): + s._handle_eio_connect(str(i), 'environ') + s._handle_eio_message(str(i), '0') + while True: + s.emit('test', 'hello') + count += 1 + if time.time() - start >= 5: + break + return count + + +if __name__ == '__main__': + count = test() + print('server_send:', count, 'packets received.') From 8d7be9989ca761ccadb21f65363864c5c08e928b Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 3 Sep 2023 20:19:49 +0100 Subject: [PATCH 384/571] Release 5.9.0 --- CHANGES.md | 12 ++++++++++++ setup.cfg | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1cda173b..f01e9fa8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,17 @@ # python-socketio change log +**Release 5.9.0** - 2023-09-03 + +- Optimized performance and memory usage for broadcasts [#1233](https://github.com/miguelgrinberg/python-socketio/issues/1233) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bf11ad36aebeebbc0b7a74d88d4b9a8fad113456)) +- Improved documentation on horizontal scaling [#1099](https://github.com/miguelgrinberg/python-socketio/issues/1099) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/9d02247bc5914798a965cf423e978747a5535e33)) +- Corrected user session documentation example [#1170](https://github.com/miguelgrinberg/python-socketio/issues/1170) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/72dfdc6c8aff7f4f61c409e771df6a19299ad36f)) +- Improved grammar in documentation [#1175](https://github.com/miguelgrinberg/python-socketio/issues/1175) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c66d7a9f1ade0f728840b8ee9079599dd6a3de3f)) (thanks **zykron1**!) +- Fix docstring typo: client/server mixup [#1163](https://github.com/miguelgrinberg/python-socketio/issues/1163) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/598dd7e258c0c1d050456bbad2cd867dfca3df0f)) (thanks **Sasja**!) +- Fix typos in the documentation [#1230](https://github.com/miguelgrinberg/python-socketio/issues/1230) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1b7ed682e06d7c8a758a2f89391ddc941f70ee98)) (thanks **Alex Kuhrt**!) +- Upgrade dependencies in Django server example ([commit](https://github.com/miguelgrinberg/python-socketio/commit/00b31663c29968bd50282d22fca9b95c09e4becf)) +- Update reference JavaScript examples ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6df96fb1c598c00ddf8f8995cbc095f0383d38ef)) +- Upgrade to pypy-3.9 in unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f49d65a0c3ff55f92a817994898c4182efa5ba69)) + **Release 5.8.0** - 2023-03-16 - Made kombu client manager more robust and efficient ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8293dc3f8fa90f3d92192a702b28c23d8c516110)) diff --git a/setup.cfg b/setup.cfg index 712d015a..5eebdce6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = python-socketio -version = 5.8.1.dev0 +version = 5.9.0 author = Miguel Grinberg author_email = miguel.grinberg@gmail.com description = Socket.IO server and client for Python From 5541ddaa9fb4be0c6ba34b322e4e8d2aaf8ff9af Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 3 Sep 2023 20:22:48 +0100 Subject: [PATCH 385/571] Version 5.9.1.dev0 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 5eebdce6..e5e80a6c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = python-socketio -version = 5.9.0 +version = 5.9.1.dev0 author = Miguel Grinberg author_email = miguel.grinberg@gmail.com description = Socket.IO server and client for Python From 7208ec09e17466758cebb0732801ec84b25f1299 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 6 Sep 2023 23:41:57 +0100 Subject: [PATCH 386/571] Refactor common testing helpers into a separate module --- tests/asyncio/helpers.py | 18 ++++++++++++ tests/asyncio/test_asyncio_client.py | 30 +------------------- tests/asyncio/test_asyncio_manager.py | 17 +---------- tests/asyncio/test_asyncio_namespace.py | 17 +---------- tests/asyncio/test_asyncio_pubsub_manager.py | 17 +---------- tests/asyncio/test_asyncio_server.py | 17 +---------- 6 files changed, 23 insertions(+), 93 deletions(-) create mode 100644 tests/asyncio/helpers.py diff --git a/tests/asyncio/helpers.py b/tests/asyncio/helpers.py new file mode 100644 index 00000000..09e323c7 --- /dev/null +++ b/tests/asyncio/helpers.py @@ -0,0 +1,18 @@ +import asyncio +from unittest import mock + + +def AsyncMock(*args, **kwargs): + """Return a mock asynchronous function.""" + m = mock.MagicMock(*args, **kwargs) + + async def mock_coro(*args, **kwargs): + return m(*args, **kwargs) + + mock_coro.mock = m + return mock_coro + + +def _run(coro): + """Run the given coroutine.""" + return asyncio.get_event_loop().run_until_complete(coro) diff --git a/tests/asyncio/test_asyncio_client.py b/tests/asyncio/test_asyncio_client.py index b8d5c4ca..96b998a0 100644 --- a/tests/asyncio/test_asyncio_client.py +++ b/tests/asyncio/test_asyncio_client.py @@ -11,35 +11,7 @@ from engineio import exceptions as engineio_exceptions from socketio import exceptions from socketio import packet - - -def AsyncMock(*args, **kwargs): - """Return a mock asynchronous function.""" - m = mock.MagicMock(*args, **kwargs) - - async def mock_coro(*args, **kwargs): - return m(*args, **kwargs) - - mock_coro.mock = m - return mock_coro - - -@contextmanager -def mock_wait_for(): - async def fake_wait_for(coro, timeout): - await coro - await fake_wait_for._mock(timeout) - - original_wait_for = asyncio.wait_for - asyncio.wait_for = fake_wait_for - fake_wait_for._mock = AsyncMock() - yield - asyncio.wait_for = original_wait_for - - -def _run(coro): - """Run the given coroutine.""" - return asyncio.get_event_loop().run_until_complete(coro) +from .helpers import AsyncMock, _run @unittest.skipIf(sys.version_info < (3, 5), 'only for Python 3.5+') diff --git a/tests/asyncio/test_asyncio_manager.py b/tests/asyncio/test_asyncio_manager.py index 1fd7b0c4..2d2768ad 100644 --- a/tests/asyncio/test_asyncio_manager.py +++ b/tests/asyncio/test_asyncio_manager.py @@ -5,22 +5,7 @@ from socketio import asyncio_manager from socketio import packet - - -def AsyncMock(*args, **kwargs): - """Return a mock asynchronous function.""" - m = mock.MagicMock(*args, **kwargs) - - async def mock_coro(*args, **kwargs): - return m(*args, **kwargs) - - mock_coro.mock = m - return mock_coro - - -def _run(coro): - """Run the given coroutine.""" - return asyncio.get_event_loop().run_until_complete(coro) +from .helpers import AsyncMock, _run @unittest.skipIf(sys.version_info < (3, 5), 'only for Python 3.5+') diff --git a/tests/asyncio/test_asyncio_namespace.py b/tests/asyncio/test_asyncio_namespace.py index fa833e08..b7f57313 100644 --- a/tests/asyncio/test_asyncio_namespace.py +++ b/tests/asyncio/test_asyncio_namespace.py @@ -4,22 +4,7 @@ from unittest import mock from socketio import asyncio_namespace - - -def AsyncMock(*args, **kwargs): - """Return a mock asynchronous function.""" - m = mock.MagicMock(*args, **kwargs) - - async def mock_coro(*args, **kwargs): - return m(*args, **kwargs) - - mock_coro.mock = m - return mock_coro - - -def _run(coro): - """Run the given coroutine.""" - return asyncio.get_event_loop().run_until_complete(coro) +from .helpers import AsyncMock, _run @unittest.skipIf(sys.version_info < (3, 5), 'only for Python 3.5+') diff --git a/tests/asyncio/test_asyncio_pubsub_manager.py b/tests/asyncio/test_asyncio_pubsub_manager.py index 5541bbda..f1a63ebe 100644 --- a/tests/asyncio/test_asyncio_pubsub_manager.py +++ b/tests/asyncio/test_asyncio_pubsub_manager.py @@ -9,22 +9,7 @@ from socketio import asyncio_manager from socketio import asyncio_pubsub_manager from socketio import packet - - -def AsyncMock(*args, **kwargs): - """Return a mock asynchronous function.""" - m = mock.MagicMock(*args, **kwargs) - - async def mock_coro(*args, **kwargs): - return m(*args, **kwargs) - - mock_coro.mock = m - return mock_coro - - -def _run(coro): - """Run the given coroutine.""" - return asyncio.get_event_loop().run_until_complete(coro) +from .helpers import AsyncMock, _run @unittest.skipIf(sys.version_info < (3, 5), 'only for Python 3.5+') diff --git a/tests/asyncio/test_asyncio_server.py b/tests/asyncio/test_asyncio_server.py index 617aadaf..0adce679 100644 --- a/tests/asyncio/test_asyncio_server.py +++ b/tests/asyncio/test_asyncio_server.py @@ -13,22 +13,7 @@ from socketio import exceptions from socketio import namespace from socketio import packet - - -def AsyncMock(*args, **kwargs): - """Return a mock asynchronous function.""" - m = mock.MagicMock(*args, **kwargs) - - async def mock_coro(*args, **kwargs): - return m(*args, **kwargs) - - mock_coro.mock = m - return mock_coro - - -def _run(coro): - """Run the given coroutine.""" - return asyncio.get_event_loop().run_until_complete(coro) +from .helpers import AsyncMock, _run @unittest.skipIf(sys.version_info < (3, 5), 'only for Python 3.5+') From 55d6310eb3e194b1e67e40942a953bc4413b98b7 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Fri, 8 Sep 2023 23:24:58 +0100 Subject: [PATCH 387/571] Simplified client (#1237) --- docs/api.rst | 12 + docs/client.rst | 230 ++++++++++++++++-- docs/conf.py | 2 + examples/README.rst | 4 +- examples/client/README.rst | 8 +- examples/client/{threads => async}/README.rst | 18 +- .../{asyncio => async}/fiddle_client.py | 0 .../{asyncio => async}/latency_client.py | 0 examples/client/{asyncio => sync}/README.rst | 18 +- .../client/{threads => sync}/fiddle_client.py | 0 .../{threads => sync}/latency_client.py | 0 examples/simple-client/README.rst | 15 ++ examples/simple-client/async/README.rst | 33 +++ examples/simple-client/async/fiddle_client.py | 13 + .../simple-client/async/latency_client.py | 25 ++ examples/simple-client/sync/README.rst | 32 +++ examples/simple-client/sync/fiddle_client.py | 12 + examples/simple-client/sync/latency_client.py | 24 ++ src/socketio/__init__.py | 12 +- src/socketio/asyncio_client.py | 22 +- src/socketio/asyncio_simple_client.py | 193 +++++++++++++++ src/socketio/client.py | 22 +- src/socketio/exceptions.py | 4 + src/socketio/simple_client.py | 177 ++++++++++++++ tests/asyncio/test_asyncio_client.py | 54 +++- tests/asyncio/test_asyncio_manager.py | 1 - tests/asyncio/test_asyncio_namespace.py | 1 - tests/asyncio/test_asyncio_simple_client.py | 163 +++++++++++++ tests/common/test_client.py | 40 ++- tests/common/test_simple_client.py | 146 +++++++++++ 30 files changed, 1208 insertions(+), 73 deletions(-) rename examples/client/{threads => async}/README.rst (57%) rename examples/client/{asyncio => async}/fiddle_client.py (100%) rename examples/client/{asyncio => async}/latency_client.py (100%) rename examples/client/{asyncio => sync}/README.rst (59%) rename examples/client/{threads => sync}/fiddle_client.py (100%) rename examples/client/{threads => sync}/latency_client.py (100%) create mode 100644 examples/simple-client/README.rst create mode 100644 examples/simple-client/async/README.rst create mode 100644 examples/simple-client/async/fiddle_client.py create mode 100644 examples/simple-client/async/latency_client.py create mode 100644 examples/simple-client/sync/README.rst create mode 100644 examples/simple-client/sync/fiddle_client.py create mode 100644 examples/simple-client/sync/latency_client.py create mode 100644 src/socketio/asyncio_simple_client.py create mode 100644 src/socketio/simple_client.py create mode 100644 tests/asyncio/test_asyncio_simple_client.py create mode 100644 tests/common/test_simple_client.py diff --git a/docs/api.rst b/docs/api.rst index 0c685e6e..2993c735 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -6,6 +6,18 @@ API Reference .. module:: socketio +``SimpleClient`` class +---------------------- + +.. autoclass:: SimpleClient + :members: + +``AsyncSimpleClient`` class +--------------------------- + +.. autoclass:: AsyncSimpleClient + :members: + ``Client`` class ---------------- diff --git a/docs/client.rst b/docs/client.rst index 07d120e6..3344bd35 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -1,15 +1,16 @@ -The Socket.IO Client -==================== +The Socket.IO Clients +===================== This package contains two Socket.IO clients: -- The :func:`socketio.Client` class creates a client compatible with the - standard Python library. -- The :func:`socketio.AsyncClient` class creates a client compatible with - the ``asyncio`` package. +- a "simple" client, which provides a straightforward API that is sufficient + for most applications +- an "event-driven" client, which provides access to all the features of the + Socket.IO protocol -The methods in the two clients are the same, with the only difference that in -the ``asyncio`` client most methods are implemented as coroutines. +Each of these clients comes in two variants: one for the standard Python +library, and another for asynchronous applications built with the ``asyncio`` +package. Installation ------------ @@ -23,8 +24,174 @@ If instead you plan on using the ``asyncio`` client, then use this:: pip install "python-socketio[asyncio_client]" +Using the Simple Client +----------------------- + +The advantage of the simple client is that it abstracts away the logic required +to maintain a Socket.IO connection. This client handles disconnections and +reconnections in a completely transparent way, without adding any complexity to +the application. + +Creating a Client Instance +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To instantiate a Socket.IO client, create an instance of the appropriate client +class:: + + import socketio + + # standard Python + sio = socketio.SimpleClient() + + # asyncio + sio = socketio.AsyncSimpleClient() + +Connecting to a Server +~~~~~~~~~~~~~~~~~~~~~~ + +The connection to a server is established by calling the ``connect()`` +method:: + + sio.connect('http://localhost:5000') + +In the case of the ``asyncio`` client, the method is a coroutine:: + + await sio.connect('http://localhost:5000') + +By default the client first connects to the server using the long-polling +transport, and then attempts to upgrade the connection to use WebSocket. To +connect directly using WebSocket, use the ``transports`` argument:: + + sio.connect('http://localhost:5000', transports=['websocket']) + +Upon connection, the server assigns the client a unique session identifier. +The application can find this identifier in the ``sid`` attribute:: + + print('my sid is', sio.sid) + +The Socket.IO transport that is used in the connection can be obtained from the +``transport`` attribute:: + + print('my transport is', sio.transport) + +The transport is given as a string, and can be either ``'websocket'`` or +``'polling'``. + +TLS/SSL Support +^^^^^^^^^^^^^^^ + +The client supports TLS/SSL connections. To enable it, use a ``https://`` +connection URL:: + + sio.connect('https://example.com') + +Or when using ``asyncio``:: + + await sio.connect('https://example.com') + +The client verifies server certificates by default. Consult the documentation +for the event-driven client for information on how to customize this behavior. + +Emitting Events +~~~~~~~~~~~~~~~ + +The client can emit an event to the server using the ``emit()`` method:: + + sio.emit('my message', {'foo': 'bar'}) + +Or in the case of ``asyncio``, as a coroutine:: + + await sio.emit('my message', {'foo': 'bar'}) + +The arguments provided to the method are the name of the event to emit and the +optional data that is passed on to the server. The data can be of type ``str``, +``bytes``, ``dict``, ``list`` or ``tuple``. When sending a ``list`` or a +``tuple``, the elements in it need to be of any allowed types except ``tuple``. +When a tuple is used, the elements of the tuple will be passed as individual +arguments to the server-side event handler function. + +Receiving Events +~~~~~~~~~~~~~~~~ + +The client can wait for the server to emit an event with the ``receive()`` +method:: + + event = sio.receive() + print(f'received event: "{event[0]}" with arguments {event[1:]}') + +When using ``asyncio``, this method needs to be awaited:: + + event = await sio.receive() + print(f'received event: "{event[0]}" with arguments {event[1:]}') + +The return value of ``receive()`` is a list. The first element of this list is +the event name, while the remaining elements are the arguments passed by the +server. + +With the usage shown above, the ``receive()`` method will return only when an +event is received from the server. An optional timeout in seconds can be passed +to prevent the client from waiting forever:: + + from socketio.exceptions import TimeoutError + + try: + event = sio.receive(timeout=5) + except TimeoutError: + print('timed out waiting for event') + else: + print('received event:', event) + +Or with ``asyncio``:: + + from socketio.exceptions import TimeoutError + + try: + event = await sio.receive(timeout=5) + except TimeoutError: + print('timed out waiting for event') + else: + print('received event:', event) + +Disconnecting from the Server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +At any time the client can request to be disconnected from the server by +invoking the ``disconnect()`` method:: + + sio.disconnect() + +For the ``asyncio`` client this is a coroutine:: + + await sio.disconnect() + +Debugging and Troubleshooting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To help you debug issues, the client can be configured to output logs to the +terminal:: + + import socketio + + # standard Python + sio = socketio.Client(logger=True, engineio_logger=True) + + # asyncio + sio = socketio.AsyncClient(logger=True, engineio_logger=True) + +The ``logger`` argument controls logging related to the Socket.IO protocol, +while ``engineio_logger`` controls logs that originate in the low-level +Engine.IO transport. These arguments can be set to ``True`` to output logs to +``stderr``, or to an object compatible with Python's ``logging`` package +where the logs should be emitted to. A value of ``False`` disables logging. + +Logging can help identify the cause of connection problems, unexpected +disconnections and other issues. + +Using the Event-Driven Client +----------------------------- + Creating a Client Instance --------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~ To instantiate an Socket.IO client, simply create an instance of the appropriate client class:: @@ -38,7 +205,7 @@ appropriate client class:: sio = socketio.AsyncClient() Defining Event Handlers ------------------------ +~~~~~~~~~~~~~~~~~~~~~~~ The Socket.IO protocol is event based. When a server wants to communicate with a client it *emits* an event. Each event has a name, and a list of @@ -69,7 +236,7 @@ If the server includes arguments with an event, those are passed to the handler function as arguments. Catch-All Event Handlers ------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~ A "catch-all" event handler is invoked for any events that do not have an event handler. You can define a catch-all handler using ``'*'`` as event name:: @@ -88,9 +255,9 @@ A catch-all event handler receives the event name as a first argument. The remaining arguments are the same as for a regular event handler. Connect, Connect Error and Disconnect Event Handlers ----------------------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``connect``, ``connect_error`` and ``disconnect`` events are special; they +The ``connect``, ``connect_error`` and ``disconnect`` events are special; they are invoked automatically when a client connects or disconnects from the server:: @@ -122,7 +289,7 @@ The ``connect``, ``connect_error`` and ``disconnect`` events have to be defined explicitly and are not invoked on a catch-all event handler. Connecting to a Server ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ The connection to a server is established by calling the ``connect()`` method:: @@ -138,8 +305,16 @@ The application can find this identifier in the ``sid`` attribute:: print('my sid is', sio.sid) +The Socket.IO transport that is used in the connection can be obtained from the +``transport`` attribute:: + + print('my transport is', sio.transport) + +The transport is given as a string, and can be either ``'websocket'`` or +``'polling'``. + TLS/SSL Support -~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^ The client supports TLS/SSL connections. To enable it, use a ``https://`` connection URL:: @@ -206,7 +381,7 @@ And for ``asyncio``:: await sio.connect('https://example.com') Emitting Events ---------------- +~~~~~~~~~~~~~~~ The client can emit an event to the server using the ``emit()`` method:: @@ -216,18 +391,19 @@ Or in the case of ``asyncio``, as a coroutine:: await sio.emit('my message', {'foo': 'bar'}) -The single argument provided to the method is the data that is passed on -to the server. The data can be of type ``str``, ``bytes``, ``dict``, -``list`` or ``tuple``. When sending a ``tuple``, the elements in it need to -be of any of the other four allowed types. The elements of the tuple will be -passed as multiple arguments to the server-side event handler function. +The arguments provided to the method are the name of the event to emit and the +optional data that is passed on to the server. The data can be of type ``str``, +``bytes``, ``dict``, ``list`` or ``tuple``. When sending a ``list`` or a +``tuple``, the elements in it need to be of any allowed types except ``tuple``. +When a tuple is used, the elements of the tuple will be passed as individual +arguments to the server-side event handler function. The ``emit()`` method can be invoked inside an event handler as a response to a server event, or in any other part of the application, including in background tasks. Event Callbacks ---------------- +~~~~~~~~~~~~~~~ When a server emits an event to a client, it can optionally provide a callback function, to be invoked as a way of acknowledgment that the server @@ -249,7 +425,7 @@ the event, and any values returned by the server handler will be passed as arguments to this function. Namespaces ----------- +~~~~~~~~~~ The Socket.IO protocol supports multiple logical connections, all multiplexed on the same physical connection. Clients can open multiple connections by @@ -281,7 +457,7 @@ If the ``namespaces`` argument of the ``connect()`` call isn't given, any namespaces used in event handlers are automatically connected. Class-Based Namespaces ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ As an alternative to the decorator-based event handlers, the event handlers that belong to a namespace can be created as methods of a subclass of @@ -332,7 +508,7 @@ decorator-based function handler, only the standalone function handler is invoked. Disconnecting from the Server ------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ At any time the client can request to be disconnected from the server by invoking the ``disconnect()`` method:: @@ -344,7 +520,7 @@ For the ``asyncio`` client this is a coroutine:: await sio.disconnect() Managing Background Tasks -------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~ When a client connection to the server is established, a few background tasks will be spawned to keep the connection alive and handle incoming @@ -398,7 +574,7 @@ The single argument passed to the method is the number of seconds to sleep for. Debugging and Troubleshooting ------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To help you debug issues, the client can be configured to output logs to the terminal:: diff --git a/docs/conf.py b/docs/conf.py index e8398c3d..237eb821 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,6 +42,8 @@ 'sphinx.ext.autodoc', ] +autodoc_member_order = 'bysource' + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/examples/README.rst b/examples/README.rst index 8651bba6..82b918e5 100644 --- a/examples/README.rst +++ b/examples/README.rst @@ -2,5 +2,5 @@ Socket.IO Examples ================== This directory contains several example Socket.IO applications. Look in the -`server` directory for Socket.IO servers, and in the `client` directory for -Socket.IO clients. \ No newline at end of file +`server` directory for Socket.IO servers, and in the `client` and +`simple-client` directories for Socket.IO clients. diff --git a/examples/client/README.rst b/examples/client/README.rst index 05346572..ce5dab04 100644 --- a/examples/client/README.rst +++ b/examples/client/README.rst @@ -4,13 +4,13 @@ Socket.IO Client Examples This directory contains several example Socket.IO client applications, organized by directory: -threads -------- +sync +---- Examples that use standard Python thread concurrency. -asyncio -------- +async +----- Examples that use Python's `asyncio` package for concurrency. diff --git a/examples/client/threads/README.rst b/examples/client/async/README.rst similarity index 57% rename from examples/client/threads/README.rst rename to examples/client/async/README.rst index 5333375e..57102139 100644 --- a/examples/client/threads/README.rst +++ b/examples/client/async/README.rst @@ -1,8 +1,8 @@ -Socket.IO Threading Examples -============================ +Socket.IO Async Client Examples +=============================== This directory contains example Socket.IO clients that work with the -`threading` package of the Python standard library. +``asyncio`` package of the Python standard library. latency_client.py ----------------- @@ -14,11 +14,19 @@ for each of these exchanges. This is an ideal application to measure the performance of the different asynchronous modes supported by the Socket.IO server. +fiddle_client.py +---------------- + +This is an extemely simple application based on the JavaScript example of the +same name. + Running the Examples -------------------- These examples work with the server examples of the same name. First run one -of the `latency.py` versions from the `examples/server/wsgi` directory. On -another terminal, then start the corresponding client:: +of the ``latency.py`` or ``fiddle.py`` versions from one of the +``examples/server`` subdirectories. On another terminal, then start the +corresponding client:: $ python latency_client.py + $ python fiddle_client.py diff --git a/examples/client/asyncio/fiddle_client.py b/examples/client/async/fiddle_client.py similarity index 100% rename from examples/client/asyncio/fiddle_client.py rename to examples/client/async/fiddle_client.py diff --git a/examples/client/asyncio/latency_client.py b/examples/client/async/latency_client.py similarity index 100% rename from examples/client/asyncio/latency_client.py rename to examples/client/async/latency_client.py diff --git a/examples/client/asyncio/README.rst b/examples/client/sync/README.rst similarity index 59% rename from examples/client/asyncio/README.rst rename to examples/client/sync/README.rst index c4ff8e2b..efc4d6f9 100644 --- a/examples/client/asyncio/README.rst +++ b/examples/client/sync/README.rst @@ -1,8 +1,8 @@ -Socket.IO Asyncio Examples -========================== +Socket.IO Client Examples +========================= This directory contains example Socket.IO clients that work with the -`asyncio` package of the Python standard library. +Python standard library. latency_client.py ----------------- @@ -14,11 +14,19 @@ for each of these exchanges. This is an ideal application to measure the performance of the different asynchronous modes supported by the Socket.IO server. +fiddle_client.py +---------------- + +This is an extemely simple application based on the JavaScript example of the +same name. + Running the Examples -------------------- These examples work with the server examples of the same name. First run one -of the `latency.py` versions from the `examples/server/wsgi` directory. On -another terminal, then start the corresponding client:: +of the ``latency.py`` or ``fiddle.py`` versions from one of the +``examples/server`` subdirectories. On another terminal, then start the +corresponding client:: $ python latency_client.py + $ python fiddle_client.py diff --git a/examples/client/threads/fiddle_client.py b/examples/client/sync/fiddle_client.py similarity index 100% rename from examples/client/threads/fiddle_client.py rename to examples/client/sync/fiddle_client.py diff --git a/examples/client/threads/latency_client.py b/examples/client/sync/latency_client.py similarity index 100% rename from examples/client/threads/latency_client.py rename to examples/client/sync/latency_client.py diff --git a/examples/simple-client/README.rst b/examples/simple-client/README.rst new file mode 100644 index 00000000..2f7c58ac --- /dev/null +++ b/examples/simple-client/README.rst @@ -0,0 +1,15 @@ +Socket.IO Simple Client Examples +================================ + +This directory contains several example Socket.IO client applications built +with the simplified client and organized by directory: + +sync +---- + +Examples that use standard Python. + +async +----- + +Examples that use Python's `asyncio` package. diff --git a/examples/simple-client/async/README.rst b/examples/simple-client/async/README.rst new file mode 100644 index 00000000..a5fe7c10 --- /dev/null +++ b/examples/simple-client/async/README.rst @@ -0,0 +1,33 @@ +Socket.IO Async Simple Client Examples +====================================== + +This directory contains example Socket.IO clients that work with the +`asyncio` package of the Python standard library, built with the simplified +client. + +latency_client.py +----------------- + +In this application the client sends *ping* messages to the server, which are +responded by the server with a *pong*. The client measures the time it takes +for each of these exchanges. + +This is an ideal application to measure the performance of the different +asynchronous modes supported by the Socket.IO server. + +fiddle_client.py +---------------- + +This is an extemely simple application based on the JavaScript example of the +same name. + +Running the Examples +-------------------- + +These examples work with the server examples of the same name. First run one +of the ``latency.py`` or ``fiddle.py`` versions from one of the +``examples/server`` subdirectories. On another terminal, then start the +corresponding client:: + + $ python latency_client.py + $ python fiddle_client.py diff --git a/examples/simple-client/async/fiddle_client.py b/examples/simple-client/async/fiddle_client.py new file mode 100644 index 00000000..305e71f6 --- /dev/null +++ b/examples/simple-client/async/fiddle_client.py @@ -0,0 +1,13 @@ +import asyncio +import socketio + + +async def main(): + sio = socketio.AsyncSimpleClient() + await sio.connect('http://localhost:5000', auth={'token': 'my-token'}) + print(await sio.receive()) + await sio.disconnect() + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/examples/simple-client/async/latency_client.py b/examples/simple-client/async/latency_client.py new file mode 100644 index 00000000..1139cccd --- /dev/null +++ b/examples/simple-client/async/latency_client.py @@ -0,0 +1,25 @@ +import asyncio +import time +import socketio + + +async def main(): + sio = socketio.AsyncSimpleClient() + await sio.connect('http://localhost:5000') + + try: + while True: + start_timer = time.time() + await sio.emit('ping_from_client') + while (await sio.receive()) != ['pong_from_server']: + pass + latency = time.time() - start_timer + print('latency is {0:.2f} ms'.format(latency * 1000)) + + await asyncio.sleep(1) + except (KeyboardInterrupt, asyncio.CancelledError): + await sio.disconnect() + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/examples/simple-client/sync/README.rst b/examples/simple-client/sync/README.rst new file mode 100644 index 00000000..d3f4f55d --- /dev/null +++ b/examples/simple-client/sync/README.rst @@ -0,0 +1,32 @@ +Socket.IO Simple Client Examples +================================ + +This directory contains example Socket.IO clients that are built using the +simplified client. + +latency_client.py +----------------- + +In this application the client sends *ping* messages to the server, which are +responded by the server with a *pong*. The client measures the time it takes +for each of these exchanges. + +This is an ideal application to measure the performance of the different +asynchronous modes supported by the Socket.IO server. + +fiddle_client.py +---------------- + +This is an extemely simple application based on the JavaScript example of the +same name. + +Running the Examples +-------------------- + +These examples work with the server examples of the same name. First run one +of the ``latency.py`` or ``fiddle.py`` versions from one of the +``examples/server`` subdirectories. On another terminal, then start the +corresponding client:: + + $ python latency_client.py + $ python fiddle_client.py diff --git a/examples/simple-client/sync/fiddle_client.py b/examples/simple-client/sync/fiddle_client.py new file mode 100644 index 00000000..2f79e97c --- /dev/null +++ b/examples/simple-client/sync/fiddle_client.py @@ -0,0 +1,12 @@ +import socketio + + +def main(): + sio = socketio.SimpleClient() + sio.connect('http://localhost:5000', auth={'token': 'my-token'}) + print(sio.receive()) + sio.disconnect() + + +if __name__ == '__main__': + main() diff --git a/examples/simple-client/sync/latency_client.py b/examples/simple-client/sync/latency_client.py new file mode 100644 index 00000000..2bf76577 --- /dev/null +++ b/examples/simple-client/sync/latency_client.py @@ -0,0 +1,24 @@ +import time +import socketio + + +def main(): + sio = socketio.SimpleClient() + sio.connect('http://localhost:5000') + + try: + while True: + start_timer = time.time() + sio.emit('ping_from_client') + while sio.receive() != ['pong_from_server']: + pass + latency = time.time() - start_timer + print('latency is {0:.2f} ms'.format(latency * 1000)) + + time.sleep(1) + except KeyboardInterrupt: + sio.disconnect() + + +if __name__ == '__main__': + main() diff --git a/src/socketio/__init__.py b/src/socketio/__init__.py index ad0a1c18..f5c851c9 100644 --- a/src/socketio/__init__.py +++ b/src/socketio/__init__.py @@ -1,6 +1,7 @@ import sys from .client import Client +from .simple_client import SimpleClient from .base_manager import BaseManager from .pubsub_manager import PubSubManager from .kombu_manager import KombuManager @@ -13,6 +14,7 @@ from .tornado import get_tornado_handler if sys.version_info >= (3, 5): # pragma: no cover from .asyncio_client import AsyncClient + from .asyncio_simple_client import AsyncSimpleClient from .asyncio_server import AsyncServer from .asyncio_manager import AsyncManager from .asyncio_namespace import AsyncNamespace, AsyncClientNamespace @@ -20,6 +22,7 @@ from .asyncio_aiopika_manager import AsyncAioPikaManager from .asgi import ASGIApp else: # pragma: no cover + AsyncSimpleClient = None AsyncClient = None AsyncServer = None AsyncManager = None @@ -27,10 +30,11 @@ AsyncRedisManager = None AsyncAioPikaManager = None -__all__ = ['Client', 'Server', 'BaseManager', 'PubSubManager', +__all__ = ['SimpleClient', 'Client', 'Server', 'BaseManager', 'PubSubManager', 'KombuManager', 'RedisManager', 'ZmqManager', 'KafkaManager', 'Namespace', 'ClientNamespace', 'WSGIApp', 'Middleware'] if AsyncServer is not None: # pragma: no cover - __all__ += ['AsyncClient', 'AsyncServer', 'AsyncNamespace', - 'AsyncClientNamespace', 'AsyncManager', 'AsyncRedisManager', - 'ASGIApp', 'get_tornado_handler', 'AsyncAioPikaManager'] + __all__ += ['AsyncSimpleClient', 'AsyncClient', 'AsyncServer', + 'AsyncNamespace', 'AsyncClientNamespace', 'AsyncManager', + 'AsyncRedisManager', 'ASGIApp', 'get_tornado_handler', + 'AsyncAioPikaManager'] diff --git a/src/socketio/asyncio_client.py b/src/socketio/asyncio_client.py index e68e270f..656b33e0 100644 --- a/src/socketio/asyncio_client.py +++ b/src/socketio/asyncio_client.py @@ -143,9 +143,10 @@ async def connect(self, url, headers={}, auth=None, transports=None, transports=transports, engineio_path=socketio_path) except engineio.exceptions.ConnectionError as exc: - await self._trigger_event( - 'connect_error', '/', - exc.args[1] if len(exc.args) > 1 else exc.args[0]) + for n in self.connection_namespaces: + await self._trigger_event( + 'connect_error', n, + exc.args[1] if len(exc.args) > 1 else exc.args[0]) raise exceptions.ConnectionError(exc.args[0]) from None if wait: @@ -271,7 +272,7 @@ async def call(self, event, data=None, namespace=None, timeout=60): argument is omitted the event is emitted to the default namespace. :param timeout: The waiting timeout. If the timeout is reached before - the client acknowledges the event, then a + the server acknowledges the event, then a ``TimeoutError`` exception is raised. Note: this method is not designed to be used concurrently. If multiple @@ -369,6 +370,7 @@ async def _handle_disconnect(self, namespace): return namespace = namespace or '/' await self._trigger_event('disconnect', namespace=namespace) + await self._trigger_event('__disconnect_final', namespace=namespace) if namespace in self.namespaces: del self.namespaces[namespace] if not self.namespaces: @@ -469,6 +471,9 @@ async def _handle_reconnect(self): try: await asyncio.wait_for(self._reconnect_abort.wait(), delay) self.logger.info('Reconnect task aborted') + for n in self.connection_namespaces: + await self._trigger_event('__disconnect_final', + namespace=n) break except (asyncio.TimeoutError, asyncio.CancelledError): pass @@ -490,6 +495,9 @@ async def _handle_reconnect(self): attempt_count >= self.reconnection_attempts: self.logger.info( 'Maximum reconnection attempts reached, giving up') + for n in self.connection_namespaces: + await self._trigger_event('__disconnect_final', + namespace=n) break client.reconnecting_clients.remove(self) @@ -533,15 +541,19 @@ async def _handle_eio_message(self, data): async def _handle_eio_disconnect(self): """Handle the Engine.IO disconnection event.""" self.logger.info('Engine.IO connection dropped') + will_reconnect = self.reconnection and self.eio.state == 'connected' if self.connected: for n in self.namespaces: await self._trigger_event('disconnect', namespace=n) + if not will_reconnect: + await self._trigger_event('__disconnect_final', + namespace=n) self.namespaces = {} self.connected = False self.callbacks = {} self._binary_packet = None self.sid = None - if self.eio.state == 'connected' and self.reconnection: + if will_reconnect: self._reconnect_task = self.start_background_task( self._handle_reconnect) diff --git a/src/socketio/asyncio_simple_client.py b/src/socketio/asyncio_simple_client.py new file mode 100644 index 00000000..f0066efa --- /dev/null +++ b/src/socketio/asyncio_simple_client.py @@ -0,0 +1,193 @@ +import asyncio +from socketio import AsyncClient +from socketio.exceptions import SocketIOError, TimeoutError, DisconnectedError + + +class AsyncSimpleClient: + """A Socket.IO client. + + This class implements a simple, yet fully compliant Socket.IO web client + with support for websocket and long-polling transports. + + Th positional and keyword arguments given in the constructor are passed + to the underlying :func:`socketio.AsyncClient` object. + """ + def __init__(self, *args, **kwargs): + self.client_args = args + self.client_kwargs = kwargs + self.client = None + self.namespace = '/' + self.connected_event = asyncio.Event() + self.connected = False + self.input_event = asyncio.Event() + self.input_buffer = [] + + async def connect(self, url, headers={}, auth=None, transports=None, + namespace='/', socketio_path='socket.io'): + """Connect to a Socket.IO server. + + :param url: The URL of the Socket.IO server. It can include custom + query string parameters if required by the server. If a + function is provided, the client will invoke it to obtain + the URL each time a connection or reconnection is + attempted. + :param headers: A dictionary with custom headers to send with the + connection request. If a function is provided, the + client will invoke it to obtain the headers dictionary + each time a connection or reconnection is attempted. + :param auth: Authentication data passed to the server with the + connection request, normally a dictionary with one or + more string key/value pairs. If a function is provided, + the client will invoke it to obtain the authentication + data each time a connection or reconnection is attempted. + :param transports: The list of allowed transports. Valid transports + are ``'polling'`` and ``'websocket'``. If not + given, the polling transport is connected first, + then an upgrade to websocket is attempted. + :param namespace: The namespace to connect to as a string. If not + given, the default namespace ``/`` is used. + :param socketio_path: The endpoint where the Socket.IO server is + installed. The default value is appropriate for + most cases. + + Note: this method is a coroutine. + """ + if self.connected: + raise RuntimeError('Already connected') + self.namespace = namespace + self.input_buffer = [] + self.input_event.clear() + self.client = AsyncClient(*self.client_args, **self.client_kwargs) + + @self.client.event + def connect(): # pragma: no cover + self.connected = True + self.connected_event.set() + + @self.client.event + def disconnect(): # pragma: no cover + self.connected_event.clear() + + @self.client.event + def __disconnect_final(): # pragma: no cover + self.connected = False + self.connected_event.set() + + @self.client.on('*') + def on_event(event, *args): # pragma: no cover + self.input_buffer.append([event, *args]) + self.input_event.set() + + await self.client.connect( + url, headers=headers, auth=auth, transports=transports, + namespaces=[namespace], socketio_path=socketio_path) + + @property + def sid(self): + """The session ID received from the server. + + The session ID is not guaranteed to remain constant throughout the life + of the connection, as reconnections can cause it to change. + """ + return self.client.sid if self.client else None + + @property + def transport(self): + """The name of the transport currently in use. + + The transport is returned as a string and can be one of ``polling`` + and ``websocket``. + """ + return self.client.transport if self.client else '' + + async def emit(self, event, data=None): + """Emit an event to the server. + + :param event: The event name. It can be any string. The event names + ``'connect'``, ``'message'`` and ``'disconnect'`` are + reserved and should not be used. + :param data: The data to send to the server. Data can be of + type ``str``, ``bytes``, ``list`` or ``dict``. To send + multiple arguments, use a tuple where each element is of + one of the types indicated above. + + Note: this method is a coroutine. + + This method schedules the event to be sent out and returns, without + actually waiting for its delivery. In cases where the client needs to + ensure that the event was received, :func:`socketio.SimpleClient.call` + should be used instead. + """ + while True: + await self.connected_event.wait() + if not self.connected: + raise DisconnectedError() + try: + return await self.client.emit(event, data, + namespace=self.namespace) + except SocketIOError: + pass + + async def call(self, event, data=None, timeout=60): + """Emit an event to the server and wait for a response. + + This method issues an emit and waits for the server to provide a + response or acknowledgement. If the response does not arrive before the + timeout, then a ``TimeoutError`` exception is raised. + + :param event: The event name. It can be any string. The event names + ``'connect'``, ``'message'`` and ``'disconnect'`` are + reserved and should not be used. + :param data: The data to send to the server. Data can be of + type ``str``, ``bytes``, ``list`` or ``dict``. To send + multiple arguments, use a tuple where each element is of + one of the types indicated above. + :param timeout: The waiting timeout. If the timeout is reached before + the server acknowledges the event, then a + ``TimeoutError`` exception is raised. + + Note: this method is a coroutine. + """ + while True: + await self.connected_event.wait() + if not self.connected: + raise DisconnectedError() + try: + return await self.client.call(event, data, + namespace=self.namespace, + timeout=timeout) + except SocketIOError: + pass + + async def receive(self, timeout=None): + """Wait for an event from the server. + + :param timeout: The waiting timeout. If the timeout is reached before + the server acknowledges the event, then a + ``TimeoutError`` exception is raised. + + Note: this method is a coroutine. + + The return value is a list with the event name as the first element. If + the server included arguments with the event, they are returned as + additional list elements. + """ + if not self.input_buffer: + await self.connected_event.wait() + if not self.connected: + raise DisconnectedError() + try: + await asyncio.wait_for(self.input_event.wait(), + timeout=timeout) + except asyncio.TimeoutError: + raise TimeoutError() + self.input_event.clear() + return self.input_buffer.pop(0) + + async def disconnect(self): + """Disconnect from the server. + + Note: this method is a coroutine. +i """ + await self.client.disconnect() + self.client = None diff --git a/src/socketio/client.py b/src/socketio/client.py index 2f645334..e6293f2b 100644 --- a/src/socketio/client.py +++ b/src/socketio/client.py @@ -92,7 +92,8 @@ class Client(object): fatal errors are logged even when ``engineio_logger`` is ``False``. """ - reserved_events = ['connect', 'connect_error', 'disconnect'] + reserved_events = ['connect', 'connect_error', 'disconnect', + '__disconnect_final'] def __init__(self, reconnection=True, reconnection_attempts=0, reconnection_delay=1, reconnection_delay_max=5, @@ -332,9 +333,10 @@ def connect(self, url, headers={}, auth=None, transports=None, transports=transports, engineio_path=socketio_path) except engineio.exceptions.ConnectionError as exc: - self._trigger_event( - 'connect_error', '/', - exc.args[1] if len(exc.args) > 1 else exc.args[0]) + for n in self.connection_namespaces: + self._trigger_event( + 'connect_error', n, + exc.args[1] if len(exc.args) > 1 else exc.args[0]) raise exceptions.ConnectionError(exc.args[0]) from None if wait: @@ -449,7 +451,7 @@ def call(self, event, data=None, namespace=None, timeout=60): argument is omitted the event is emitted to the default namespace. :param timeout: The waiting timeout. If the timeout is reached before - the client acknowledges the event, then a + the server acknowledges the event, then a ``TimeoutError`` exception is raised. Note: this method is not thread safe. If multiple threads are emitting @@ -569,6 +571,7 @@ def _handle_disconnect(self, namespace): return namespace = namespace or '/' self._trigger_event('disconnect', namespace=namespace) + self._trigger_event('__disconnect_final', namespace=namespace) if namespace in self.namespaces: del self.namespaces[namespace] if not self.namespaces: @@ -654,6 +657,8 @@ def _handle_reconnect(self): delay)) if self._reconnect_abort.wait(delay): self.logger.info('Reconnect task aborted') + for n in self.connection_namespaces: + self._trigger_event('__disconnect_final', namespace=n) break attempt_count += 1 try: @@ -673,6 +678,8 @@ def _handle_reconnect(self): attempt_count >= self.reconnection_attempts: self.logger.info( 'Maximum reconnection attempts reached, giving up') + for n in self.connection_namespaces: + self._trigger_event('__disconnect_final', namespace=n) break reconnecting_clients.remove(self) @@ -716,15 +723,18 @@ def _handle_eio_message(self, data): def _handle_eio_disconnect(self): """Handle the Engine.IO disconnection event.""" self.logger.info('Engine.IO connection dropped') + will_reconnect = self.reconnection and self.eio.state == 'connected' if self.connected: for n in self.namespaces: self._trigger_event('disconnect', namespace=n) + if not will_reconnect: + self._trigger_event('__disconnect_final', namespace=n) self.namespaces = {} self.connected = False self.callbacks = {} self._binary_packet = None self.sid = None - if self.eio.state == 'connected' and self.reconnection: + if will_reconnect: self._reconnect_task = self.start_background_task( self._handle_reconnect) diff --git a/src/socketio/exceptions.py b/src/socketio/exceptions.py index d9dae4a5..19d6e39e 100644 --- a/src/socketio/exceptions.py +++ b/src/socketio/exceptions.py @@ -32,3 +32,7 @@ class TimeoutError(SocketIOError): class BadNamespaceError(SocketIOError): pass + + +class DisconnectedError(SocketIOError): + pass diff --git a/src/socketio/simple_client.py b/src/socketio/simple_client.py new file mode 100644 index 00000000..9a58cba1 --- /dev/null +++ b/src/socketio/simple_client.py @@ -0,0 +1,177 @@ +from threading import Event +from socketio import Client +from socketio.exceptions import SocketIOError, TimeoutError, DisconnectedError + + +class SimpleClient: + """A Socket.IO client. + + This class implements a simple, yet fully compliant Socket.IO web client + with support for websocket and long-polling transports. + + Th positional and keyword arguments given in the constructor are passed + to the underlying :func:`socketio.Client` object. + """ + def __init__(self, *args, **kwargs): + self.client_args = args + self.client_kwargs = kwargs + self.client = None + self.namespace = '/' + self.connected_event = Event() + self.connected = False + self.input_event = Event() + self.input_buffer = [] + + def connect(self, url, headers={}, auth=None, transports=None, + namespace='/', socketio_path='socket.io'): + """Connect to a Socket.IO server. + + :param url: The URL of the Socket.IO server. It can include custom + query string parameters if required by the server. If a + function is provided, the client will invoke it to obtain + the URL each time a connection or reconnection is + attempted. + :param headers: A dictionary with custom headers to send with the + connection request. If a function is provided, the + client will invoke it to obtain the headers dictionary + each time a connection or reconnection is attempted. + :param auth: Authentication data passed to the server with the + connection request, normally a dictionary with one or + more string key/value pairs. If a function is provided, + the client will invoke it to obtain the authentication + data each time a connection or reconnection is attempted. + :param transports: The list of allowed transports. Valid transports + are ``'polling'`` and ``'websocket'``. If not + given, the polling transport is connected first, + then an upgrade to websocket is attempted. + :param namespace: The namespace to connect to as a string. If not + given, the default namespace ``/`` is used. + :param socketio_path: The endpoint where the Socket.IO server is + installed. The default value is appropriate for + most cases. + """ + if self.connected: + raise RuntimeError('Already connected') + self.namespace = namespace + self.input_buffer = [] + self.input_event.clear() + self.client = Client(*self.client_args, **self.client_kwargs) + + @self.client.event + def connect(): # pragma: no cover + self.connected = True + self.connected_event.set() + + @self.client.event + def disconnect(): # pragma: no cover + self.connected_event.clear() + + @self.client.event + def __disconnect_final(): # pragma: no cover + self.connected = False + self.connected_event.set() + + @self.client.on('*') + def on_event(event, *args): # pragma: no cover + self.input_buffer.append([event, *args]) + self.input_event.set() + + self.client.connect(url, headers=headers, auth=auth, + transports=transports, namespaces=[namespace], + socketio_path=socketio_path) + + @property + def sid(self): + """The session ID received from the server. + + The session ID is not guaranteed to remain constant throughout the life + of the connection, as reconnections can cause it to change. + """ + return self.client.sid if self.client else None + + @property + def transport(self): + """The name of the transport currently in use. + + The transport is returned as a string and can be one of ``polling`` + and ``websocket``. + """ + return self.client.transport if self.client else '' + + def emit(self, event, data=None): + """Emit an event to the server. + + :param event: The event name. It can be any string. The event names + ``'connect'``, ``'message'`` and ``'disconnect'`` are + reserved and should not be used. + :param data: The data to send to the server. Data can be of + type ``str``, ``bytes``, ``list`` or ``dict``. To send + multiple arguments, use a tuple where each element is of + one of the types indicated above. + + This method schedules the event to be sent out and returns, without + actually waiting for its delivery. In cases where the client needs to + ensure that the event was received, :func:`socketio.SimpleClient.call` + should be used instead. + """ + while True: + self.connected_event.wait() + if not self.connected: + raise DisconnectedError() + try: + return self.client.emit(event, data, namespace=self.namespace) + except SocketIOError: + pass + + def call(self, event, data=None, timeout=60): + """Emit an event to the server and wait for a response. + + This method issues an emit and waits for the server to provide a + response or acknowledgement. If the response does not arrive before the + timeout, then a ``TimeoutError`` exception is raised. + + :param event: The event name. It can be any string. The event names + ``'connect'``, ``'message'`` and ``'disconnect'`` are + reserved and should not be used. + :param data: The data to send to the server. Data can be of + type ``str``, ``bytes``, ``list`` or ``dict``. To send + multiple arguments, use a tuple where each element is of + one of the types indicated above. + :param timeout: The waiting timeout. If the timeout is reached before + the server acknowledges the event, then a + ``TimeoutError`` exception is raised. + """ + while True: + self.connected_event.wait() + if not self.connected: + raise DisconnectedError() + try: + return self.client.call(event, data, namespace=self.namespace, + timeout=timeout) + except SocketIOError: + pass + + def receive(self, timeout=None): + """Wait for an event from the server. + + :param timeout: The waiting timeout. If the timeout is reached before + the server acknowledges the event, then a + ``TimeoutError`` exception is raised. + + The return value is a list with the event name as the first element. If + the server included arguments with the event, they are returned as + additional list elements. + """ + if not self.input_buffer: + self.connected_event.wait() + if not self.connected: + raise DisconnectedError() + if not self.input_event.wait(timeout=timeout): + raise TimeoutError() + self.input_event.clear() + return self.input_buffer.pop(0) + + def disconnect(self): + """Disconnect from the server.""" + self.client.disconnect() + self.client = None diff --git a/tests/asyncio/test_asyncio_client.py b/tests/asyncio/test_asyncio_client.py index 96b998a0..25bdd795 100644 --- a/tests/asyncio/test_asyncio_client.py +++ b/tests/asyncio/test_asyncio_client.py @@ -1,5 +1,4 @@ import asyncio -from contextlib import contextmanager import sys import unittest from unittest import mock @@ -603,12 +602,15 @@ def test_handle_disconnect(self): c.connected = True c._trigger_event = AsyncMock() _run(c._handle_disconnect('/')) - c._trigger_event.mock.assert_called_once_with( + c._trigger_event.mock.assert_any_call( 'disconnect', namespace='/' ) + c._trigger_event.mock.assert_any_call( + '__disconnect_final', namespace='/' + ) assert not c.connected _run(c._handle_disconnect('/')) - assert c._trigger_event.mock.call_count == 1 + assert c._trigger_event.mock.call_count == 2 def test_handle_disconnect_namespace(self): c = asyncio_client.AsyncClient() @@ -616,11 +618,23 @@ def test_handle_disconnect_namespace(self): c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = AsyncMock() _run(c._handle_disconnect('/foo')) - c._trigger_event.mock.assert_called_once_with( + c._trigger_event.mock.assert_any_call( 'disconnect', namespace='/foo' ) + c._trigger_event.mock.assert_any_call( + '__disconnect_final', namespace='/foo' + ) assert c.namespaces == {'/bar': '2'} assert c.connected + _run(c._handle_disconnect('/bar')) + c._trigger_event.mock.assert_any_call( + 'disconnect', namespace='/bar' + ) + c._trigger_event.mock.assert_any_call( + '__disconnect_final', namespace='/bar' + ) + assert c.namespaces == {} + assert not c.connected def test_handle_disconnect_unknown_namespace(self): c = asyncio_client.AsyncClient() @@ -628,9 +642,12 @@ def test_handle_disconnect_unknown_namespace(self): c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = AsyncMock() _run(c._handle_disconnect('/baz')) - c._trigger_event.mock.assert_called_once_with( + c._trigger_event.mock.assert_any_call( 'disconnect', namespace='/baz' ) + c._trigger_event.mock.assert_any_call( + '__disconnect_final', namespace='/baz' + ) assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected @@ -640,7 +657,9 @@ def test_handle_disconnect_default_namespaces(self): c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = AsyncMock() _run(c._handle_disconnect('/')) - c._trigger_event.mock.assert_called_with('disconnect', namespace='/') + c._trigger_event.mock.assert_any_call('disconnect', namespace='/') + c._trigger_event.mock.assert_any_call('__disconnect_final', + namespace='/') assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected @@ -901,7 +920,9 @@ def test_handle_reconnect_max_delay(self, random, wait_for): @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) def test_handle_reconnect_max_attempts(self, random, wait_for): c = asyncio_client.AsyncClient(reconnection_attempts=2, logger=True) + c.connection_namespaces = ['/'] c._reconnect_task = 'foo' + c._trigger_event = AsyncMock() c.connect = AsyncMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) @@ -912,6 +933,8 @@ def test_handle_reconnect_max_attempts(self, random, wait_for): 1.5, ] assert c._reconnect_task == 'foo' + c._trigger_event.mock.assert_called_once_with('__disconnect_final', + namespace='/') @mock.patch( 'asyncio.wait_for', @@ -921,7 +944,9 @@ def test_handle_reconnect_max_attempts(self, random, wait_for): @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) def test_handle_reconnect_aborted(self, random, wait_for): c = asyncio_client.AsyncClient(logger=True) + c.connection_namespaces = ['/'] c._reconnect_task = 'foo' + c._trigger_event = AsyncMock() c.connect = AsyncMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) @@ -932,6 +957,8 @@ def test_handle_reconnect_aborted(self, random, wait_for): 1.5, ] assert c._reconnect_task == 'foo' + c._trigger_event.mock.assert_called_once_with('__disconnect_final', + namespace='/') def test_handle_eio_connect(self): c = asyncio_client.AsyncClient() @@ -1029,10 +1056,11 @@ def test_handle_eio_message(self): _run(c._handle_eio_message('9')) def test_eio_disconnect(self): - c = asyncio_client.AsyncClient(reconnection=False) + c = asyncio_client.AsyncClient() c.namespaces = {'/': '1'} c.connected = True c._trigger_event = AsyncMock() + c.start_background_task = mock.MagicMock() c.sid = 'foo' c.eio.state = 'connected' _run(c._handle_eio_disconnect()) @@ -1071,7 +1099,19 @@ def test_eio_disconnect_self_disconnect(self): def test_eio_disconnect_no_reconnect(self): c = asyncio_client.AsyncClient(reconnection=False) + c.namespaces = {'/': '1'} + c.connected = True + c._trigger_event = AsyncMock() c.start_background_task = mock.MagicMock() + c.sid = 'foo' c.eio.state = 'connected' _run(c._handle_eio_disconnect()) + c._trigger_event.mock.assert_any_call( + 'disconnect', namespace='/' + ) + c._trigger_event.mock.assert_any_call( + '__disconnect_final', namespace='/' + ) + assert c.sid is None + assert not c.connected c.start_background_task.assert_not_called() diff --git a/tests/asyncio/test_asyncio_manager.py b/tests/asyncio/test_asyncio_manager.py index 2d2768ad..c4247234 100644 --- a/tests/asyncio/test_asyncio_manager.py +++ b/tests/asyncio/test_asyncio_manager.py @@ -1,4 +1,3 @@ -import asyncio import sys import unittest from unittest import mock diff --git a/tests/asyncio/test_asyncio_namespace.py b/tests/asyncio/test_asyncio_namespace.py index b7f57313..0d9e6ce7 100644 --- a/tests/asyncio/test_asyncio_namespace.py +++ b/tests/asyncio/test_asyncio_namespace.py @@ -1,4 +1,3 @@ -import asyncio import sys import unittest from unittest import mock diff --git a/tests/asyncio/test_asyncio_simple_client.py b/tests/asyncio/test_asyncio_simple_client.py new file mode 100644 index 00000000..9188c4fe --- /dev/null +++ b/tests/asyncio/test_asyncio_simple_client.py @@ -0,0 +1,163 @@ +import asyncio +import unittest +from unittest import mock +import pytest +from socketio import AsyncSimpleClient +from socketio.exceptions import SocketIOError, TimeoutError, DisconnectedError +from .helpers import AsyncMock, _run + + +class TestAsyncAsyncSimpleClient(unittest.TestCase): + def test_constructor(self): + client = AsyncSimpleClient(1, '2', a='3', b=4) + assert client.client_args == (1, '2') + assert client.client_kwargs == {'a': '3', 'b': 4} + assert client.client is None + assert client.input_buffer == [] + assert not client.connected + + def test_connect(self): + client = AsyncSimpleClient(123, a='b') + with mock.patch('socketio.asyncio_simple_client.AsyncClient') \ + as mock_client: + mock_client.return_value.connect = AsyncMock() + + _run(client.connect('url', headers='h', auth='a', transports='t', + namespace='n', socketio_path='s')) + mock_client.assert_called_once_with(123, a='b') + assert client.client == mock_client() + mock_client().connect.mock.assert_called_once_with( + 'url', headers='h', auth='a', transports='t', + namespaces=['n'], socketio_path='s') + mock_client().event.call_count == 3 + mock_client().on.called_once_with('*') + assert client.namespace == 'n' + assert not client.input_event.is_set() + + def test_connect_twice(self): + client = AsyncSimpleClient(123, a='b') + client.client = mock.MagicMock() + client.connected = True + + with pytest.raises(RuntimeError): + _run(client.connect('url')) + + def test_properties(self): + client = AsyncSimpleClient() + client.client = mock.MagicMock(sid='sid', transport='websocket') + client.connected_event.set() + client.connected = True + + assert client.sid == 'sid' + assert client.transport == 'websocket' + + def test_emit(self): + client = AsyncSimpleClient() + client.client = mock.MagicMock() + client.client.emit = AsyncMock() + client.namespace = '/ns' + client.connected_event.set() + client.connected = True + + _run(client.emit('foo', 'bar')) + assert client.client.emit.mock.called_once_with('foo', 'bar', + namespace='/ns') + + def test_emit_disconnected(self): + client = AsyncSimpleClient() + client.connected_event.set() + client.connected = False + with pytest.raises(DisconnectedError): + _run(client.emit('foo', 'bar')) + + def test_emit_retries(self): + client = AsyncSimpleClient() + client.connected_event.set() + client.connected = True + client.client = mock.MagicMock() + client.client.emit = AsyncMock() + client.client.emit.mock.side_effect = [SocketIOError(), None] + + _run(client.emit('foo', 'bar')) + client.client.emit.mock.assert_called_with('foo', 'bar', namespace='/') + + def test_call(self): + client = AsyncSimpleClient() + client.client = mock.MagicMock() + client.client.call = AsyncMock() + client.client.call.mock.return_value = 'result' + client.namespace = '/ns' + client.connected_event.set() + client.connected = True + + assert _run(client.call('foo', 'bar')) == 'result' + assert client.client.call.mock.called_once_with('foo', 'bar', + namespace='/ns', + timeout=60) + + def test_call_disconnected(self): + client = AsyncSimpleClient() + client.connected_event.set() + client.connected = False + with pytest.raises(DisconnectedError): + _run(client.call('foo', 'bar')) + + def test_call_retries(self): + client = AsyncSimpleClient() + client.connected_event.set() + client.connected = True + client.client = mock.MagicMock() + client.client.call = AsyncMock() + client.client.call.mock.side_effect = [SocketIOError(), 'result'] + + assert _run(client.call('foo', 'bar')) == 'result' + client.client.call.mock.assert_called_with('foo', 'bar', namespace='/', + timeout=60) + + def test_receive_with_input_buffer(self): + client = AsyncSimpleClient() + client.input_buffer = ['foo', 'bar'] + assert _run(client.receive()) == 'foo' + assert _run(client.receive()) == 'bar' + + def test_receive_without_input_buffer(self): + client = AsyncSimpleClient() + client.connected_event.set() + client.connected = True + client.input_event = mock.MagicMock() + + async def fake_wait(timeout=None): + client.input_buffer = ['foo'] + return True + + client.input_event.wait = fake_wait + assert _run(client.receive()) == 'foo' + + def test_receive_with_timeout(self): + client = AsyncSimpleClient() + client.connected_event.set() + client.connected = True + client.input_event = mock.MagicMock() + + async def fake_wait(timeout=None): + await asyncio.sleep(1) + + client.input_event.wait = fake_wait + with pytest.raises(TimeoutError): + _run(client.receive(timeout=0.01)) + + def test_receive_disconnected(self): + client = AsyncSimpleClient() + client.connected_event.set() + client.connected = False + with pytest.raises(DisconnectedError): + _run(client.receive()) + + def test_disconnect(self): + client = AsyncSimpleClient() + mc = mock.MagicMock() + mc.disconnect = AsyncMock() + client.client = mc + _run(client.disconnect()) + mc.disconnect.mock.assert_called_once_with() + assert client.client is None diff --git a/tests/common/test_client.py b/tests/common/test_client.py index a9415efc..78dd1d80 100644 --- a/tests/common/test_client.py +++ b/tests/common/test_client.py @@ -752,10 +752,11 @@ def test_handle_disconnect(self): c.connected = True c._trigger_event = mock.MagicMock() c._handle_disconnect('/') - c._trigger_event.assert_called_once_with('disconnect', namespace='/') + c._trigger_event.assert_any_call('disconnect', namespace='/') + c._trigger_event.assert_any_call('__disconnect_final', namespace='/') assert not c.connected c._handle_disconnect('/') - assert c._trigger_event.call_count == 1 + assert c._trigger_event.call_count == 2 def test_handle_disconnect_namespace(self): c = client.Client() @@ -763,15 +764,21 @@ def test_handle_disconnect_namespace(self): c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_disconnect('/foo') - c._trigger_event.assert_called_once_with( + c._trigger_event.assert_any_call( 'disconnect', namespace='/foo' ) + c._trigger_event.assert_any_call( + '__disconnect_final', namespace='/foo' + ) assert c.namespaces == {'/bar': '2'} assert c.connected c._handle_disconnect('/bar') - c._trigger_event.assert_called_with( + c._trigger_event.assert_any_call( 'disconnect', namespace='/bar' ) + c._trigger_event.assert_any_call( + '__disconnect_final', namespace='/bar' + ) assert c.namespaces == {} assert not c.connected @@ -781,9 +788,12 @@ def test_handle_disconnect_unknown_namespace(self): c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_disconnect('/baz') - c._trigger_event.assert_called_once_with( + c._trigger_event.assert_any_call( 'disconnect', namespace='/baz' ) + c._trigger_event.assert_any_call( + '__disconnect_final', namespace='/baz' + ) assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected @@ -793,7 +803,9 @@ def test_handle_disconnect_default_namespace(self): c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_disconnect('/') - c._trigger_event.assert_called_with('disconnect', namespace='/') + print(c._trigger_event.call_args_list) + c._trigger_event.assert_any_call('disconnect', namespace='/') + c._trigger_event.assert_any_call('__disconnect_final', namespace='/') assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected @@ -1023,9 +1035,11 @@ def test_handle_reconnect_max_delay(self, random): @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) def test_handle_reconnect_max_attempts(self, random): c = client.Client(reconnection_attempts=2) + c.connection_namespaces = ['/'] c._reconnect_task = 'foo' c._reconnect_abort = c.eio.create_event() c._reconnect_abort.wait = mock.MagicMock(return_value=False) + c._trigger_event = mock.MagicMock() c.connect = mock.MagicMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) @@ -1036,13 +1050,17 @@ def test_handle_reconnect_max_attempts(self, random): mock.call(1.5), ] assert c._reconnect_task == 'foo' + c._trigger_event.assert_called_once_with('__disconnect_final', + namespace='/') @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) def test_handle_reconnect_aborted(self, random): c = client.Client() + c.connection_namespaces = ['/'] c._reconnect_task = 'foo' c._reconnect_abort = c.eio.create_event() c._reconnect_abort.wait = mock.MagicMock(side_effect=[False, True]) + c._trigger_event = mock.MagicMock() c.connect = mock.MagicMock(side_effect=exceptions.ConnectionError) c._handle_reconnect() assert c._reconnect_abort.wait.call_count == 2 @@ -1051,6 +1069,8 @@ def test_handle_reconnect_aborted(self, random): mock.call(1.5), ] assert c._reconnect_task == 'foo' + c._trigger_event.assert_called_once_with('__disconnect_final', + namespace='/') def test_handle_eio_connect(self): c = client.Client() @@ -1189,7 +1209,15 @@ def test_eio_disconnect_self_disconnect(self): def test_eio_disconnect_no_reconnect(self): c = client.Client(reconnection=False) + c.namespaces = {'/': '1'} + c.connected = True + c._trigger_event = mock.MagicMock() c.start_background_task = mock.MagicMock() + c.sid = 'foo' c.eio.state = 'connected' c._handle_eio_disconnect() + c._trigger_event.assert_any_call('disconnect', namespace='/') + c._trigger_event.assert_any_call('__disconnect_final', namespace='/') + assert c.sid is None + assert not c.connected c.start_background_task.assert_not_called() diff --git a/tests/common/test_simple_client.py b/tests/common/test_simple_client.py new file mode 100644 index 00000000..f445ff85 --- /dev/null +++ b/tests/common/test_simple_client.py @@ -0,0 +1,146 @@ +import unittest +from unittest import mock +import pytest +from socketio import SimpleClient +from socketio.exceptions import SocketIOError, TimeoutError, DisconnectedError + + +class TestSimpleClient(unittest.TestCase): + def test_constructor(self): + client = SimpleClient(1, '2', a='3', b=4) + assert client.client_args == (1, '2') + assert client.client_kwargs == {'a': '3', 'b': 4} + assert client.client is None + assert client.input_buffer == [] + assert not client.connected + + def test_connect(self): + client = SimpleClient(123, a='b') + with mock.patch('socketio.simple_client.Client') as mock_client: + client.connect('url', headers='h', auth='a', transports='t', + namespace='n', socketio_path='s') + mock_client.assert_called_once_with(123, a='b') + assert client.client == mock_client() + mock_client().connect.assert_called_once_with( + 'url', headers='h', auth='a', transports='t', + namespaces=['n'], socketio_path='s') + mock_client().event.call_count == 3 + mock_client().on.called_once_with('*') + assert client.namespace == 'n' + assert not client.input_event.is_set() + + def test_connect_twice(self): + client = SimpleClient(123, a='b') + client.client = mock.MagicMock() + client.connected = True + + with pytest.raises(RuntimeError): + client.connect('url') + + def test_properties(self): + client = SimpleClient() + client.client = mock.MagicMock(sid='sid', transport='websocket') + client.connected_event.set() + client.connected = True + + assert client.sid == 'sid' + assert client.transport == 'websocket' + + def test_emit(self): + client = SimpleClient() + client.client = mock.MagicMock() + client.namespace = '/ns' + client.connected_event.set() + client.connected = True + + client.emit('foo', 'bar') + assert client.client.emit.called_once_with('foo', 'bar', + namespace='/ns') + + def test_emit_disconnected(self): + client = SimpleClient() + client.connected_event.set() + client.connected = False + with pytest.raises(DisconnectedError): + client.emit('foo', 'bar') + + def test_emit_retries(self): + client = SimpleClient() + client.connected_event.set() + client.connected = True + client.client = mock.MagicMock() + client.client.emit.side_effect = [SocketIOError(), None] + + client.emit('foo', 'bar') + client.client.emit.assert_called_with('foo', 'bar', namespace='/') + + def test_call(self): + client = SimpleClient() + client.client = mock.MagicMock() + client.client.call.return_value = 'result' + client.namespace = '/ns' + client.connected_event.set() + client.connected = True + + assert client.call('foo', 'bar') == 'result' + client.client.call.called_once_with('foo', 'bar', namespace='/ns', + timeout=60) + + def test_call_disconnected(self): + client = SimpleClient() + client.connected_event.set() + client.connected = False + with pytest.raises(DisconnectedError): + client.call('foo', 'bar') + + def test_call_retries(self): + client = SimpleClient() + client.connected_event.set() + client.connected = True + client.client = mock.MagicMock() + client.client.call.side_effect = [SocketIOError(), 'result'] + + assert client.call('foo', 'bar') == 'result' + client.client.call.assert_called_with('foo', 'bar', namespace='/', + timeout=60) + + def test_receive_with_input_buffer(self): + client = SimpleClient() + client.input_buffer = ['foo', 'bar'] + assert client.receive() == 'foo' + assert client.receive() == 'bar' + + def test_receive_without_input_buffer(self): + client = SimpleClient() + client.connected_event.set() + client.connected = True + client.input_event = mock.MagicMock() + + def fake_wait(timeout=None): + client.input_buffer = ['foo'] + return True + + client.input_event.wait = fake_wait + assert client.receive() == 'foo' + + def test_receive_with_timeout(self): + client = SimpleClient() + client.connected_event.set() + client.connected = True + with pytest.raises(TimeoutError): + client.receive(timeout=0.01) + + def test_receive_disconnected(self): + client = SimpleClient() + client.connected_event.set() + client.connected = False + with pytest.raises(DisconnectedError): + client.receive() + + def test_disconnect(self): + client = SimpleClient() + mc = mock.MagicMock() + client.client = mc + client.disconnect() + mc.disconnect.assert_called_once_with() + assert client.client is None From 699ee9c47adcb44f1a8150d8c92a9555d07f7b5b Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 10 Sep 2023 17:22:09 +0100 Subject: [PATCH 388/571] Context manager interface for the simple clients --- docs/client.rst | 22 +++++++++++++-- examples/simple-client/async/fiddle_client.py | 7 ++--- .../simple-client/async/latency_client.py | 8 ++---- examples/simple-client/sync/fiddle_client.py | 7 ++--- examples/simple-client/sync/latency_client.py | 8 ++---- src/socketio/asyncio_simple_client.py | 28 +++++++++++++------ src/socketio/simple_client.py | 26 +++++++++++------ tests/asyncio/test_asyncio_simple_client.py | 24 ++++++++++++++++ tests/common/test_simple_client.py | 17 +++++++++++ 9 files changed, 109 insertions(+), 38 deletions(-) diff --git a/docs/client.rst b/docs/client.rst index 3344bd35..aea9aba3 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -35,8 +35,26 @@ the application. Creating a Client Instance ~~~~~~~~~~~~~~~~~~~~~~~~~~ -To instantiate a Socket.IO client, create an instance of the appropriate client -class:: +The easiest way to create a Socket.IO client is to use the context manager +interface:: + + import socketio + + # standard Python + with socketio.SimpleClient() as sio: + # ... connect to a server and use the client + # ... no need to manually disconnect! + + # asyncio + async with socketio.AsyncSimpleClient() as sio: + # ... connect to a server and use the client + # ... no need to manually disconnect! + + +With this usage the context manager will ensure that the client is properly +disconnected before exiting the ``with`` or ``async with`` block. + +If preferred, a client can be manually instantiated:: import socketio diff --git a/examples/simple-client/async/fiddle_client.py b/examples/simple-client/async/fiddle_client.py index 305e71f6..d9744803 100644 --- a/examples/simple-client/async/fiddle_client.py +++ b/examples/simple-client/async/fiddle_client.py @@ -3,10 +3,9 @@ async def main(): - sio = socketio.AsyncSimpleClient() - await sio.connect('http://localhost:5000', auth={'token': 'my-token'}) - print(await sio.receive()) - await sio.disconnect() + async with socketio.AsyncSimpleClient() as sio: + await sio.connect('http://localhost:5000', auth={'token': 'my-token'}) + print(await sio.receive()) if __name__ == '__main__': diff --git a/examples/simple-client/async/latency_client.py b/examples/simple-client/async/latency_client.py index 1139cccd..96387c65 100644 --- a/examples/simple-client/async/latency_client.py +++ b/examples/simple-client/async/latency_client.py @@ -4,10 +4,8 @@ async def main(): - sio = socketio.AsyncSimpleClient() - await sio.connect('http://localhost:5000') - - try: + async with socketio.AsyncSimpleClient() as sio: + await sio.connect('http://localhost:5000') while True: start_timer = time.time() await sio.emit('ping_from_client') @@ -17,8 +15,6 @@ async def main(): print('latency is {0:.2f} ms'.format(latency * 1000)) await asyncio.sleep(1) - except (KeyboardInterrupt, asyncio.CancelledError): - await sio.disconnect() if __name__ == '__main__': diff --git a/examples/simple-client/sync/fiddle_client.py b/examples/simple-client/sync/fiddle_client.py index 2f79e97c..1be759cb 100644 --- a/examples/simple-client/sync/fiddle_client.py +++ b/examples/simple-client/sync/fiddle_client.py @@ -2,10 +2,9 @@ def main(): - sio = socketio.SimpleClient() - sio.connect('http://localhost:5000', auth={'token': 'my-token'}) - print(sio.receive()) - sio.disconnect() + with socketio.SimpleClient() as sio: + sio.connect('http://localhost:5000', auth={'token': 'my-token'}) + print(sio.receive()) if __name__ == '__main__': diff --git a/examples/simple-client/sync/latency_client.py b/examples/simple-client/sync/latency_client.py index 2bf76577..d5cd853e 100644 --- a/examples/simple-client/sync/latency_client.py +++ b/examples/simple-client/sync/latency_client.py @@ -3,10 +3,8 @@ def main(): - sio = socketio.SimpleClient() - sio.connect('http://localhost:5000') - - try: + with socketio.SimpleClient() as sio: + sio.connect('http://localhost:5000') while True: start_timer = time.time() sio.emit('ping_from_client') @@ -16,8 +14,6 @@ def main(): print('latency is {0:.2f} ms'.format(latency * 1000)) time.sleep(1) - except KeyboardInterrupt: - sio.disconnect() if __name__ == '__main__': diff --git a/src/socketio/asyncio_simple_client.py b/src/socketio/asyncio_simple_client.py index f0066efa..68dce66f 100644 --- a/src/socketio/asyncio_simple_client.py +++ b/src/socketio/asyncio_simple_client.py @@ -59,21 +59,21 @@ async def connect(self, url, headers={}, auth=None, transports=None, self.input_event.clear() self.client = AsyncClient(*self.client_args, **self.client_kwargs) - @self.client.event + @self.client.event(namespace=self.namespace) def connect(): # pragma: no cover self.connected = True self.connected_event.set() - @self.client.event + @self.client.event(namespace=self.namespace) def disconnect(): # pragma: no cover self.connected_event.clear() - @self.client.event + @self.client.event(namespace=self.namespace) def __disconnect_final(): # pragma: no cover self.connected = False self.connected_event.set() - @self.client.on('*') + @self.client.on('*', namespace=self.namespace) def on_event(event, *args): # pragma: no cover self.input_buffer.append([event, *args]) self.input_event.set() @@ -172,8 +172,12 @@ async def receive(self, timeout=None): the server included arguments with the event, they are returned as additional list elements. """ - if not self.input_buffer: - await self.connected_event.wait() + while not self.input_buffer: + try: + await asyncio.wait_for(self.connected_event.wait(), + timeout=timeout) + except asyncio.TimeoutError: # pragma: no cover + raise TimeoutError() if not self.connected: raise DisconnectedError() try: @@ -189,5 +193,13 @@ async def disconnect(self): Note: this method is a coroutine. i """ - await self.client.disconnect() - self.client = None + if self.connected: + await self.client.disconnect() + self.client = None + self.connected = False + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.disconnect() diff --git a/src/socketio/simple_client.py b/src/socketio/simple_client.py index 9a58cba1..4a883806 100644 --- a/src/socketio/simple_client.py +++ b/src/socketio/simple_client.py @@ -57,21 +57,21 @@ def connect(self, url, headers={}, auth=None, transports=None, self.input_event.clear() self.client = Client(*self.client_args, **self.client_kwargs) - @self.client.event + @self.client.event(namespace=self.namespace) def connect(): # pragma: no cover self.connected = True self.connected_event.set() - @self.client.event + @self.client.event(namespace=self.namespace) def disconnect(): # pragma: no cover self.connected_event.clear() - @self.client.event + @self.client.event(namespace=self.namespace) def __disconnect_final(): # pragma: no cover self.connected = False self.connected_event.set() - @self.client.on('*') + @self.client.on('*', namespace=self.namespace) def on_event(event, *args): # pragma: no cover self.input_buffer.append([event, *args]) self.input_event.set() @@ -162,8 +162,10 @@ def receive(self, timeout=None): the server included arguments with the event, they are returned as additional list elements. """ - if not self.input_buffer: - self.connected_event.wait() + while not self.input_buffer: + if not self.connected_event.wait( + timeout=timeout): # pragma: no cover + raise TimeoutError() if not self.connected: raise DisconnectedError() if not self.input_event.wait(timeout=timeout): @@ -173,5 +175,13 @@ def receive(self, timeout=None): def disconnect(self): """Disconnect from the server.""" - self.client.disconnect() - self.client = None + if self.connected: + self.client.disconnect() + self.client = None + self.connected = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.disconnect() diff --git a/tests/asyncio/test_asyncio_simple_client.py b/tests/asyncio/test_asyncio_simple_client.py index 9188c4fe..6a935978 100644 --- a/tests/asyncio/test_asyncio_simple_client.py +++ b/tests/asyncio/test_asyncio_simple_client.py @@ -34,6 +34,28 @@ def test_connect(self): assert client.namespace == 'n' assert not client.input_event.is_set() + def test_connect_context_manager(self): + async def _t(): + async with AsyncSimpleClient(123, a='b') as client: + with mock.patch('socketio.asyncio_simple_client.AsyncClient') \ + as mock_client: + mock_client.return_value.connect = AsyncMock() + + await client.connect('url', headers='h', auth='a', + transports='t', namespace='n', + socketio_path='s') + mock_client.assert_called_once_with(123, a='b') + assert client.client == mock_client() + mock_client().connect.mock.assert_called_once_with( + 'url', headers='h', auth='a', transports='t', + namespaces=['n'], socketio_path='s') + mock_client().event.call_count == 3 + mock_client().on.called_once_with('*') + assert client.namespace == 'n' + assert not client.input_event.is_set() + + _run(_t()) + def test_connect_twice(self): client = AsyncSimpleClient(123, a='b') client.client = mock.MagicMock() @@ -158,6 +180,8 @@ def test_disconnect(self): mc = mock.MagicMock() mc.disconnect = AsyncMock() client.client = mc + client.connected = True + _run(client.disconnect()) _run(client.disconnect()) mc.disconnect.mock.assert_called_once_with() assert client.client is None diff --git a/tests/common/test_simple_client.py b/tests/common/test_simple_client.py index f445ff85..2a0b7b7d 100644 --- a/tests/common/test_simple_client.py +++ b/tests/common/test_simple_client.py @@ -29,6 +29,21 @@ def test_connect(self): assert client.namespace == 'n' assert not client.input_event.is_set() + def test_connect_context_manager(self): + with SimpleClient(123, a='b') as client: + with mock.patch('socketio.simple_client.Client') as mock_client: + client.connect('url', headers='h', auth='a', transports='t', + namespace='n', socketio_path='s') + mock_client.assert_called_once_with(123, a='b') + assert client.client == mock_client() + mock_client().connect.assert_called_once_with( + 'url', headers='h', auth='a', transports='t', + namespaces=['n'], socketio_path='s') + mock_client().event.call_count == 3 + mock_client().on.called_once_with('*') + assert client.namespace == 'n' + assert not client.input_event.is_set() + def test_connect_twice(self): client = SimpleClient(123, a='b') client.client = mock.MagicMock() @@ -141,6 +156,8 @@ def test_disconnect(self): client = SimpleClient() mc = mock.MagicMock() client.client = mc + client.connected = True + client.disconnect() client.disconnect() mc.disconnect.assert_called_once_with() assert client.client is None From c419fc5481846ab026ca847234f5ebe5420e510a Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Thu, 14 Sep 2023 00:08:02 +0100 Subject: [PATCH 389/571] Add a shutdown() function for the server --- src/socketio/asyncio_server.py | 9 +++++++++ src/socketio/server.py | 9 +++++++++ tests/asyncio/test_asyncio_server.py | 6 ++++++ tests/common/test_server.py | 5 +++++ 4 files changed, 29 insertions(+) diff --git a/src/socketio/asyncio_server.py b/src/socketio/asyncio_server.py index aff8812a..acdb265d 100644 --- a/src/socketio/asyncio_server.py +++ b/src/socketio/asyncio_server.py @@ -387,6 +387,15 @@ async def disconnect(self, sid, namespace=None, ignore_queue=False): await self.manager.disconnect(sid, namespace=namespace, ignore_queue=True) + async def shutdown(self): + """Stop Socket.IO background tasks. + + This method stops all background activity initiated by the Socket.IO + server. It must be called before shutting down the web server. + """ + self.logger.info('Socket.IO is shutting down') + await self.eio.shutdown() + async def handle_request(self, *args, **kwargs): """Handle an HTTP request from the client. diff --git a/src/socketio/server.py b/src/socketio/server.py index 48d55983..8675b1e2 100644 --- a/src/socketio/server.py +++ b/src/socketio/server.py @@ -570,6 +570,15 @@ def disconnect(self, sid, namespace=None, ignore_queue=False): self.manager.disconnect(sid, namespace=namespace, ignore_queue=True) + def shutdown(self): + """Stop Socket.IO background tasks. + + This method stops all background activity initiated by the Socket.IO + server. It must be called before shutting down the web server. + """ + self.logger.info('Socket.IO is shutting down') + self.eio.shutdown() + def transport(self, sid): """Return the name of the transport used by the client. diff --git a/tests/asyncio/test_asyncio_server.py b/tests/asyncio/test_asyncio_server.py index 0adce679..ef9c2bb6 100644 --- a/tests/asyncio/test_asyncio_server.py +++ b/tests/asyncio/test_asyncio_server.py @@ -981,6 +981,12 @@ def test_async_handlers(self, eio): None, ) + def test_shutdown(self, eio): + s = asyncio_server.AsyncServer() + s.eio.shutdown = AsyncMock() + _run(s.shutdown()) + s.eio.shutdown.mock.assert_called_once_with() + def test_start_background_task(self, eio): s = asyncio_server.AsyncServer() s.start_background_task('foo', 'bar', baz='baz') diff --git a/tests/common/test_server.py b/tests/common/test_server.py index 9285e570..08c59ac8 100644 --- a/tests/common/test_server.py +++ b/tests/common/test_server.py @@ -917,6 +917,11 @@ def test_async_handlers(self, eio): None, ) + def test_shutdown(self, eio): + s = server.Server() + s.shutdown() + s.eio.shutdown.assert_called_once_with() + def test_start_background_task(self, eio): s = server.Server() s.start_background_task('foo', 'bar', baz='baz') From dc6e4f516f38d1125cb44ccae11b120ffa8fe7f3 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 16 Sep 2023 20:20:16 +0100 Subject: [PATCH 390/571] Message queue optimizations (Fixes #1240) --- src/socketio/asyncio_pubsub_manager.py | 55 +++++++----- src/socketio/pubsub_manager.py | 67 ++++++++------ tests/asyncio/test_asyncio_pubsub_manager.py | 91 +++++++++++++++----- tests/common/test_pubsub_manager.py | 87 ++++++++++++++----- 4 files changed, 204 insertions(+), 96 deletions(-) diff --git a/src/socketio/asyncio_pubsub_manager.py b/src/socketio/asyncio_pubsub_manager.py index 1a06889e..b6c1a150 100644 --- a/src/socketio/asyncio_pubsub_manager.py +++ b/src/socketio/asyncio_pubsub_manager.py @@ -64,10 +64,12 @@ async def emit(self, event, data, namespace=None, room=None, skip_sid=None, callback = (room, namespace, id) else: callback = None - await self._publish({'method': 'emit', 'event': event, 'data': data, - 'namespace': namespace, 'room': room, - 'skip_sid': skip_sid, 'callback': callback, - 'host_id': self.host_id}) + message = {'method': 'emit', 'event': event, 'data': data, + 'namespace': namespace, 'room': room, + 'skip_sid': skip_sid, 'callback': callback, + 'host_id': self.host_id} + await self._handle_emit(message) # handle in this host + await self._publish(message) # notify other hosts async def can_disconnect(self, sid, namespace): if self.is_connected(sid, namespace): @@ -76,18 +78,23 @@ async def can_disconnect(self, sid, namespace): else: # client is in another server, so we post request to the queue await self._publish({'method': 'disconnect', 'sid': sid, - 'namespace': namespace or '/'}) + 'namespace': namespace or '/', + 'host_id': self.host_id}) async def disconnect(self, sid, namespace, **kwargs): if kwargs.get('ignore_queue'): return await super(AsyncPubSubManager, self).disconnect( sid, namespace=namespace) - await self._publish({'method': 'disconnect', 'sid': sid, - 'namespace': namespace or '/'}) + message = {'method': 'disconnect', 'sid': sid, + 'namespace': namespace or '/', 'host_id': self.host_id} + await self._handle_disconnect(message) # handle in this host + await self._publish(message) # notify other hosts async def close_room(self, room, namespace=None): - await self._publish({'method': 'close_room', 'room': room, - 'namespace': namespace or '/'}) + message = {'method': 'close_room', 'room': room, + 'namespace': namespace or '/', 'host_id': self.host_id} + await self._handle_close_room(message) # handle in this host + await self._publish(message) # notify other hosts async def _publish(self, data): """Publish a message on the Socket.IO channel. @@ -139,9 +146,12 @@ async def _return_callback(self, host_id, sid, namespace, callback_id, *args): # When an event callback is received, the callback is returned back # the sender, which is identified by the host_id - await self._publish({'method': 'callback', 'host_id': host_id, - 'sid': sid, 'namespace': namespace, - 'id': callback_id, 'args': args}) + if host_id == self.host_id: + await self.trigger_callback(sid, callback_id, args) + else: + await self._publish({'method': 'callback', 'host_id': host_id, + 'sid': sid, 'namespace': namespace, + 'id': callback_id, 'args': args}) async def _handle_disconnect(self, message): await self.server.disconnect(sid=message.get('sid'), @@ -149,8 +159,8 @@ async def _handle_disconnect(self, message): ignore_queue=True) async def _handle_close_room(self, message): - await super().close_room( - room=message.get('room'), namespace=message.get('namespace')) + await super().close_room(room=message.get('room'), + namespace=message.get('namespace')) async def _thread(self): while True: @@ -171,17 +181,18 @@ async def _thread(self): except: pass if data and 'method' in data: - self._get_logger().info('pubsub message: {}'.format( + self._get_logger().debug('pubsub message: {}'.format( data['method'])) try: - if data['method'] == 'emit': - await self._handle_emit(data) - elif data['method'] == 'callback': + if data['method'] == 'callback': await self._handle_callback(data) - elif data['method'] == 'disconnect': - await self._handle_disconnect(data) - elif data['method'] == 'close_room': - await self._handle_close_room(data) + elif data.get('host_id') != self.host_id: + if data['method'] == 'emit': + await self._handle_emit(data) + elif data['method'] == 'disconnect': + await self._handle_disconnect(data) + elif data['method'] == 'close_room': + await self._handle_close_room(data) except asyncio.CancelledError: raise # let the outer try/except handle it except: diff --git a/src/socketio/pubsub_manager.py b/src/socketio/pubsub_manager.py index 51079bf2..788f0a48 100644 --- a/src/socketio/pubsub_manager.py +++ b/src/socketio/pubsub_manager.py @@ -61,10 +61,12 @@ def emit(self, event, data, namespace=None, room=None, skip_sid=None, callback = (room, namespace, id) else: callback = None - self._publish({'method': 'emit', 'event': event, 'data': data, - 'namespace': namespace, 'room': room, - 'skip_sid': skip_sid, 'callback': callback, - 'host_id': self.host_id}) + message = {'method': 'emit', 'event': event, 'data': data, + 'namespace': namespace, 'room': room, + 'skip_sid': skip_sid, 'callback': callback, + 'host_id': self.host_id} + self._handle_emit(message) # handle in this host + self._publish(message) # notify other hosts def can_disconnect(self, sid, namespace): if self.is_connected(sid, namespace): @@ -72,19 +74,25 @@ def can_disconnect(self, sid, namespace): return super().can_disconnect(sid, namespace) else: # client is in another server, so we post request to the queue - self._publish({'method': 'disconnect', 'sid': sid, - 'namespace': namespace or '/'}) + message = {'method': 'disconnect', 'sid': sid, + 'namespace': namespace or '/', 'host_id': self.host_id} + self._handle_disconnect(message) # handle in this host + self._publish(message) # notify other hosts def disconnect(self, sid, namespace=None, **kwargs): if kwargs.get('ignore_queue'): return super(PubSubManager, self).disconnect( sid, namespace=namespace) - self._publish({'method': 'disconnect', 'sid': sid, - 'namespace': namespace or '/'}) + message = {'method': 'disconnect', 'sid': sid, + 'namespace': namespace or '/', 'host_id': self.host_id} + self._handle_disconnect(message) # handle in this host + self._publish(message) # notify other hosts def close_room(self, room, namespace=None): - self._publish({'method': 'close_room', 'room': room, - 'namespace': namespace or '/'}) + message = {'method': 'close_room', 'room': room, + 'namespace': namespace or '/', 'host_id': self.host_id} + self._handle_close_room(message) # handle in this host + self._publish(message) # notify other hosts def _publish(self, data): """Publish a message on the Socket.IO channel. @@ -116,11 +124,10 @@ def _handle_emit(self, message): *remote_callback) else: callback = None - super(PubSubManager, self).emit(message['event'], message['data'], - namespace=message.get('namespace'), - room=message.get('room'), - skip_sid=message.get('skip_sid'), - callback=callback) + super().emit(message['event'], message['data'], + namespace=message.get('namespace'), + room=message.get('room'), + skip_sid=message.get('skip_sid'), callback=callback) def _handle_callback(self, message): if self.host_id == message.get('host_id'): @@ -135,9 +142,12 @@ def _handle_callback(self, message): def _return_callback(self, host_id, sid, namespace, callback_id, *args): # When an event callback is received, the callback is returned back # to the sender, which is identified by the host_id - self._publish({'method': 'callback', 'host_id': host_id, - 'sid': sid, 'namespace': namespace, 'id': callback_id, - 'args': args}) + if host_id == self.host_id: + self.trigger_callback(sid, callback_id, args) + else: + self._publish({'method': 'callback', 'host_id': host_id, + 'sid': sid, 'namespace': namespace, + 'id': callback_id, 'args': args}) def _handle_disconnect(self, message): self.server.disconnect(sid=message.get('sid'), @@ -145,8 +155,8 @@ def _handle_disconnect(self, message): ignore_queue=True) def _handle_close_room(self, message): - super(PubSubManager, self).close_room( - room=message.get('room'), namespace=message.get('namespace')) + super().close_room(room=message.get('room'), + namespace=message.get('namespace')) def _thread(self): for message in self._listen(): @@ -165,17 +175,18 @@ def _thread(self): except: pass if data and 'method' in data: - self._get_logger().info('pubsub message: {}'.format( + self._get_logger().debug('pubsub message: {}'.format( data['method'])) try: - if data['method'] == 'emit': - self._handle_emit(data) - elif data['method'] == 'callback': + if data['method'] == 'callback': self._handle_callback(data) - elif data['method'] == 'disconnect': - self._handle_disconnect(data) - elif data['method'] == 'close_room': - self._handle_close_room(data) + elif data.get('host_id') != self.host_id: + if data['method'] == 'emit': + self._handle_emit(data) + elif data['method'] == 'disconnect': + self._handle_disconnect(data) + elif data['method'] == 'close_room': + self._handle_close_room(data) except: self.server.logger.exception( 'Unknown error in pubsub listening thread') diff --git a/tests/asyncio/test_asyncio_pubsub_manager.py b/tests/asyncio/test_asyncio_pubsub_manager.py index f1a63ebe..fa74f551 100644 --- a/tests/asyncio/test_asyncio_pubsub_manager.py +++ b/tests/asyncio/test_asyncio_pubsub_manager.py @@ -163,13 +163,15 @@ def test_can_disconnect(self): assert _run(self.pm.can_disconnect(sid, '/')) is True _run(self.pm.can_disconnect(sid, '/foo')) self.pm._publish.mock.assert_called_once_with( - {'method': 'disconnect', 'sid': sid, 'namespace': '/foo'} + {'method': 'disconnect', 'sid': sid, 'namespace': '/foo', + 'host_id': '123456'} ) def test_disconnect(self): _run(self.pm.disconnect('foo', '/')) self.pm._publish.mock.assert_called_once_with( - {'method': 'disconnect', 'sid': 'foo', 'namespace': '/'} + {'method': 'disconnect', 'sid': 'foo', 'namespace': '/', + 'host_id': '123456'} ) def test_disconnect_ignore_queue(self): @@ -182,13 +184,15 @@ def test_disconnect_ignore_queue(self): def test_close_room(self): _run(self.pm.close_room('foo')) self.pm._publish.mock.assert_called_once_with( - {'method': 'close_room', 'room': 'foo', 'namespace': '/'} + {'method': 'close_room', 'room': 'foo', 'namespace': '/', + 'host_id': '123456'} ) def test_close_room_with_namespace(self): _run(self.pm.close_room('foo', '/bar')) self.pm._publish.mock.assert_called_once_with( - {'method': 'close_room', 'room': 'foo', 'namespace': '/bar'} + {'method': 'close_room', 'room': 'foo', 'namespace': '/bar', + 'host_id': '123456'} ) def test_handle_emit(self): @@ -263,8 +267,7 @@ def test_handle_emit_with_skip_sid(self): callback=None, ) - def test_handle_emit_with_callback(self): - host_id = self.pm.host_id + def test_handle_emit_with_remote_callback(self): with mock.patch.object( asyncio_manager.AsyncManager, 'emit', new=AsyncMock() ) as super_emit: @@ -275,7 +278,7 @@ def test_handle_emit_with_callback(self): 'data': 'bar', 'namespace': '/baz', 'callback': ('sid', '/baz', 123), - 'host_id': '123456', + 'host_id': 'x', } ) ) @@ -291,7 +294,7 @@ def test_handle_emit_with_callback(self): self.pm._publish.mock.assert_called_once_with( { 'method': 'callback', - 'host_id': host_id, + 'host_id': 'x', 'sid': 'sid', 'namespace': '/baz', 'id': 123, @@ -299,6 +302,32 @@ def test_handle_emit_with_callback(self): } ) + def test_handle_emit_with_local_callback(self): + with mock.patch.object( + asyncio_manager.AsyncManager, 'emit', new=AsyncMock() + ) as super_emit: + _run( + self.pm._handle_emit( + { + 'event': 'foo', + 'data': 'bar', + 'namespace': '/baz', + 'callback': ('sid', '/baz', 123), + 'host_id': self.pm.host_id, + } + ) + ) + assert super_emit.mock.call_count == 1 + assert super_emit.mock.call_args[0] == (self.pm, 'foo', 'bar') + assert super_emit.mock.call_args[1]['namespace'] == '/baz' + assert super_emit.mock.call_args[1]['room'] is None + assert super_emit.mock.call_args[1]['skip_sid'] is None + assert isinstance( + super_emit.mock.call_args[1]['callback'], functools.partial + ) + _run(super_emit.mock.call_args[1]['callback']('one', 2, 'three')) + self.pm._publish.mock.assert_not_called() + def test_handle_callback(self): host_id = self.pm.host_id with mock.patch.object( @@ -419,34 +448,50 @@ def test_background_thread(self): self.pm._handle_callback = AsyncMock() self.pm._handle_disconnect = AsyncMock() self.pm._handle_close_room = AsyncMock() + host_id = self.pm.host_id async def messages(): import pickle - yield {'method': 'emit', 'value': 'foo'} - yield {'missing': 'method'} - yield '{"method": "callback", "value": "bar"}' - yield {'method': 'disconnect', 'sid': '123', 'namespace': '/foo'} - yield {'method': 'bogus'} - yield pickle.dumps({'method': 'close_room', 'value': 'baz'}) + yield {'method': 'emit', 'value': 'foo', 'host_id': 'x'} + yield {'missing': 'method', 'host_id': 'x'} + yield '{"method": "callback", "value": "bar", "host_id": "x"}' + yield {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', + 'host_id': 'x'} + yield {'method': 'bogus', 'host_id': 'x'} + yield pickle.dumps({'method': 'close_room', 'value': 'baz', + 'host_id': 'x'}) yield 'bad json' yield b'bad pickled' + + # these should not publish anything on the queue, as they come from + # the same host + yield {'method': 'emit', 'value': 'foo', 'host_id': host_id} + yield {'method': 'callback', 'value': 'bar', 'host_id': host_id} + yield {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', + 'host_id': host_id} + yield pickle.dumps({'method': 'close_room', 'value': 'baz', + 'host_id': host_id}) raise asyncio.CancelledError() # force the thread to exit self.pm._listen = messages _run(self.pm._thread()) self.pm._handle_emit.mock.assert_called_once_with( - {'method': 'emit', 'value': 'foo'} + {'method': 'emit', 'value': 'foo', 'host_id': 'x'} + ) + self.pm._handle_callback.mock.assert_any_call( + {'method': 'callback', 'value': 'bar', 'host_id': 'x'} ) - self.pm._handle_callback.mock.assert_called_once_with( - {'method': 'callback', 'value': 'bar'} + self.pm._handle_callback.mock.assert_any_call( + {'method': 'callback', 'value': 'bar', 'host_id': host_id} ) self.pm._handle_disconnect.mock.assert_called_once_with( - {'method': 'disconnect', 'sid': '123', 'namespace': '/foo'} + {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', + 'host_id': 'x'} ) self.pm._handle_close_room.mock.assert_called_once_with( - {'method': 'close_room', 'value': 'baz'} + {'method': 'close_room', 'value': 'baz', 'host_id': 'x'} ) def test_background_thread_exception(self): @@ -454,15 +499,15 @@ def test_background_thread_exception(self): asyncio.CancelledError]) async def messages(): - yield {'method': 'emit', 'value': 'foo'} - yield {'method': 'emit', 'value': 'bar'} + yield {'method': 'emit', 'value': 'foo', 'host_id': 'x'} + yield {'method': 'emit', 'value': 'bar', 'host_id': 'x'} self.pm._listen = messages _run(self.pm._thread()) self.pm._handle_emit.mock.assert_any_call( - {'method': 'emit', 'value': 'foo'} + {'method': 'emit', 'value': 'foo', 'host_id': 'x'} ) self.pm._handle_emit.mock.assert_called_with( - {'method': 'emit', 'value': 'bar'} + {'method': 'emit', 'value': 'bar', 'host_id': 'x'} ) diff --git a/tests/common/test_pubsub_manager.py b/tests/common/test_pubsub_manager.py index 51e1f921..269c7c1a 100644 --- a/tests/common/test_pubsub_manager.py +++ b/tests/common/test_pubsub_manager.py @@ -169,13 +169,15 @@ def test_can_disconnect(self): assert self.pm.can_disconnect(sid, '/') self.pm.can_disconnect(sid, '/foo') self.pm._publish.assert_called_once_with( - {'method': 'disconnect', 'sid': sid, 'namespace': '/foo'} + {'method': 'disconnect', 'sid': sid, 'namespace': '/foo', + 'host_id': '123456'} ) def test_disconnect(self): self.pm.disconnect('foo') self.pm._publish.assert_called_once_with( - {'method': 'disconnect', 'sid': 'foo', 'namespace': '/'} + {'method': 'disconnect', 'sid': 'foo', 'namespace': '/', + 'host_id': '123456'} ) def test_disconnect_ignore_queue(self): @@ -188,13 +190,15 @@ def test_disconnect_ignore_queue(self): def test_close_room(self): self.pm.close_room('foo') self.pm._publish.assert_called_once_with( - {'method': 'close_room', 'room': 'foo', 'namespace': '/'} + {'method': 'close_room', 'room': 'foo', 'namespace': '/', + 'host_id': '123456'} ) def test_close_room_with_namespace(self): self.pm.close_room('foo', '/bar') self.pm._publish.assert_called_once_with( - {'method': 'close_room', 'room': 'foo', 'namespace': '/bar'} + {'method': 'close_room', 'room': 'foo', 'namespace': '/bar', + 'host_id': '123456'} ) def test_handle_emit(self): @@ -251,8 +255,7 @@ def test_handle_emit_with_skip_sid(self): callback=None, ) - def test_handle_emit_with_callback(self): - host_id = self.pm.host_id + def test_handle_emit_with_remote_callback(self): with mock.patch.object(base_manager.BaseManager, 'emit') as super_emit: self.pm._handle_emit( { @@ -260,7 +263,7 @@ def test_handle_emit_with_callback(self): 'data': 'bar', 'namespace': '/baz', 'callback': ('sid', '/baz', 123), - 'host_id': host_id, + 'host_id': 'x', } ) assert super_emit.call_count == 1 @@ -275,7 +278,7 @@ def test_handle_emit_with_callback(self): self.pm._publish.assert_called_once_with( { 'method': 'callback', - 'host_id': host_id, + 'host_id': 'x', 'sid': 'sid', 'namespace': '/baz', 'id': 123, @@ -283,6 +286,28 @@ def test_handle_emit_with_callback(self): } ) + def test_handle_emit_with_local_callback(self): + with mock.patch.object(base_manager.BaseManager, 'emit') as super_emit: + self.pm._handle_emit( + { + 'event': 'foo', + 'data': 'bar', + 'namespace': '/baz', + 'callback': ('sid', '/baz', 123), + 'host_id': self.pm.host_id, + } + ) + assert super_emit.call_count == 1 + assert super_emit.call_args[0] == ('foo', 'bar') + assert super_emit.call_args[1]['namespace'] == '/baz' + assert super_emit.call_args[1]['room'] is None + assert super_emit.call_args[1]['skip_sid'] is None + assert isinstance( + super_emit.call_args[1]['callback'], functools.partial + ) + super_emit.call_args[1]['callback']('one', 2, 'three') + self.pm._publish.assert_not_called() + def test_handle_callback(self): host_id = self.pm.host_id with mock.patch.object(self.pm, 'trigger_callback') as trigger: @@ -373,19 +398,31 @@ def test_background_thread(self): self.pm._handle_callback = mock.MagicMock() self.pm._handle_disconnect = mock.MagicMock() self.pm._handle_close_room = mock.MagicMock() + host_id = self.pm.host_id def messages(): import pickle - yield {'method': 'emit', 'value': 'foo'} - yield {'missing': 'method'} - yield '{"method": "callback", "value": "bar"}' - yield {'method': 'disconnect', 'sid': '123', 'namespace': '/foo'} - yield {'method': 'bogus'} - yield pickle.dumps({'method': 'close_room', 'value': 'baz'}) + yield {'method': 'emit', 'value': 'foo', 'host_id': 'x'} + yield {'missing': 'method', 'host_id': 'x'} + yield '{"method": "callback", "value": "bar", "host_id": "x"}' + yield {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', + 'host_id': 'x'} + yield {'method': 'bogus', 'host_id': 'x'} + yield pickle.dumps({'method': 'close_room', 'value': 'baz', + 'host_id': 'x'}) yield 'bad json' yield b'bad pickled' + # these should not publish anything on the queue, as they come from + # the same host + yield {'method': 'emit', 'value': 'foo', 'host_id': host_id} + yield {'method': 'callback', 'value': 'bar', 'host_id': host_id} + yield {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', + 'host_id': host_id} + yield pickle.dumps({'method': 'close_room', 'value': 'baz', + 'host_id': host_id}) + self.pm._listen = mock.MagicMock(side_effect=messages) try: self.pm._thread() @@ -393,24 +430,28 @@ def messages(): pass self.pm._handle_emit.assert_called_once_with( - {'method': 'emit', 'value': 'foo'} + {'method': 'emit', 'value': 'foo', 'host_id': 'x'} + ) + self.pm._handle_callback.assert_any_call( + {'method': 'callback', 'value': 'bar', 'host_id': 'x'} ) - self.pm._handle_callback.assert_called_once_with( - {'method': 'callback', 'value': 'bar'} + self.pm._handle_callback.assert_any_call( + {'method': 'callback', 'value': 'bar', 'host_id': host_id} ) self.pm._handle_disconnect.assert_called_once_with( - {'method': 'disconnect', 'sid': '123', 'namespace': '/foo'} + {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', + 'host_id': 'x'} ) self.pm._handle_close_room.assert_called_once_with( - {'method': 'close_room', 'value': 'baz'} + {'method': 'close_room', 'value': 'baz', 'host_id': 'x'} ) def test_background_thread_exception(self): self.pm._handle_emit = mock.MagicMock(side_effect=[ValueError(), None]) def messages(): - yield {'method': 'emit', 'value': 'foo'} - yield {'method': 'emit', 'value': 'bar'} + yield {'method': 'emit', 'value': 'foo', 'host_id': 'x'} + yield {'method': 'emit', 'value': 'bar', 'host_id': 'x'} self.pm._listen = mock.MagicMock(side_effect=messages) try: @@ -419,8 +460,8 @@ def messages(): pass self.pm._handle_emit.assert_any_call( - {'method': 'emit', 'value': 'foo'} + {'method': 'emit', 'value': 'foo', 'host_id': 'x'} ) self.pm._handle_emit.assert_called_with( - {'method': 'emit', 'value': 'bar'} + {'method': 'emit', 'value': 'bar', 'host_id': 'x'} ) From 8da3c617a66877ec0fc5ab25256ead41ae611351 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 16 Sep 2023 23:41:34 +0100 Subject: [PATCH 391/571] Remove unneeded arguments from super() --- src/socketio/asyncio_pubsub_manager.py | 2 +- src/socketio/kafka_manager.py | 3 +-- src/socketio/kombu_manager.py | 6 ++---- src/socketio/middleware.py | 8 +++----- src/socketio/namespace.py | 4 ++-- src/socketio/pubsub_manager.py | 9 ++++----- src/socketio/redis_manager.py | 6 ++---- src/socketio/zmq_manager.py | 4 +--- 8 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/socketio/asyncio_pubsub_manager.py b/src/socketio/asyncio_pubsub_manager.py index b6c1a150..ab0a93a1 100644 --- a/src/socketio/asyncio_pubsub_manager.py +++ b/src/socketio/asyncio_pubsub_manager.py @@ -83,7 +83,7 @@ async def can_disconnect(self, sid, namespace): async def disconnect(self, sid, namespace, **kwargs): if kwargs.get('ignore_queue'): - return await super(AsyncPubSubManager, self).disconnect( + return await super().disconnect( sid, namespace=namespace) message = {'method': 'disconnect', 'sid': sid, 'namespace': namespace or '/', 'host_id': self.host_id} diff --git a/src/socketio/kafka_manager.py b/src/socketio/kafka_manager.py index 739871a3..4d87d46f 100644 --- a/src/socketio/kafka_manager.py +++ b/src/socketio/kafka_manager.py @@ -43,8 +43,7 @@ def __init__(self, url='kafka://localhost:9092', channel='socketio', '(Run "pip install kafka-python" in your ' 'virtualenv).') - super(KafkaManager, self).__init__(channel=channel, - write_only=write_only) + super().__init__(channel=channel, write_only=write_only) urls = [url] if isinstance(url, str) else url self.kafka_urls = [url[8:] if url != 'kafka://' else 'localhost:9092' diff --git a/src/socketio/kombu_manager.py b/src/socketio/kombu_manager.py index 7350c156..0a63bc26 100644 --- a/src/socketio/kombu_manager.py +++ b/src/socketio/kombu_manager.py @@ -54,9 +54,7 @@ def __init__(self, url='amqp://guest:guest@localhost:5672//', raise RuntimeError('Kombu package is not installed ' '(Run "pip install kombu" in your ' 'virtualenv).') - super(KombuManager, self).__init__(channel=channel, - write_only=write_only, - logger=logger) + super().__init__(channel=channel, write_only=write_only, logger=logger) self.url = url self.connection_options = connection_options or {} self.exchange_options = exchange_options or {} @@ -65,7 +63,7 @@ def __init__(self, url='amqp://guest:guest@localhost:5672//', self.publisher_connection = self._connection() def initialize(self): - super(KombuManager, self).initialize() + super().initialize() monkey_patched = True if self.server.async_mode == 'eventlet': diff --git a/src/socketio/middleware.py b/src/socketio/middleware.py index 1a697408..acc8ffd3 100644 --- a/src/socketio/middleware.py +++ b/src/socketio/middleware.py @@ -29,14 +29,12 @@ class WSGIApp(engineio.WSGIApp): """ def __init__(self, socketio_app, wsgi_app=None, static_files=None, socketio_path='socket.io'): - super(WSGIApp, self).__init__(socketio_app, wsgi_app, - static_files=static_files, - engineio_path=socketio_path) + super().__init__(socketio_app, wsgi_app, static_files=static_files, + engineio_path=socketio_path) class Middleware(WSGIApp): """This class has been renamed to WSGIApp and is now deprecated.""" def __init__(self, socketio_app, wsgi_app=None, socketio_path='socket.io'): - super(Middleware, self).__init__(socketio_app, wsgi_app, - socketio_path=socketio_path) + super().__init__(socketio_app, wsgi_app, socketio_path=socketio_path) diff --git a/src/socketio/namespace.py b/src/socketio/namespace.py index c0fddd14..5088cd55 100644 --- a/src/socketio/namespace.py +++ b/src/socketio/namespace.py @@ -31,7 +31,7 @@ class Namespace(BaseNamespace): omitted, the default namespace is used. """ def __init__(self, namespace=None): - super(Namespace, self).__init__(namespace=namespace) + super().__init__(namespace=namespace) self.server = None def _set_server(self, server): @@ -166,7 +166,7 @@ class ClientNamespace(BaseNamespace): omitted, the default namespace is used. """ def __init__(self, namespace=None): - super(ClientNamespace, self).__init__(namespace=namespace) + super().__init__(namespace=namespace) self.client = None def _set_client(self, client): diff --git a/src/socketio/pubsub_manager.py b/src/socketio/pubsub_manager.py index 788f0a48..fa5eba4d 100644 --- a/src/socketio/pubsub_manager.py +++ b/src/socketio/pubsub_manager.py @@ -24,14 +24,14 @@ class PubSubManager(BaseManager): name = 'pubsub' def __init__(self, channel='socketio', write_only=False, logger=None): - super(PubSubManager, self).__init__() + super().__init__() self.channel = channel self.write_only = write_only self.host_id = uuid.uuid4().hex self.logger = logger def initialize(self): - super(PubSubManager, self).initialize() + super().initialize() if not self.write_only: self.thread = self.server.start_background_task(self._thread) self._get_logger().info(self.name + ' backend initialized.') @@ -47,7 +47,7 @@ def emit(self, event, data, namespace=None, room=None, skip_sid=None, The parameters are the same as in :meth:`.Server.emit`. """ if kwargs.get('ignore_queue'): - return super(PubSubManager, self).emit( + return super().emit( event, data, namespace=namespace, room=room, skip_sid=skip_sid, callback=callback) namespace = namespace or '/' @@ -81,8 +81,7 @@ def can_disconnect(self, sid, namespace): def disconnect(self, sid, namespace=None, **kwargs): if kwargs.get('ignore_queue'): - return super(PubSubManager, self).disconnect( - sid, namespace=namespace) + return super().disconnect(sid, namespace=namespace) message = {'method': 'disconnect', 'sid': sid, 'namespace': namespace or '/', 'host_id': self.host_id} self._handle_disconnect(message) # handle in this host diff --git a/src/socketio/redis_manager.py b/src/socketio/redis_manager.py index ab40739e..ae9fa292 100644 --- a/src/socketio/redis_manager.py +++ b/src/socketio/redis_manager.py @@ -48,12 +48,10 @@ def __init__(self, url='redis://localhost:6379/0', channel='socketio', self.redis_url = url self.redis_options = redis_options or {} self._redis_connect() - super(RedisManager, self).__init__(channel=channel, - write_only=write_only, - logger=logger) + super().__init__(channel=channel, write_only=write_only, logger=logger) def initialize(self): - super(RedisManager, self).initialize() + super().initialize() monkey_patched = True if self.server.async_mode == 'eventlet': diff --git a/src/socketio/zmq_manager.py b/src/socketio/zmq_manager.py index ec360607..760fbc38 100644 --- a/src/socketio/zmq_manager.py +++ b/src/socketio/zmq_manager.py @@ -72,9 +72,7 @@ def __init__(self, url='zmq+tcp://localhost:5555+5556', self.sink = sink self.sub = sub self.channel = channel - super(ZmqManager, self).__init__(channel=channel, - write_only=write_only, - logger=logger) + super().__init__(channel=channel, write_only=write_only, logger=logger) def _publish(self, data): pickled_data = pickle.dumps( From 8e3460c5fcc351ca0cff83c690eedcde0e8a6e7e Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 19 Sep 2023 15:22:05 +0100 Subject: [PATCH 392/571] Async versions of enter_room and leave_room should be coroutines (breaking change) --- examples/server/aiohttp/app.py | 4 +-- examples/server/asgi/app.py | 4 +-- examples/server/sanic/app.py | 4 +-- examples/server/tornado/app.py | 4 +-- src/socketio/asyncio_manager.py | 14 ++++++++ src/socketio/asyncio_namespace.py | 24 ++++++++++++++ src/socketio/asyncio_server.py | 34 +++++++++++++++++++ src/socketio/base_manager.py | 20 +++++++---- tests/asyncio/test_asyncio_manager.py | 44 +++++++++++++------------ tests/asyncio/test_asyncio_namespace.py | 24 ++++++++------ tests/asyncio/test_asyncio_server.py | 20 ++++++----- 11 files changed, 143 insertions(+), 53 deletions(-) diff --git a/examples/server/aiohttp/app.py b/examples/server/aiohttp/app.py index 14864728..cba51937 100644 --- a/examples/server/aiohttp/app.py +++ b/examples/server/aiohttp/app.py @@ -33,14 +33,14 @@ async def my_broadcast_event(sid, message): @sio.event async def join(sid, message): - sio.enter_room(sid, message['room']) + await sio.enter_room(sid, message['room']) await sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, room=sid) @sio.event async def leave(sid, message): - sio.leave_room(sid, message['room']) + await sio.leave_room(sid, message['room']) await sio.emit('my_response', {'data': 'Left room: ' + message['room']}, room=sid) diff --git a/examples/server/asgi/app.py b/examples/server/asgi/app.py index c6068237..22180bb0 100644 --- a/examples/server/asgi/app.py +++ b/examples/server/asgi/app.py @@ -31,14 +31,14 @@ async def test_broadcast_message(sid, message): @sio.on('join') async def join(sid, message): - sio.enter_room(sid, message['room']) + await sio.enter_room(sid, message['room']) await sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, room=sid) @sio.on('leave') async def leave(sid, message): - sio.leave_room(sid, message['room']) + await sio.leave_room(sid, message['room']) await sio.emit('my_response', {'data': 'Left room: ' + message['room']}, room=sid) diff --git a/examples/server/sanic/app.py b/examples/server/sanic/app.py index 8e9ab47a..e10d764d 100644 --- a/examples/server/sanic/app.py +++ b/examples/server/sanic/app.py @@ -40,14 +40,14 @@ async def my_broadcast_event(sid, message): @sio.event async def join(sid, message): - sio.enter_room(sid, message['room']) + await sio.enter_room(sid, message['room']) await sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, room=sid) @sio.event async def leave(sid, message): - sio.leave_room(sid, message['room']) + await sio.leave_room(sid, message['room']) await sio.emit('my_response', {'data': 'Left room: ' + message['room']}, room=sid) diff --git a/examples/server/tornado/app.py b/examples/server/tornado/app.py index 92fbfed6..16f7a191 100644 --- a/examples/server/tornado/app.py +++ b/examples/server/tornado/app.py @@ -38,14 +38,14 @@ async def my_broadcast_event(sid, message): @sio.event async def join(sid, message): - sio.enter_room(sid, message['room']) + await sio.enter_room(sid, message['room']) await sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, room=sid) @sio.event async def leave(sid, message): - sio.leave_room(sid, message['room']) + await sio.leave_room(sid, message['room']) await sio.emit('my_response', {'data': 'Left room: ' + message['room']}, room=sid) diff --git a/src/socketio/asyncio_manager.py b/src/socketio/asyncio_manager.py index 2bf90011..cc0ebfd5 100644 --- a/src/socketio/asyncio_manager.py +++ b/src/socketio/asyncio_manager.py @@ -69,6 +69,20 @@ async def disconnect(self, sid, namespace, **kwargs): """ return super().disconnect(sid, namespace, **kwargs) + async def enter_room(self, sid, namespace, room, eio_sid=None): + """Add a client to a room. + + Note: this method is a coroutine. + """ + return super().enter_room(sid, namespace, room, eio_sid=eio_sid) + + async def leave_room(self, sid, namespace, room): + """Remove a client from a room. + + Note: this method is a coroutine. + """ + return super().leave_room(sid, namespace, room) + async def close_room(self, room, namespace): """Remove all participants from a room. diff --git a/src/socketio/asyncio_namespace.py b/src/socketio/asyncio_namespace.py index 4baf28fc..96f8d27a 100644 --- a/src/socketio/asyncio_namespace.py +++ b/src/socketio/asyncio_namespace.py @@ -86,6 +86,30 @@ async def call(self, event, data=None, to=None, sid=None, namespace=None, timeout=timeout, ignore_queue=ignore_queue) + async def enter_room(self, sid, room, namespace=None): + """Enter a room. + + The only difference with the :func:`socketio.Server.enter_room` method + is that when the ``namespace`` argument is not given the namespace + associated with the class is used. + + Note: this method is a coroutine. + """ + return await self.server.enter_room( + sid, room, namespace=namespace or self.namespace) + + async def leave_room(self, sid, room, namespace=None): + """Leave a room. + + The only difference with the :func:`socketio.Server.leave_room` method + is that when the ``namespace`` argument is not given the namespace + associated with the class is used. + + Note: this method is a coroutine. + """ + return await self.server.leave_room( + sid, room, namespace=namespace or self.namespace) + async def close_room(self, room, namespace=None): """Close a room. diff --git a/src/socketio/asyncio_server.py b/src/socketio/asyncio_server.py index acdb265d..1e9cd9c4 100644 --- a/src/socketio/asyncio_server.py +++ b/src/socketio/asyncio_server.py @@ -275,6 +275,40 @@ def event_callback(*args): else callback_args[0][0] if len(callback_args[0]) == 1 \ else None + async def enter_room(self, sid, room, namespace=None): + """Enter a room. + + This function adds the client to a room. The :func:`emit` and + :func:`send` functions can optionally broadcast events to all the + clients in a room. + + :param sid: Session ID of the client. + :param room: Room name. If the room does not exist it is created. + :param namespace: The Socket.IO namespace for the event. If this + argument is omitted the default namespace is used. + + Note: this method is a coroutine. + """ + namespace = namespace or '/' + self.logger.info('%s is entering room %s [%s]', sid, room, namespace) + await self.manager.enter_room(sid, namespace, room) + + async def leave_room(self, sid, room, namespace=None): + """Leave a room. + + This function removes the client from a room. + + :param sid: Session ID of the client. + :param room: Room name. + :param namespace: The Socket.IO namespace for the event. If this + argument is omitted the default namespace is used. + + Note: this method is a coroutine. + """ + namespace = namespace or '/' + self.logger.info('%s is leaving room %s [%s]', sid, room, namespace) + await self.manager.leave_room(sid, namespace, room) + async def close_room(self, room, namespace=None): """Close a room. diff --git a/src/socketio/base_manager.py b/src/socketio/base_manager.py index 4330bac4..f0556131 100644 --- a/src/socketio/base_manager.py +++ b/src/socketio/base_manager.py @@ -54,11 +54,11 @@ def connect(self, eio_sid, namespace): """Register a client connection to a namespace.""" sid = self.server.eio.generate_id() try: - self.enter_room(sid, namespace, None, eio_sid=eio_sid) + self.basic_enter_room(sid, namespace, None, eio_sid=eio_sid) except ValueDuplicationError: # already connected return None - self.enter_room(sid, namespace, sid, eio_sid=eio_sid) + self.basic_enter_room(sid, namespace, sid, eio_sid=eio_sid) return sid def is_connected(self, sid, namespace): @@ -106,7 +106,7 @@ def disconnect(self, sid, namespace, **kwargs): if sid in room: rooms.append(room_name) for room in rooms: - self.leave_room(sid, namespace, room) + self.basic_leave_room(sid, namespace, room) if sid in self.callbacks: del self.callbacks[sid] if namespace in self.pending_disconnect and \ @@ -115,7 +115,7 @@ def disconnect(self, sid, namespace, **kwargs): if len(self.pending_disconnect[namespace]) == 0: del self.pending_disconnect[namespace] - def enter_room(self, sid, namespace, room, eio_sid=None): + def basic_enter_room(self, sid, namespace, room, eio_sid=None): """Add a client to a room.""" if eio_sid is None and namespace not in self.rooms: raise ValueError('sid is not connected to requested namespace') @@ -127,7 +127,7 @@ def enter_room(self, sid, namespace, room, eio_sid=None): eio_sid = self.rooms[namespace][None][sid] self.rooms[namespace][room][sid] = eio_sid - def leave_room(self, sid, namespace, room): + def basic_leave_room(self, sid, namespace, room): """Remove a client from a room.""" try: del self.rooms[namespace][room][sid] @@ -138,11 +138,19 @@ def leave_room(self, sid, namespace, room): except KeyError: pass + def enter_room(self, sid, namespace, room, eio_sid=None): + """Add a client to a room.""" + self.basic_enter_room(sid, namespace, room, eio_sid=eio_sid) + + def leave_room(self, sid, namespace, room): + """Remove a client from a room.""" + self.basic_leave_room(sid, namespace, room) + def close_room(self, room, namespace): """Remove all participants from a room.""" try: for sid, _ in self.get_participants(namespace, room): - self.leave_room(sid, namespace, room) + self.basic_leave_room(sid, namespace, room) except KeyError: pass diff --git a/tests/asyncio/test_asyncio_manager.py b/tests/asyncio/test_asyncio_manager.py index c4247234..a32b0fdb 100644 --- a/tests/asyncio/test_asyncio_manager.py +++ b/tests/asyncio/test_asyncio_manager.py @@ -54,8 +54,8 @@ def test_pre_disconnect(self): def test_disconnect(self): sid1 = self.bm.connect('123', '/foo') sid2 = self.bm.connect('456', '/foo') - self.bm.enter_room(sid1, '/foo', 'bar') - self.bm.enter_room(sid2, '/foo', 'baz') + _run(self.bm.enter_room(sid1, '/foo', 'bar')) + _run(self.bm.enter_room(sid2, '/foo', 'baz')) _run(self.bm.disconnect(sid1, '/foo')) assert dict(self.bm.rooms['/foo'][None]) == {sid2: '456'} assert dict(self.bm.rooms['/foo'][sid2]) == {sid2: '456'} @@ -97,8 +97,8 @@ def test_disconnect_twice(self): def test_disconnect_all(self): sid1 = self.bm.connect('123', '/foo') sid2 = self.bm.connect('456', '/foo') - self.bm.enter_room(sid1, '/foo', 'bar') - self.bm.enter_room(sid2, '/foo', 'baz') + _run(self.bm.enter_room(sid1, '/foo', 'bar')) + _run(self.bm.enter_room(sid2, '/foo', 'baz')) _run(self.bm.disconnect(sid1, '/foo')) _run(self.bm.disconnect(sid2, '/foo')) assert self.bm.rooms == {} @@ -173,8 +173,8 @@ def test_get_participants(self): def test_leave_invalid_room(self): sid = self.bm.connect('123', '/foo') - self.bm.leave_room(sid, '/foo', 'baz') - self.bm.leave_room(sid, '/bar', 'baz') + _run(self.bm.leave_room(sid, '/foo', 'baz')) + _run(self.bm.leave_room(sid, '/bar', 'baz')) def test_no_room(self): rooms = self.bm.get_rooms('123', '/foo') @@ -184,9 +184,11 @@ def test_close_room(self): sid = self.bm.connect('123', '/foo') self.bm.connect('456', '/foo') self.bm.connect('789', '/foo') - self.bm.enter_room(sid, '/foo', 'bar') - self.bm.enter_room(sid, '/foo', 'bar') + _run(self.bm.enter_room(sid, '/foo', 'bar')) + _run(self.bm.enter_room(sid, '/foo', 'bar')) _run(self.bm.close_room('bar', '/foo')) + from pprint import pprint + pprint(self.bm.rooms) assert 'bar' not in self.bm.rooms['/foo'] def test_close_invalid_room(self): @@ -194,7 +196,7 @@ def test_close_invalid_room(self): def test_rooms(self): sid = self.bm.connect('123', '/foo') - self.bm.enter_room(sid, '/foo', 'bar') + _run(self.bm.enter_room(sid, '/foo', 'bar')) r = self.bm.get_rooms(sid, '/foo') assert len(r) == 2 assert sid in r @@ -216,9 +218,9 @@ def test_emit_to_sid(self): def test_emit_to_room(self): sid1 = self.bm.connect('123', '/foo') - self.bm.enter_room(sid1, '/foo', 'bar') + _run(self.bm.enter_room(sid1, '/foo', 'bar')) sid2 = self.bm.connect('456', '/foo') - self.bm.enter_room(sid2, '/foo', 'bar') + _run(self.bm.enter_room(sid2, '/foo', 'bar')) self.bm.connect('789', '/foo') _run( self.bm.emit( @@ -237,12 +239,12 @@ def test_emit_to_room(self): def test_emit_to_rooms(self): sid1 = self.bm.connect('123', '/foo') - self.bm.enter_room(sid1, '/foo', 'bar') + _run(self.bm.enter_room(sid1, '/foo', 'bar')) sid2 = self.bm.connect('456', '/foo') - self.bm.enter_room(sid2, '/foo', 'bar') - self.bm.enter_room(sid2, '/foo', 'baz') + _run(self.bm.enter_room(sid2, '/foo', 'bar')) + _run(self.bm.enter_room(sid2, '/foo', 'baz')) sid3 = self.bm.connect('789', '/foo') - self.bm.enter_room(sid3, '/foo', 'baz') + _run(self.bm.enter_room(sid3, '/foo', 'baz')) _run( self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', room=['bar', 'baz']) @@ -263,9 +265,9 @@ def test_emit_to_rooms(self): def test_emit_to_all(self): sid1 = self.bm.connect('123', '/foo') - self.bm.enter_room(sid1, '/foo', 'bar') + _run(self.bm.enter_room(sid1, '/foo', 'bar')) sid2 = self.bm.connect('456', '/foo') - self.bm.enter_room(sid2, '/foo', 'bar') + _run(self.bm.enter_room(sid2, '/foo', 'bar')) self.bm.connect('789', '/foo') self.bm.connect('abc', '/bar') _run(self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo')) @@ -285,9 +287,9 @@ def test_emit_to_all(self): def test_emit_to_all_skip_one(self): sid1 = self.bm.connect('123', '/foo') - self.bm.enter_room(sid1, '/foo', 'bar') + _run(self.bm.enter_room(sid1, '/foo', 'bar')) sid2 = self.bm.connect('456', '/foo') - self.bm.enter_room(sid2, '/foo', 'bar') + _run(self.bm.enter_room(sid2, '/foo', 'bar')) self.bm.connect('789', '/foo') self.bm.connect('abc', '/bar') _run( @@ -307,9 +309,9 @@ def test_emit_to_all_skip_one(self): def test_emit_to_all_skip_two(self): sid1 = self.bm.connect('123', '/foo') - self.bm.enter_room(sid1, '/foo', 'bar') + _run(self.bm.enter_room(sid1, '/foo', 'bar')) sid2 = self.bm.connect('456', '/foo') - self.bm.enter_room(sid2, '/foo', 'bar') + _run(self.bm.enter_room(sid2, '/foo', 'bar')) sid3 = self.bm.connect('789', '/foo') self.bm.connect('abc', '/bar') _run( diff --git a/tests/asyncio/test_asyncio_namespace.py b/tests/asyncio/test_asyncio_namespace.py index 0d9e6ce7..f0fd0213 100644 --- a/tests/asyncio/test_asyncio_namespace.py +++ b/tests/asyncio/test_asyncio_namespace.py @@ -176,25 +176,29 @@ def test_call(self): def test_enter_room(self): ns = asyncio_namespace.AsyncNamespace('/foo') - ns._set_server(mock.MagicMock()) - ns.enter_room('sid', 'room') - ns.server.enter_room.assert_called_with( + mock_server = mock.MagicMock() + mock_server.enter_room = AsyncMock() + ns._set_server(mock_server) + _run(ns.enter_room('sid', 'room')) + ns.server.enter_room.mock.assert_called_with( 'sid', 'room', namespace='/foo' ) - ns.enter_room('sid', 'room', namespace='/bar') - ns.server.enter_room.assert_called_with( + _run(ns.enter_room('sid', 'room', namespace='/bar')) + ns.server.enter_room.mock.assert_called_with( 'sid', 'room', namespace='/bar' ) def test_leave_room(self): ns = asyncio_namespace.AsyncNamespace('/foo') - ns._set_server(mock.MagicMock()) - ns.leave_room('sid', 'room') - ns.server.leave_room.assert_called_with( + mock_server = mock.MagicMock() + mock_server.leave_room = AsyncMock() + ns._set_server(mock_server) + _run(ns.leave_room('sid', 'room')) + ns.server.leave_room.mock.assert_called_with( 'sid', 'room', namespace='/foo' ) - ns.leave_room('sid', 'room', namespace='/bar') - ns.server.leave_room.assert_called_with( + _run(ns.leave_room('sid', 'room', namespace='/bar')) + ns.server.leave_room.mock.assert_called_with( 'sid', 'room', namespace='/bar' ) diff --git a/tests/asyncio/test_asyncio_server.py b/tests/asyncio/test_asyncio_server.py index ef9c2bb6..c39ed788 100644 --- a/tests/asyncio/test_asyncio_server.py +++ b/tests/asyncio/test_asyncio_server.py @@ -29,6 +29,8 @@ def _get_mock_manager(self): mgr = mock.MagicMock() mgr.can_disconnect = AsyncMock() mgr.emit = AsyncMock() + mgr.enter_room = AsyncMock() + mgr.leave_room = AsyncMock() mgr.close_room = AsyncMock() mgr.trigger_callback = AsyncMock() return mgr @@ -231,26 +233,28 @@ def test_call_without_async_handlers(self, eio): def test_enter_room(self, eio): mgr = self._get_mock_manager() s = asyncio_server.AsyncServer(client_manager=mgr) - s.enter_room('123', 'room', namespace='/foo') - s.manager.enter_room.assert_called_once_with('123', '/foo', 'room') + _run(s.enter_room('123', 'room', namespace='/foo')) + s.manager.enter_room.mock.assert_called_once_with('123', '/foo', + 'room') def test_enter_room_default_namespace(self, eio): mgr = self._get_mock_manager() s = asyncio_server.AsyncServer(client_manager=mgr) - s.enter_room('123', 'room') - s.manager.enter_room.assert_called_once_with('123', '/', 'room') + _run(s.enter_room('123', 'room')) + s.manager.enter_room.mock.assert_called_once_with('123', '/', 'room') def test_leave_room(self, eio): mgr = self._get_mock_manager() s = asyncio_server.AsyncServer(client_manager=mgr) - s.leave_room('123', 'room', namespace='/foo') - s.manager.leave_room.assert_called_once_with('123', '/foo', 'room') + _run(s.leave_room('123', 'room', namespace='/foo')) + s.manager.leave_room.mock.assert_called_once_with('123', '/foo', + 'room') def test_leave_room_default_namespace(self, eio): mgr = self._get_mock_manager() s = asyncio_server.AsyncServer(client_manager=mgr) - s.leave_room('123', 'room') - s.manager.leave_room.assert_called_once_with('123', '/', 'room') + _run(s.leave_room('123', 'room')) + s.manager.leave_room.mock.assert_called_once_with('123', '/', 'room') def test_close_room(self, eio): mgr = self._get_mock_manager() From 3f78af283171d23b7f1dbfd055e21afe00877399 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 19 Sep 2023 19:13:37 +0100 Subject: [PATCH 393/571] Update Socket.IO JavaScript client to version 4.7.2 in all examples --- examples/client/javascript/package-lock.json | 126 +++++++++--------- examples/server/aiohttp/app.html | 2 +- examples/server/aiohttp/fiddle.html | 2 +- examples/server/aiohttp/latency.html | 2 +- examples/server/asgi/app.html | 2 +- examples/server/asgi/fiddle.html | 2 +- examples/server/asgi/latency.html | 2 +- examples/server/javascript/package-lock.json | 126 +++++++++--------- examples/server/sanic/app.html | 2 +- examples/server/sanic/fiddle.html | 2 +- examples/server/sanic/latency.html | 2 +- examples/server/tornado/templates/app.html | 2 +- examples/server/tornado/templates/fiddle.html | 2 +- .../server/tornado/templates/latency.html | 2 +- .../socketio_app/static/index.html | 2 +- examples/server/wsgi/templates/fiddle.html | 2 +- examples/server/wsgi/templates/index.html | 4 +- examples/server/wsgi/templates/latency.html | 2 +- 18 files changed, 145 insertions(+), 141 deletions(-) diff --git a/examples/client/javascript/package-lock.json b/examples/client/javascript/package-lock.json index 27ab1964..e9d22417 100644 --- a/examples/client/javascript/package-lock.json +++ b/examples/client/javascript/package-lock.json @@ -25,17 +25,17 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "node_modules/@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "version": "2.8.14", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.14.tgz", + "integrity": "sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { - "version": "18.15.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz", - "integrity": "sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ==" + "version": "20.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", + "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==" }, "node_modules/accepts": { "version": "1.3.8", @@ -188,9 +188,9 @@ } }, "node_modules/engine.io": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz", - "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2.tgz", + "integrity": "sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -200,21 +200,21 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", + "engine.io-parser": "~5.2.1", "ws": "~8.11.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/engine.io-client": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz", - "integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz", + "integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", + "engine.io-parser": "~5.2.1", "ws": "~8.11.0", "xmlhttprequest-ssl": "~2.0.0" } @@ -241,9 +241,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/engine.io-parser": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", - "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", "engines": { "node": ">=10.0.0" } @@ -685,19 +685,20 @@ "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" }, "node_modules/socket.io": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", - "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", + "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", + "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.4.1", + "engine.io": "~6.5.2", "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.1" + "socket.io-parser": "~4.2.4" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/socket.io-adapter": { @@ -709,14 +710,14 @@ } }, "node_modules/socket.io-client": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz", - "integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", + "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", - "engine.io-client": "~6.4.0", - "socket.io-parser": "~4.2.1" + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" }, "engines": { "node": ">=10.0.0" @@ -744,9 +745,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/socket.io-parser": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz", - "integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -890,17 +891,17 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "version": "2.8.14", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.14.tgz", + "integrity": "sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ==", "requires": { "@types/node": "*" } }, "@types/node": { - "version": "18.15.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz", - "integrity": "sha512-9avDaQJczATcXgfmMAW3MIWArOO7A+m90vuCFLr8AotWf8igO/mRoYukrk2cqZVtv38tHs33retzHEilM7FpeQ==" + "version": "20.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", + "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==" }, "accepts": { "version": "1.3.8", @@ -1015,9 +1016,9 @@ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "engine.io": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz", - "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2.tgz", + "integrity": "sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==", "requires": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -1027,7 +1028,7 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", + "engine.io-parser": "~5.2.1", "ws": "~8.11.0" }, "dependencies": { @@ -1052,13 +1053,13 @@ } }, "engine.io-client": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz", - "integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz", + "integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==", "requires": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", + "engine.io-parser": "~5.2.1", "ws": "~8.11.0", "xmlhttprequest-ssl": "~2.0.0" }, @@ -1079,9 +1080,9 @@ } }, "engine.io-parser": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", - "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" }, "escape-html": { "version": "1.0.3", @@ -1386,16 +1387,17 @@ "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" }, "socket.io": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", - "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", + "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", "requires": { "accepts": "~1.3.4", "base64id": "~2.0.0", + "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.4.1", + "engine.io": "~6.5.2", "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.1" + "socket.io-parser": "~4.2.4" }, "dependencies": { "debug": { @@ -1422,14 +1424,14 @@ } }, "socket.io-client": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz", - "integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", + "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", "requires": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", - "engine.io-client": "~6.4.0", - "socket.io-parser": "~4.2.1" + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" }, "dependencies": { "debug": { @@ -1448,9 +1450,9 @@ } }, "socket.io-parser": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz", - "integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "requires": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" diff --git a/examples/server/aiohttp/app.html b/examples/server/aiohttp/app.html index 5cb854df..74d404d7 100644 --- a/examples/server/aiohttp/app.html +++ b/examples/server/aiohttp/app.html @@ -3,7 +3,7 @@ python-socketio test - + + diff --git a/examples/server/aiohttp/latency.html b/examples/server/aiohttp/latency.html index 9c885eb9..b3450a61 100644 --- a/examples/server/aiohttp/latency.html +++ b/examples/server/aiohttp/latency.html @@ -11,7 +11,7 @@

(connecting)

- + - + + diff --git a/examples/server/asgi/latency.html b/examples/server/asgi/latency.html index 9c885eb9..b3450a61 100644 --- a/examples/server/asgi/latency.html +++ b/examples/server/asgi/latency.html @@ -11,7 +11,7 @@

(connecting)

- + - + + diff --git a/examples/server/sanic/latency.html b/examples/server/sanic/latency.html index 9c885eb9..b3450a61 100644 --- a/examples/server/sanic/latency.html +++ b/examples/server/sanic/latency.html @@ -11,7 +11,7 @@

(connecting)

- + - + + diff --git a/examples/server/tornado/templates/latency.html b/examples/server/tornado/templates/latency.html index 9c885eb9..b3450a61 100644 --- a/examples/server/tornado/templates/latency.html +++ b/examples/server/tornado/templates/latency.html @@ -11,7 +11,7 @@

(connecting)

- + - + + diff --git a/examples/server/wsgi/templates/index.html b/examples/server/wsgi/templates/index.html index 5d18f6bb..7c9ae41f 100644 --- a/examples/server/wsgi/templates/index.html +++ b/examples/server/wsgi/templates/index.html @@ -3,10 +3,10 @@ Flask-SocketIO Test - + - + -

Flask-SocketIO Test

+

Python-SocketIO Test

Send:

diff --git a/examples/server/wsgi/templates/index.html b/examples/server/wsgi/templates/index.html index bec1a628..8a7308af 100644 --- a/examples/server/wsgi/templates/index.html +++ b/examples/server/wsgi/templates/index.html @@ -1,7 +1,7 @@ - Flask-SocketIO Test + Python-SocketIO Test -

Flask-SocketIO Test

+

Python-SocketIO Test

Send:

diff --git a/src/socketio/kombu_manager.py b/src/socketio/kombu_manager.py index 0a63bc26..09e260c9 100644 --- a/src/socketio/kombu_manager.py +++ b/src/socketio/kombu_manager.py @@ -86,7 +86,7 @@ def _exchange(self): return kombu.Exchange(self.channel, **options) def _queue(self): - queue_name = 'flask-socketio.' + str(uuid.uuid4()) + queue_name = 'python-socketio.' + str(uuid.uuid4()) options = {'durable': False, 'queue_arguments': {'x-expires': 300000}} options.update(self.queue_options) return kombu.Queue(queue_name, self._exchange(), **options) From c535558a8e2e651f38023d1c570739adee2c6dc6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:59:36 +0100 Subject: [PATCH 463/571] Bump path-to-regexp and express in /examples/client/javascript (#1381) #nolog Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) to 0.1.10 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `path-to-regexp` from 0.1.7 to 0.1.10 - [Release notes](https://github.com/pillarjs/path-to-regexp/releases) - [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md) - [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.7...v0.1.10) Updates `express` from 4.19.2 to 4.20.0 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.20.0) --- updated-dependencies: - dependency-name: path-to-regexp dependency-type: indirect - dependency-name: express dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/client/javascript/package-lock.json | 214 ++++++++++++++----- examples/client/javascript/package.json | 2 +- 2 files changed, 158 insertions(+), 58 deletions(-) diff --git a/examples/client/javascript/package-lock.json b/examples/client/javascript/package-lock.json index d88717a4..6c7f856a 100644 --- a/examples/client/javascript/package-lock.json +++ b/examples/client/javascript/package-lock.json @@ -8,7 +8,7 @@ "name": "socketio-examples", "version": "0.1.0", "dependencies": { - "express": "^4.19.2", + "express": "^4.20.0", "smoothie": "1.19.0", "socket.io": "^4.6.1", "socket.io-client": "^4.6.1" @@ -63,9 +63,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -75,7 +75,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -85,6 +85,20 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -332,36 +346,36 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", + "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -372,6 +386,14 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -534,9 +556,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/methods": { "version": "1.1.2", @@ -598,9 +623,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -625,9 +653,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -702,9 +730,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -730,9 +758,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", + "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -743,6 +771,34 @@ "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -1047,9 +1103,9 @@ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -1059,10 +1115,20 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" + }, + "dependencies": { + "qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "requires": { + "side-channel": "^1.0.6" + } + } } }, "bytes": { @@ -1245,41 +1311,48 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", + "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } } }, "finalhandler": { @@ -1393,9 +1466,9 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "methods": { "version": "1.1.2", @@ -1436,9 +1509,9 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, "on-finished": { "version": "2.4.1", @@ -1454,9 +1527,9 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "proxy-addr": { "version": "2.0.7", @@ -1502,9 +1575,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -1529,14 +1602,41 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", + "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.18.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + } + } } }, "set-function-length": { diff --git a/examples/client/javascript/package.json b/examples/client/javascript/package.json index 2de1e136..3a173a86 100644 --- a/examples/client/javascript/package.json +++ b/examples/client/javascript/package.json @@ -2,7 +2,7 @@ "name": "socketio-examples", "version": "0.1.0", "dependencies": { - "express": "^4.19.2", + "express": "^4.20.0", "smoothie": "1.19.0", "socket.io": "^4.6.1", "socket.io-client": "^4.6.1" From 3157681c1a30ec37d6ee71261d805a2893c1e16a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:46:37 +0100 Subject: [PATCH 464/571] Bump send and express in /examples/server/javascript (#1385) #nolog Bumps [send](https://github.com/pillarjs/send) to 0.19.0 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `send` from 0.18.0 to 0.19.0 - [Release notes](https://github.com/pillarjs/send/releases) - [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md) - [Commits](https://github.com/pillarjs/send/compare/0.18.0...0.19.0) Updates `express` from 4.19.2 to 4.20.0 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.20.0) --- updated-dependencies: - dependency-name: send dependency-type: indirect - dependency-name: express dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/javascript/package-lock.json | 238 ++++++++++++++----- examples/server/javascript/package.json | 2 +- 2 files changed, 182 insertions(+), 58 deletions(-) diff --git a/examples/server/javascript/package-lock.json b/examples/server/javascript/package-lock.json index 44349e94..8211e39b 100644 --- a/examples/server/javascript/package-lock.json +++ b/examples/server/javascript/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@socket.io/admin-ui": "^0.5.1", - "express": "^4.19.2", + "express": "^4.20.0", "smoothie": "1.19.0", "socket.io": "^4.6.1", "socket.io-client": "^4.6.1" @@ -87,9 +87,9 @@ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -99,7 +99,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -122,6 +122,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -335,36 +349,36 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", + "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -383,6 +397,14 @@ "ms": "2.0.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -563,9 +585,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/methods": { "version": "1.1.2", @@ -627,9 +652,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -654,9 +682,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -731,9 +759,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -772,9 +800,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", + "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -785,6 +813,47 @@ "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-static/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/serve-static/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -1025,9 +1094,9 @@ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -1037,7 +1106,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -1055,6 +1124,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "requires": { + "side-channel": "^1.0.6" + } } } }, @@ -1210,36 +1287,36 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", + "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.2.0", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -1255,6 +1332,11 @@ "ms": "2.0.0" } }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1388,9 +1470,9 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "methods": { "version": "1.1.2", @@ -1431,9 +1513,9 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, "on-finished": { "version": "2.4.1", @@ -1449,9 +1531,9 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "proxy-addr": { "version": "2.0.7", @@ -1497,9 +1579,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -1539,14 +1621,56 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", + "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.18.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + } + } } }, "set-function-length": { diff --git a/examples/server/javascript/package.json b/examples/server/javascript/package.json index 94aa21d7..04d6abad 100644 --- a/examples/server/javascript/package.json +++ b/examples/server/javascript/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "dependencies": { "@socket.io/admin-ui": "^0.5.1", - "express": "^4.19.2", + "express": "^4.20.0", "smoothie": "1.19.0", "socket.io": "^4.6.1", "socket.io-client": "^4.6.1" From 98f94f465bd7217398a88dc0f0d37bb7ad9602a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:46:56 +0100 Subject: [PATCH 465/571] Bump serve-static and express in /examples/server/javascript (#1386) #nolog Bumps [serve-static](https://github.com/expressjs/serve-static) to 1.16.0 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `serve-static` from 1.15.0 to 1.16.0 - [Release notes](https://github.com/expressjs/serve-static/releases) - [Changelog](https://github.com/expressjs/serve-static/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/serve-static/compare/v1.15.0...1.16.0) Updates `express` from 4.19.2 to 4.20.0 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.20.0) --- updated-dependencies: - dependency-name: serve-static dependency-type: indirect - dependency-name: express dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From aaab78a47923c8b645ee2f116494a24b7033c7e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:47:14 +0100 Subject: [PATCH 466/571] Bump path-to-regexp and express in /examples/server/javascript (#1383) #nolog Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) to 0.1.10 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `path-to-regexp` from 0.1.7 to 0.1.10 - [Release notes](https://github.com/pillarjs/path-to-regexp/releases) - [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md) - [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.7...v0.1.10) Updates `express` from 4.19.2 to 4.20.0 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.20.0) --- updated-dependencies: - dependency-name: path-to-regexp dependency-type: indirect - dependency-name: express dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 664acee9ef081ceef98d9ff2a292ac3233f6d210 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:47:31 +0100 Subject: [PATCH 467/571] Bump body-parser and express in /examples/server/javascript (#1384) #nolog Bumps [body-parser](https://github.com/expressjs/body-parser) to 1.20.3 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `body-parser` from 1.20.2 to 1.20.3 - [Release notes](https://github.com/expressjs/body-parser/releases) - [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3) Updates `express` from 4.19.2 to 4.20.0 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.20.0) --- updated-dependencies: - dependency-name: body-parser dependency-type: indirect - dependency-name: express dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From a9b02a5a65d8e72f3a812037afd361b06aaaa040 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:00:16 +0100 Subject: [PATCH 468/571] Bump django in /examples/server/wsgi/django_socketio (#1392) #nolog Bumps [django](https://github.com/django/django) from 4.2.15 to 4.2.16. - [Commits](https://github.com/django/django/compare/4.2.15...4.2.16) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/django_socketio/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/django_socketio/requirements.txt b/examples/server/wsgi/django_socketio/requirements.txt index a247dbf8..53a28118 100644 --- a/examples/server/wsgi/django_socketio/requirements.txt +++ b/examples/server/wsgi/django_socketio/requirements.txt @@ -1,6 +1,6 @@ asgiref==3.6.0 bidict==0.22.1 -Django==4.2.15 +Django==4.2.16 gunicorn==22.0.0 h11==0.14.0 python-engineio From 694d0a294ce761f7ae568025fc93b224ca98711a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:03:00 +0100 Subject: [PATCH 469/571] Bump send and express in /examples/client/javascript (#1393) #nolog Bumps [send](https://github.com/pillarjs/send) to 0.19.0 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `send` from 0.18.0 to 0.19.0 - [Release notes](https://github.com/pillarjs/send/releases) - [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md) - [Commits](https://github.com/pillarjs/send/compare/0.18.0...0.19.0) Updates `express` from 4.20.0 to 4.21.1 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md) - [Commits](https://github.com/expressjs/express/compare/4.20.0...4.21.1) --- updated-dependencies: - dependency-name: send dependency-type: indirect - dependency-name: express dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/client/javascript/package-lock.json | 191 +++++++------------ examples/client/javascript/package.json | 2 +- 2 files changed, 72 insertions(+), 121 deletions(-) diff --git a/examples/client/javascript/package-lock.json b/examples/client/javascript/package-lock.json index 6c7f856a..9b87e499 100644 --- a/examples/client/javascript/package-lock.json +++ b/examples/client/javascript/package-lock.json @@ -8,7 +8,7 @@ "name": "socketio-examples", "version": "0.1.0", "dependencies": { - "express": "^4.20.0", + "express": "^4.21.1", "smoothie": "1.19.0", "socket.io": "^4.6.1", "socket.io-client": "^4.6.1" @@ -85,20 +85,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -145,9 +131,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -346,23 +332,23 @@ } }, "node_modules/express": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", - "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", @@ -371,11 +357,11 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", - "serve-static": "1.16.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -395,12 +381,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -411,6 +397,14 @@ "node": ">= 0.8" } }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -670,11 +664,11 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -758,45 +752,25 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serve-static": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", - "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/serve-static/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static/node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { - "node": ">= 0.8.0" + "node": ">= 0.8" } }, "node_modules/set-function-length": { @@ -1119,16 +1093,6 @@ "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" - }, - "dependencies": { - "qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "requires": { - "side-channel": "^1.0.6" - } - } } }, "bytes": { @@ -1162,9 +1126,9 @@ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" }, "cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" }, "cookie-signature": { "version": "1.0.6", @@ -1311,23 +1275,23 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", - "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", @@ -1336,11 +1300,11 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", - "serve-static": "1.16.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -1356,17 +1320,24 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } } }, "forwarded": { @@ -1541,11 +1512,11 @@ } }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "range-parser": { @@ -1602,40 +1573,20 @@ } }, "serve-static": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", - "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "dependencies": { - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - } + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" } } }, diff --git a/examples/client/javascript/package.json b/examples/client/javascript/package.json index 3a173a86..a9bc3402 100644 --- a/examples/client/javascript/package.json +++ b/examples/client/javascript/package.json @@ -2,7 +2,7 @@ "name": "socketio-examples", "version": "0.1.0", "dependencies": { - "express": "^4.20.0", + "express": "^4.21.1", "smoothie": "1.19.0", "socket.io": "^4.6.1", "socket.io-client": "^4.6.1" From 42da5d2f5426e812fd37d4cabcb9277810cae9c1 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Thu, 10 Oct 2024 23:59:41 +0100 Subject: [PATCH 470/571] Add Python 3.13 CI builds --- .github/workflows/tests.yml | 2 +- tox.ini | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index efb0b628..9d2f4759 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: [windows-latest, macos-latest, ubuntu-latest] - python: ['pypy-3.10', '3.8', '3.9', '3.10', '3.11', '3.12'] + python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.10'] exclude: # pypy3 currently fails to run on Windows - os: windows-latest diff --git a/tox.ini b/tox.ini index 12deda1c..6d809d40 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=flake8,py{38,39,310,311,312,py3},docs +envlist=flake8,py{38,39,310,311,312,313},docs skip_missing_interpreters=True [gh-actions] @@ -9,6 +9,7 @@ python = 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 pypy-3: pypy3 [testenv] From e2237077de025e8bff6772ba1c56906ad03695e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 13 Oct 2024 15:12:07 +0100 Subject: [PATCH 471/571] Bump cookie, socket.io and express in /examples/server/javascript (#1395) #nolog Bumps [cookie](https://github.com/jshttp/cookie) to 0.7.1 and updates ancestor dependencies [cookie](https://github.com/jshttp/cookie), [socket.io](https://github.com/socketio/socket.io) and [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `cookie` from 0.6.0 to 0.7.1 - [Release notes](https://github.com/jshttp/cookie/releases) - [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1) Updates `socket.io` from 4.7.2 to 4.8.0 - [Release notes](https://github.com/socketio/socket.io/releases) - [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md) - [Commits](https://github.com/socketio/socket.io/compare/4.7.2...socket.io@4.8.0) Updates `express` from 4.20.0 to 4.21.1 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md) - [Commits](https://github.com/expressjs/express/compare/4.20.0...4.21.1) --- updated-dependencies: - dependency-name: cookie dependency-type: indirect - dependency-name: socket.io dependency-type: direct:production - dependency-name: express dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/javascript/package-lock.json | 299 ++++++++----------- examples/server/javascript/package.json | 4 +- 2 files changed, 121 insertions(+), 182 deletions(-) diff --git a/examples/server/javascript/package-lock.json b/examples/server/javascript/package-lock.json index 8211e39b..befb6751 100644 --- a/examples/server/javascript/package-lock.json +++ b/examples/server/javascript/package-lock.json @@ -9,9 +9,9 @@ "version": "0.1.0", "dependencies": { "@socket.io/admin-ui": "^0.5.1", - "express": "^4.20.0", + "express": "^4.21.1", "smoothie": "1.19.0", - "socket.io": "^4.6.1", + "socket.io": "^4.8.0", "socket.io-client": "^4.6.1" } }, @@ -44,17 +44,20 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "node_modules/@types/cors": { - "version": "2.8.14", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.14.tgz", - "integrity": "sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { - "version": "20.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", - "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==" + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dependencies": { + "undici-types": "~6.19.2" + } }, "node_modules/accepts": { "version": "1.3.8", @@ -122,20 +125,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -182,9 +171,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -269,16 +258,16 @@ } }, "node_modules/engine.io": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", - "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", @@ -309,9 +298,9 @@ } }, "node_modules/engine.io/node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "engines": { "node": ">= 0.6" } @@ -349,23 +338,23 @@ } }, "node_modules/express": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", - "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", @@ -374,11 +363,11 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", - "serve-static": "1.16.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -411,12 +400,12 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -435,6 +424,14 @@ "ms": "2.0.0" } }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -699,11 +696,11 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -800,58 +797,25 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serve-static": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", - "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/serve-static/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-static/node_modules/debug/node_modules/ms": { + "node_modules/serve-static/node_modules/encodeurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/serve-static/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static/node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { - "node": ">= 0.8.0" + "node": ">= 0.8" } }, "node_modules/set-function-length": { @@ -898,15 +862,15 @@ "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" }, "node_modules/socket.io": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", - "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.5.2", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" }, @@ -977,6 +941,11 @@ "node": ">= 0.6" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1057,17 +1026,20 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "@types/cors": { - "version": "2.8.14", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.14.tgz", - "integrity": "sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "requires": { "@types/node": "*" } }, "@types/node": { - "version": "20.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", - "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==" + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "requires": { + "undici-types": "~6.19.2" + } }, "accepts": { "version": "1.3.8", @@ -1124,14 +1096,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "requires": { - "side-channel": "^1.0.6" - } } } }, @@ -1166,9 +1130,9 @@ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" }, "cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" }, "cookie-signature": { "version": "1.0.6", @@ -1223,16 +1187,16 @@ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "engine.io": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", - "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "requires": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", @@ -1240,9 +1204,9 @@ }, "dependencies": { "cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" } } }, @@ -1287,23 +1251,23 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz", - "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", @@ -1312,11 +1276,11 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", - "serve-static": "1.16.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -1345,12 +1309,12 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -1366,6 +1330,11 @@ "ms": "2.0.0" } }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1545,11 +1514,11 @@ } }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "range-parser": { @@ -1621,55 +1590,20 @@ } }, "serve-static": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz", - "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - } + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" } } }, @@ -1708,15 +1642,15 @@ "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" }, "socket.io": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", - "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", "requires": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.5.2", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } @@ -1769,6 +1703,11 @@ "mime-types": "~2.1.24" } }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/examples/server/javascript/package.json b/examples/server/javascript/package.json index 04d6abad..b93cdb63 100644 --- a/examples/server/javascript/package.json +++ b/examples/server/javascript/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "dependencies": { "@socket.io/admin-ui": "^0.5.1", - "express": "^4.20.0", + "express": "^4.21.1", "smoothie": "1.19.0", - "socket.io": "^4.6.1", + "socket.io": "^4.8.0", "socket.io-client": "^4.6.1" } } From d2059c1b8f628aef5931f6a5218f26a7ac0369cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 13 Oct 2024 15:12:25 +0100 Subject: [PATCH 472/571] Bump cookie and socket.io in /examples/client/javascript (#1394) #nolog Bumps [cookie](https://github.com/jshttp/cookie) to 0.7.1 and updates ancestor dependency [socket.io](https://github.com/socketio/socket.io). These dependencies need to be updated together. Updates `cookie` from 0.4.2 to 0.7.1 - [Release notes](https://github.com/jshttp/cookie/releases) - [Commits](https://github.com/jshttp/cookie/compare/v0.4.2...v0.7.1) Updates `socket.io` from 4.7.2 to 4.8.0 - [Release notes](https://github.com/socketio/socket.io/releases) - [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md) - [Commits](https://github.com/socketio/socket.io/compare/4.7.2...socket.io@4.8.0) --- updated-dependencies: - dependency-name: cookie dependency-type: indirect - dependency-name: socket.io dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/client/javascript/package-lock.json | 114 +++++++++++-------- examples/client/javascript/package.json | 2 +- 2 files changed, 66 insertions(+), 50 deletions(-) diff --git a/examples/client/javascript/package-lock.json b/examples/client/javascript/package-lock.json index 9b87e499..ca02f7af 100644 --- a/examples/client/javascript/package-lock.json +++ b/examples/client/javascript/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "express": "^4.21.1", "smoothie": "1.19.0", - "socket.io": "^4.6.1", + "socket.io": "^4.8.0", "socket.io-client": "^4.6.1" } }, @@ -25,17 +25,20 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "node_modules/@types/cors": { - "version": "2.8.14", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.14.tgz", - "integrity": "sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { - "version": "20.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", - "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==" + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dependencies": { + "undici-types": "~6.19.2" + } }, "node_modules/accepts": { "version": "1.3.8", @@ -210,16 +213,16 @@ } }, "node_modules/engine.io": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", - "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", @@ -271,19 +274,19 @@ } }, "node_modules/engine.io/node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "engines": { "node": ">= 0.6" } }, "node_modules/engine.io/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -295,9 +298,9 @@ } }, "node_modules/engine.io/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/es-define-property": { "version": "1.0.0", @@ -817,15 +820,15 @@ "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" }, "node_modules/socket.io": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", - "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.5.2", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" }, @@ -980,6 +983,11 @@ "node": ">= 0.6" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1045,17 +1053,20 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "@types/cors": { - "version": "2.8.14", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.14.tgz", - "integrity": "sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "requires": { "@types/node": "*" } }, "@types/node": { - "version": "20.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", - "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==" + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "requires": { + "undici-types": "~6.19.2" + } }, "accepts": { "version": "1.3.8", @@ -1183,16 +1194,16 @@ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "engine.io": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", - "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "requires": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", @@ -1200,22 +1211,22 @@ }, "dependencies": { "cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" } } }, @@ -1625,15 +1636,15 @@ "integrity": "sha512-DHH09adx8ltbo/8udr52RcOXggH7HTe0dPmFvTx9iShBl8QAr/WHogup4pU4hCEFWswus8cwNcP7KhTpH5ftCw==" }, "socket.io": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", - "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", "requires": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.5.2", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" }, @@ -1746,6 +1757,11 @@ "mime-types": "~2.1.24" } }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/examples/client/javascript/package.json b/examples/client/javascript/package.json index a9bc3402..6e806da3 100644 --- a/examples/client/javascript/package.json +++ b/examples/client/javascript/package.json @@ -4,7 +4,7 @@ "dependencies": { "express": "^4.21.1", "smoothie": "1.19.0", - "socket.io": "^4.6.1", + "socket.io": "^4.8.0", "socket.io-client": "^4.6.1" } } From 72d37ea79f4cf6076591782e4781fd4868a7e0d6 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 9 Nov 2024 18:40:38 -0500 Subject: [PATCH 473/571] Fix typo with `AsyncClient.connect` example (#1403) --- src/socketio/async_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/socketio/async_client.py b/src/socketio/async_client.py index 5fd8daaf..e79fce0e 100644 --- a/src/socketio/async_client.py +++ b/src/socketio/async_client.py @@ -116,7 +116,7 @@ async def connect(self, url, headers={}, auth=None, transports=None, Example usage:: sio = socketio.AsyncClient() - sio.connect('http://localhost:5000') + await sio.connect('http://localhost:5000') """ if self.connected: raise exceptions.ConnectionError('Already connected') From 03b445a7cebf8c4788039ebd94f3d69aa5586c62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:27:48 +0000 Subject: [PATCH 474/571] Bump aiohttp from 3.10.2 to 3.10.11 in /examples/server/aiohttp (#1406) #nolog Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.2 to 3.10.11. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.2...v3.10.11) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/aiohttp/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/aiohttp/requirements.txt b/examples/server/aiohttp/requirements.txt index 6f19551e..2bda3cff 100644 --- a/examples/server/aiohttp/requirements.txt +++ b/examples/server/aiohttp/requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.10.2 +aiohttp==3.10.11 async-timeout==1.1.0 chardet==2.3.0 multidict==2.1.4 From 46c6a70046b340fcf6a564bbc81754a0d086e030 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 23 Nov 2024 09:23:02 +0000 Subject: [PATCH 475/571] Bump tornado from 6.4.1 to 6.4.2 in /examples/server/tornado (#1408) #nolog Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.4.1 to 6.4.2. - [Changelog](https://github.com/tornadoweb/tornado/blob/v6.4.2/docs/releases.rst) - [Commits](https://github.com/tornadoweb/tornado/compare/v6.4.1...v6.4.2) --- updated-dependencies: - dependency-name: tornado dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/tornado/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/tornado/requirements.txt b/examples/server/tornado/requirements.txt index 9719ca24..32346eff 100644 --- a/examples/server/tornado/requirements.txt +++ b/examples/server/tornado/requirements.txt @@ -1,4 +1,4 @@ -tornado==6.4.1 +tornado==6.4.2 python-engineio python_socketio six==1.10.0 From abf336e108b01f44afb473eb86c1dece6360195c Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 24 Nov 2024 19:55:15 +0000 Subject: [PATCH 476/571] Removed dependency on unittest.TestCase base class --- tests/async/test_admin.py | 40 ++++++++++++++--------------- tests/async/test_client.py | 3 +-- tests/async/test_manager.py | 5 ++-- tests/async/test_namespace.py | 5 +--- tests/async/test_pubsub_manager.py | 5 ++-- tests/async/test_server.py | 5 ++-- tests/async/test_simple_client.py | 3 +-- tests/common/test_admin.py | 38 +++++++++++++-------------- tests/common/test_client.py | 3 +-- tests/common/test_manager.py | 5 ++-- tests/common/test_middleware.py | 3 +-- tests/common/test_msgpack_packet.py | 4 +-- tests/common/test_namespace.py | 3 +-- tests/common/test_packet.py | 4 +-- tests/common/test_pubsub_manager.py | 5 ++-- tests/common/test_server.py | 5 ++-- tests/common/test_simple_client.py | 3 +-- 17 files changed, 60 insertions(+), 79 deletions(-) diff --git a/tests/async/test_admin.py b/tests/async/test_admin.py index 4988277f..4f283538 100644 --- a/tests/async/test_admin.py +++ b/tests/async/test_admin.py @@ -2,7 +2,6 @@ import threading import time from unittest import mock -import unittest import pytest try: from engineio.async_socket import AsyncSocket as EngineIOSocket @@ -38,13 +37,13 @@ def connect(sid, environ, auth): pass async def shutdown(): - await instrumented_server.shutdown() + await self.isvr.shutdown() await sio.shutdown() if 'server_stats_interval' not in ikwargs: ikwargs['server_stats_interval'] = 0.25 - instrumented_server = sio.instrument(auth=auth, **ikwargs) + self.isvr = sio.instrument(auth=auth, **ikwargs) server = SocketIOWebServer(sio, on_shutdown=shutdown) server.start() @@ -56,10 +55,11 @@ async def shutdown(): EngineIOSocket.schedule_ping = mock.MagicMock() try: - ret = f(self, instrumented_server, *args, **kwargs) + ret = f(self, *args, **kwargs) finally: server.stop() - instrumented_server.uninstrument() + self.isvr.uninstrument() + self.isvr = None EngineIOSocket.schedule_ping = original_schedule_ping @@ -80,12 +80,12 @@ async def _async_custom_auth(auth): return auth == {'foo': 'bar'} -class TestAsyncAdmin(unittest.TestCase): - def setUp(self): +class TestAsyncAdmin: + def setup_method(self): print('threads at start:', threading.enumerate()) self.thread_count = threading.active_count() - def tearDown(self): + def teardown_method(self): print('threads at end:', threading.enumerate()) assert self.thread_count == threading.active_count() @@ -107,7 +107,7 @@ def test_missing_auth(self): sio.instrument() @with_instrumented_server(auth=False) - def test_admin_connect_with_no_auth(self, isvr): + def test_admin_connect_with_no_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') with socketio.SimpleClient() as admin_client: @@ -115,7 +115,7 @@ def test_admin_connect_with_no_auth(self, isvr): auth={'foo': 'bar'}) @with_instrumented_server(auth={'foo': 'bar'}) - def test_admin_connect_with_dict_auth(self, isvr): + def test_admin_connect_with_dict_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) @@ -131,7 +131,7 @@ def test_admin_connect_with_dict_auth(self, isvr): @with_instrumented_server(auth=[{'foo': 'bar'}, {'u': 'admin', 'p': 'secret'}]) - def test_admin_connect_with_list_auth(self, isvr): + def test_admin_connect_with_list_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) @@ -148,7 +148,7 @@ def test_admin_connect_with_list_auth(self, isvr): namespace='/admin') @with_instrumented_server(auth=_custom_auth) - def test_admin_connect_with_function_auth(self, isvr): + def test_admin_connect_with_function_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) @@ -162,7 +162,7 @@ def test_admin_connect_with_function_auth(self, isvr): namespace='/admin') @with_instrumented_server(auth=_async_custom_auth) - def test_admin_connect_with_async_function_auth(self, isvr): + def test_admin_connect_with_async_function_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) @@ -176,7 +176,7 @@ def test_admin_connect_with_async_function_auth(self, isvr): namespace='/admin') @with_instrumented_server() - def test_admin_connect_only_admin(self, isvr): + def test_admin_connect_only_admin(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') sid = admin_client.sid @@ -201,7 +201,7 @@ def test_admin_connect_only_admin(self, isvr): events['server_stats']['namespaces'] @with_instrumented_server() - def test_admin_connect_with_others(self, isvr): + def test_admin_connect_with_others(self): with socketio.SimpleClient() as client1, \ socketio.SimpleClient() as client2, \ socketio.SimpleClient() as client3, \ @@ -210,12 +210,12 @@ def test_admin_connect_with_others(self, isvr): client1.emit('enter_room', 'room') sid1 = client1.sid - saved_check_for_upgrade = isvr._check_for_upgrade - isvr._check_for_upgrade = AsyncMock() + saved_check_for_upgrade = self.isvr._check_for_upgrade + self.isvr._check_for_upgrade = AsyncMock() client2.connect('http://localhost:8900', namespace='/foo', transports=['polling']) sid2 = client2.sid - isvr._check_for_upgrade = saved_check_for_upgrade + self.isvr._check_for_upgrade = saved_check_for_upgrade client3.connect('http://localhost:8900', namespace='/admin') sid3 = client3.sid @@ -251,7 +251,7 @@ def test_admin_connect_with_others(self, isvr): assert socket['rooms'] == [sid3] @with_instrumented_server(mode='production', read_only=True) - def test_admin_connect_production(self, isvr): + def test_admin_connect_production(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') events = self._expect({'config': 1, 'server_stats': 2}, @@ -272,7 +272,7 @@ def test_admin_connect_production(self, isvr): events['server_stats']['namespaces'] @with_instrumented_server() - def test_admin_features(self, isvr): + def test_admin_features(self): with socketio.SimpleClient() as client1, \ socketio.SimpleClient() as client2, \ socketio.SimpleClient() as admin_client: diff --git a/tests/async/test_client.py b/tests/async/test_client.py index cf99059e..38c690d3 100644 --- a/tests/async/test_client.py +++ b/tests/async/test_client.py @@ -1,5 +1,4 @@ import asyncio -import unittest from unittest import mock import pytest @@ -12,7 +11,7 @@ from .helpers import AsyncMock, _run -class TestAsyncClient(unittest.TestCase): +class TestAsyncClient: def test_is_asyncio_based(self): c = async_client.AsyncClient() assert c.is_asyncio_based() diff --git a/tests/async/test_manager.py b/tests/async/test_manager.py index 83f64752..50d1dd9d 100644 --- a/tests/async/test_manager.py +++ b/tests/async/test_manager.py @@ -1,4 +1,3 @@ -import unittest from unittest import mock from socketio import async_manager @@ -6,8 +5,8 @@ from .helpers import AsyncMock, _run -class TestAsyncManager(unittest.TestCase): - def setUp(self): +class TestAsyncManager: + def setup_method(self): id = 0 def generate_id(): diff --git a/tests/async/test_namespace.py b/tests/async/test_namespace.py index 60430032..873f4791 100644 --- a/tests/async/test_namespace.py +++ b/tests/async/test_namespace.py @@ -1,13 +1,10 @@ -import sys -import unittest from unittest import mock from socketio import async_namespace from .helpers import AsyncMock, _run -@unittest.skipIf(sys.version_info < (3, 5), 'only for Python 3.5+') -class TestAsyncNamespace(unittest.TestCase): +class TestAsyncNamespace: def test_connect_event(self): result = {} diff --git a/tests/async/test_pubsub_manager.py b/tests/async/test_pubsub_manager.py index c7aeb6e9..8a509d23 100644 --- a/tests/async/test_pubsub_manager.py +++ b/tests/async/test_pubsub_manager.py @@ -1,6 +1,5 @@ import asyncio import functools -import unittest from unittest import mock import pytest @@ -11,8 +10,8 @@ from .helpers import AsyncMock, _run -class TestAsyncPubSubManager(unittest.TestCase): - def setUp(self): +class TestAsyncPubSubManager: + def setup_method(self): id = 0 def generate_id(): diff --git a/tests/async/test_server.py b/tests/async/test_server.py index 471e562a..8b2dfe28 100644 --- a/tests/async/test_server.py +++ b/tests/async/test_server.py @@ -1,6 +1,5 @@ import asyncio import logging -import unittest from unittest import mock from engineio import json @@ -18,8 +17,8 @@ @mock.patch('socketio.server.engineio.AsyncServer', **{ 'return_value.generate_id.side_effect': [str(i) for i in range(1, 10)], 'return_value.send_packet': AsyncMock()}) -class TestAsyncServer(unittest.TestCase): - def tearDown(self): +class TestAsyncServer: + def teardown_method(self): # restore JSON encoder, in case a test changed it packet.Packet.json = json diff --git a/tests/async/test_simple_client.py b/tests/async/test_simple_client.py index 08b2ea65..6a2eb7ad 100644 --- a/tests/async/test_simple_client.py +++ b/tests/async/test_simple_client.py @@ -1,5 +1,4 @@ import asyncio -import unittest from unittest import mock import pytest @@ -8,7 +7,7 @@ from .helpers import AsyncMock, _run -class TestAsyncAsyncSimpleClient(unittest.TestCase): +class TestAsyncAsyncSimpleClient: def test_constructor(self): client = AsyncSimpleClient(1, '2', a='3', b=4) assert client.client_args == (1, '2') diff --git a/tests/common/test_admin.py b/tests/common/test_admin.py index 2b2d0164..e7667311 100644 --- a/tests/common/test_admin.py +++ b/tests/common/test_admin.py @@ -2,7 +2,6 @@ import threading import time from unittest import mock -import unittest import pytest from engineio.socket import Socket as EngineIOSocket import socketio @@ -36,7 +35,7 @@ def connect(sid, environ, auth): if 'server_stats_interval' not in ikwargs: ikwargs['server_stats_interval'] = 0.25 - instrumented_server = sio.instrument(auth=auth, **ikwargs) + self.isvr = sio.instrument(auth=auth, **ikwargs) server = SocketIOWebServer(sio) server.start() @@ -48,11 +47,12 @@ def connect(sid, environ, auth): EngineIOSocket.schedule_ping = mock.MagicMock() try: - ret = f(self, instrumented_server, *args, **kwargs) + ret = f(self, *args, **kwargs) finally: server.stop() - instrumented_server.shutdown() - instrumented_server.uninstrument() + self.isvr.shutdown() + self.isvr.uninstrument() + self.isvr = None EngineIOSocket.schedule_ping = original_schedule_ping @@ -69,12 +69,12 @@ def _custom_auth(auth): return auth == {'foo': 'bar'} -class TestAdmin(unittest.TestCase): - def setUp(self): +class TestAdmin: + def setup_method(self): print('threads at start:', threading.enumerate()) self.thread_count = threading.active_count() - def tearDown(self): + def teardown_method(self): print('threads at end:', threading.enumerate()) assert self.thread_count == threading.active_count() @@ -96,7 +96,7 @@ def test_missing_auth(self): sio.instrument() @with_instrumented_server(auth=False) - def test_admin_connect_with_no_auth(self, isvr): + def test_admin_connect_with_no_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') with socketio.SimpleClient() as admin_client: @@ -104,7 +104,7 @@ def test_admin_connect_with_no_auth(self, isvr): auth={'foo': 'bar'}) @with_instrumented_server(auth={'foo': 'bar'}) - def test_admin_connect_with_dict_auth(self, isvr): + def test_admin_connect_with_dict_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) @@ -120,7 +120,7 @@ def test_admin_connect_with_dict_auth(self, isvr): @with_instrumented_server(auth=[{'foo': 'bar'}, {'u': 'admin', 'p': 'secret'}]) - def test_admin_connect_with_list_auth(self, isvr): + def test_admin_connect_with_list_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) @@ -137,7 +137,7 @@ def test_admin_connect_with_list_auth(self, isvr): namespace='/admin') @with_instrumented_server(auth=_custom_auth) - def test_admin_connect_with_function_auth(self, isvr): + def test_admin_connect_with_function_auth(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) @@ -151,7 +151,7 @@ def test_admin_connect_with_function_auth(self, isvr): namespace='/admin') @with_instrumented_server() - def test_admin_connect_only_admin(self, isvr): + def test_admin_connect_only_admin(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') sid = admin_client.sid @@ -176,7 +176,7 @@ def test_admin_connect_only_admin(self, isvr): events['server_stats']['namespaces'] @with_instrumented_server() - def test_admin_connect_with_others(self, isvr): + def test_admin_connect_with_others(self): with socketio.SimpleClient() as client1, \ socketio.SimpleClient() as client2, \ socketio.SimpleClient() as client3, \ @@ -185,12 +185,12 @@ def test_admin_connect_with_others(self, isvr): client1.emit('enter_room', 'room') sid1 = client1.sid - saved_check_for_upgrade = isvr._check_for_upgrade - isvr._check_for_upgrade = mock.MagicMock() + saved_check_for_upgrade = self.isvr._check_for_upgrade + self.isvr._check_for_upgrade = mock.MagicMock() client2.connect('http://localhost:8900', namespace='/foo', transports=['polling']) sid2 = client2.sid - isvr._check_for_upgrade = saved_check_for_upgrade + self.isvr._check_for_upgrade = saved_check_for_upgrade client3.connect('http://localhost:8900', namespace='/admin') sid3 = client3.sid @@ -226,7 +226,7 @@ def test_admin_connect_with_others(self, isvr): assert socket['rooms'] == [sid3] @with_instrumented_server(mode='production', read_only=True) - def test_admin_connect_production(self, isvr): + def test_admin_connect_production(self): with socketio.SimpleClient() as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') events = self._expect({'config': 1, 'server_stats': 2}, @@ -247,7 +247,7 @@ def test_admin_connect_production(self, isvr): events['server_stats']['namespaces'] @with_instrumented_server() - def test_admin_features(self, isvr): + def test_admin_features(self): with socketio.SimpleClient() as client1, \ socketio.SimpleClient() as client2, \ socketio.SimpleClient() as admin_client: diff --git a/tests/common/test_client.py b/tests/common/test_client.py index 52cfc336..c7de4b98 100644 --- a/tests/common/test_client.py +++ b/tests/common/test_client.py @@ -1,6 +1,5 @@ import logging import time -import unittest from unittest import mock from engineio import exceptions as engineio_exceptions @@ -16,7 +15,7 @@ from socketio import packet -class TestClient(unittest.TestCase): +class TestClient: def test_is_asyncio_based(self): c = client.Client() assert not c.is_asyncio_based() diff --git a/tests/common/test_manager.py b/tests/common/test_manager.py index 571d2fdc..65ed1cb2 100644 --- a/tests/common/test_manager.py +++ b/tests/common/test_manager.py @@ -1,4 +1,3 @@ -import unittest from unittest import mock import pytest @@ -7,8 +6,8 @@ from socketio import packet -class TestBaseManager(unittest.TestCase): - def setUp(self): +class TestBaseManager: + def setup_method(self): id = 0 def generate_id(): diff --git a/tests/common/test_middleware.py b/tests/common/test_middleware.py index 8611a041..05795034 100644 --- a/tests/common/test_middleware.py +++ b/tests/common/test_middleware.py @@ -1,10 +1,9 @@ -import unittest from unittest import mock from socketio import middleware -class TestMiddleware(unittest.TestCase): +class TestMiddleware: def test_wsgi_routing(self): mock_wsgi_app = mock.MagicMock() mock_sio_app = 'foo' diff --git a/tests/common/test_msgpack_packet.py b/tests/common/test_msgpack_packet.py index 4930cffb..e0197a27 100644 --- a/tests/common/test_msgpack_packet.py +++ b/tests/common/test_msgpack_packet.py @@ -1,10 +1,8 @@ -import unittest - from socketio import msgpack_packet from socketio import packet -class TestMsgPackPacket(unittest.TestCase): +class TestMsgPackPacket: def test_encode_decode(self): p = msgpack_packet.MsgPackPacket( packet.CONNECT, data={'auth': {'token': '123'}}, namespace='/foo') diff --git a/tests/common/test_namespace.py b/tests/common/test_namespace.py index 7967ceca..8bfa9899 100644 --- a/tests/common/test_namespace.py +++ b/tests/common/test_namespace.py @@ -1,10 +1,9 @@ -import unittest from unittest import mock from socketio import namespace -class TestNamespace(unittest.TestCase): +class TestNamespace: def test_connect_event(self): result = {} diff --git a/tests/common/test_packet.py b/tests/common/test_packet.py index 1dcc8f0a..5682dab0 100644 --- a/tests/common/test_packet.py +++ b/tests/common/test_packet.py @@ -1,11 +1,9 @@ -import unittest - import pytest from socketio import packet -class TestPacket(unittest.TestCase): +class TestPacket: def test_encode_default_packet(self): pkt = packet.Packet() assert pkt.packet_type == packet.EVENT diff --git a/tests/common/test_pubsub_manager.py b/tests/common/test_pubsub_manager.py index 5a4653ec..6d8eda75 100644 --- a/tests/common/test_pubsub_manager.py +++ b/tests/common/test_pubsub_manager.py @@ -1,6 +1,5 @@ import functools import logging -import unittest from unittest import mock import pytest @@ -10,8 +9,8 @@ from socketio import packet -class TestPubSubManager(unittest.TestCase): - def setUp(self): +class TestPubSubManager: + def setup_method(self): id = 0 def generate_id(): diff --git a/tests/common/test_server.py b/tests/common/test_server.py index 33790dc7..e6b02e5a 100644 --- a/tests/common/test_server.py +++ b/tests/common/test_server.py @@ -1,5 +1,4 @@ import logging -import unittest from unittest import mock from engineio import json @@ -15,8 +14,8 @@ @mock.patch('socketio.server.engineio.Server', **{ 'return_value.generate_id.side_effect': [str(i) for i in range(1, 10)]}) -class TestServer(unittest.TestCase): - def tearDown(self): +class TestServer: + def teardown_method(self): # restore JSON encoder, in case a test changed it packet.Packet.json = json diff --git a/tests/common/test_simple_client.py b/tests/common/test_simple_client.py index 3b9f9830..42790573 100644 --- a/tests/common/test_simple_client.py +++ b/tests/common/test_simple_client.py @@ -1,11 +1,10 @@ -import unittest from unittest import mock import pytest from socketio import SimpleClient from socketio.exceptions import SocketIOError, TimeoutError, DisconnectedError -class TestSimpleClient(unittest.TestCase): +class TestSimpleClient: def test_constructor(self): client = SimpleClient(1, '2', a='3', b=4) assert client.client_args == (1, '2') From 8f0e66c1cd1cd63dcef703576cc9cb9c99104df7 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 24 Nov 2024 20:24:13 +0000 Subject: [PATCH 477/571] Adopted unittest.mock.AsyncMock in async unit tests --- tests/async/helpers.py | 12 - tests/async/test_admin.py | 3 +- tests/async/test_client.py | 455 ++++++++++++++--------------- tests/async/test_manager.py | 110 +++---- tests/async/test_namespace.py | 78 ++--- tests/async/test_pubsub_manager.py | 172 ++++++----- tests/async/test_server.py | 240 ++++++++------- tests/async/test_simple_client.py | 40 +-- 8 files changed, 543 insertions(+), 567 deletions(-) diff --git a/tests/async/helpers.py b/tests/async/helpers.py index 09e323c7..c9b708c9 100644 --- a/tests/async/helpers.py +++ b/tests/async/helpers.py @@ -1,16 +1,4 @@ import asyncio -from unittest import mock - - -def AsyncMock(*args, **kwargs): - """Return a mock asynchronous function.""" - m = mock.MagicMock(*args, **kwargs) - - async def mock_coro(*args, **kwargs): - return m(*args, **kwargs) - - mock_coro.mock = m - return mock_coro def _run(coro): diff --git a/tests/async/test_admin.py b/tests/async/test_admin.py index 4f283538..a1cf97c4 100644 --- a/tests/async/test_admin.py +++ b/tests/async/test_admin.py @@ -10,7 +10,6 @@ import socketio from socketio.exceptions import ConnectionError from tests.asyncio_web_server import SocketIOWebServer -from .helpers import AsyncMock def with_instrumented_server(auth=False, **ikwargs): @@ -211,7 +210,7 @@ def test_admin_connect_with_others(self): sid1 = client1.sid saved_check_for_upgrade = self.isvr._check_for_upgrade - self.isvr._check_for_upgrade = AsyncMock() + self.isvr._check_for_upgrade = mock.AsyncMock() client2.connect('http://localhost:8900', namespace='/foo', transports=['polling']) sid2 = client2.sid diff --git a/tests/async/test_client.py b/tests/async/test_client.py index 38c690d3..289abd94 100644 --- a/tests/async/test_client.py +++ b/tests/async/test_client.py @@ -8,7 +8,7 @@ from engineio import exceptions as engineio_exceptions from socketio import exceptions from socketio import packet -from .helpers import AsyncMock, _run +from .helpers import _run class TestAsyncClient: @@ -18,7 +18,7 @@ def test_is_asyncio_based(self): def test_connect(self): c = async_client.AsyncClient() - c.eio.connect = AsyncMock() + c.eio.connect = mock.AsyncMock() _run( c.connect( 'url', @@ -36,7 +36,7 @@ def test_connect(self): assert c.connection_transports == 'transports' assert c.connection_namespaces == ['/foo', '/', '/bar'] assert c.socketio_path == 'path' - c.eio.connect.mock.assert_called_once_with( + c.eio.connect.assert_awaited_once_with( 'url', headers='headers', transports='transports', @@ -48,7 +48,7 @@ async def headers(): return 'headers' c = async_client.AsyncClient() - c.eio.connect = AsyncMock() + c.eio.connect = mock.AsyncMock() _run( c.connect( lambda: 'url', @@ -60,7 +60,7 @@ async def headers(): wait=False, ) ) - c.eio.connect.mock.assert_called_once_with( + c.eio.connect.assert_awaited_once_with( 'url', headers='headers', transports='transports', @@ -69,7 +69,7 @@ async def headers(): def test_connect_one_namespace(self): c = async_client.AsyncClient() - c.eio.connect = AsyncMock() + c.eio.connect = mock.AsyncMock() _run( c.connect( 'url', @@ -85,7 +85,7 @@ def test_connect_one_namespace(self): assert c.connection_transports == 'transports' assert c.connection_namespaces == ['/foo'] assert c.socketio_path == 'path' - c.eio.connect.mock.assert_called_once_with( + c.eio.connect.assert_awaited_once_with( 'url', headers='headers', transports='transports', @@ -94,7 +94,7 @@ def test_connect_one_namespace(self): def test_connect_default_namespaces(self): c = async_client.AsyncClient() - c.eio.connect = AsyncMock() + c.eio.connect = mock.AsyncMock() c.on('foo', mock.MagicMock(), namespace='/foo') c.on('bar', mock.MagicMock(), namespace='/') c.on('baz', mock.MagicMock(), namespace='*') @@ -113,7 +113,7 @@ def test_connect_default_namespaces(self): assert c.connection_namespaces == ['/', '/foo'] or \ c.connection_namespaces == ['/foo', '/'] assert c.socketio_path == 'path' - c.eio.connect.mock.assert_called_once_with( + c.eio.connect.assert_awaited_once_with( 'url', headers='headers', transports='transports', @@ -122,7 +122,7 @@ def test_connect_default_namespaces(self): def test_connect_no_namespaces(self): c = async_client.AsyncClient() - c.eio.connect = AsyncMock() + c.eio.connect = mock.AsyncMock() _run( c.connect( 'url', @@ -137,7 +137,7 @@ def test_connect_no_namespaces(self): assert c.connection_transports == 'transports' assert c.connection_namespaces == ['/'] assert c.socketio_path == 'path' - c.eio.connect.mock.assert_called_once_with( + c.eio.connect.assert_awaited_once_with( 'url', headers='headers', transports='transports', @@ -146,7 +146,7 @@ def test_connect_no_namespaces(self): def test_connect_error(self): c = async_client.AsyncClient() - c.eio.connect = AsyncMock( + c.eio.connect = mock.AsyncMock( side_effect=engineio_exceptions.ConnectionError('foo') ) c.on('foo', mock.MagicMock(), namespace='/foo') @@ -164,7 +164,7 @@ def test_connect_error(self): def test_connect_twice(self): c = async_client.AsyncClient() - c.eio.connect = AsyncMock() + c.eio.connect = mock.AsyncMock() _run( c.connect( 'url', @@ -181,7 +181,7 @@ def test_connect_twice(self): def test_connect_wait_single_namespace(self): c = async_client.AsyncClient() - c.eio.connect = AsyncMock() + c.eio.connect = mock.AsyncMock() c._connect_event = mock.MagicMock() async def mock_connect(): @@ -200,7 +200,7 @@ async def mock_connect(): def test_connect_wait_two_namespaces(self): c = async_client.AsyncClient() - c.eio.connect = AsyncMock() + c.eio.connect = mock.AsyncMock() c._connect_event = mock.MagicMock() async def mock_connect(): @@ -226,8 +226,8 @@ async def mock_connect(): def test_connect_timeout(self): c = async_client.AsyncClient() - c.eio.connect = AsyncMock() - c.disconnect = AsyncMock() + c.eio.connect = mock.AsyncMock() + c.disconnect = mock.AsyncMock() with pytest.raises(exceptions.ConnectionError): _run( c.connect( @@ -236,21 +236,21 @@ def test_connect_timeout(self): wait_timeout=0.01, ) ) - c.disconnect.mock.assert_called_once_with() + c.disconnect.assert_awaited_once_with() def test_wait_no_reconnect(self): c = async_client.AsyncClient() - c.eio.wait = AsyncMock() - c.sleep = AsyncMock() + c.eio.wait = mock.AsyncMock() + c.sleep = mock.AsyncMock() c._reconnect_task = None _run(c.wait()) - c.eio.wait.mock.assert_called_once_with() - c.sleep.mock.assert_called_once_with(1) + c.eio.wait.assert_awaited_once_with() + c.sleep.assert_awaited_once_with(1) def test_wait_reconnect_failed(self): c = async_client.AsyncClient() - c.eio.wait = AsyncMock() - c.sleep = AsyncMock() + c.eio.wait = mock.AsyncMock() + c.sleep = mock.AsyncMock() states = ['disconnected'] async def fake_wait(): @@ -258,13 +258,13 @@ async def fake_wait(): c._reconnect_task = fake_wait() _run(c.wait()) - c.eio.wait.mock.assert_called_once_with() - c.sleep.mock.assert_called_once_with(1) + c.eio.wait.assert_awaited_once_with() + c.sleep.assert_awaited_once_with(1) def test_wait_reconnect_successful(self): c = async_client.AsyncClient() - c.eio.wait = AsyncMock() - c.sleep = AsyncMock() + c.eio.wait = mock.AsyncMock() + c.sleep = mock.AsyncMock() states = ['connected', 'disconnected'] async def fake_wait(): @@ -273,26 +273,26 @@ async def fake_wait(): c._reconnect_task = fake_wait() _run(c.wait()) - assert c.eio.wait.mock.call_count == 2 - assert c.sleep.mock.call_count == 2 + assert c.eio.wait.await_count == 2 + assert c.sleep.await_count == 2 def test_emit_no_arguments(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} - c._send_packet = AsyncMock() + c._send_packet = mock.AsyncMock() _run(c.emit('foo')) expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=None) - assert c._send_packet.mock.call_count == 1 + assert c._send_packet.await_count == 1 assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) def test_emit_one_argument(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} - c._send_packet = AsyncMock() + c._send_packet = mock.AsyncMock() _run(c.emit('foo', 'bar')) expected_packet = packet.Packet( packet.EVENT, @@ -300,16 +300,16 @@ def test_emit_one_argument(self): data=['foo', 'bar'], id=None, ) - assert c._send_packet.mock.call_count == 1 + assert c._send_packet.await_count == 1 assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) def test_emit_one_argument_list(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} - c._send_packet = AsyncMock() + c._send_packet = mock.AsyncMock() _run(c.emit('foo', ['bar', 'baz'])) expected_packet = packet.Packet( packet.EVENT, @@ -317,16 +317,16 @@ def test_emit_one_argument_list(self): data=['foo', ['bar', 'baz']], id=None, ) - assert c._send_packet.mock.call_count == 1 + assert c._send_packet.await_count == 1 assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) def test_emit_two_arguments(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} - c._send_packet = AsyncMock() + c._send_packet = mock.AsyncMock() _run(c.emit('foo', ('bar', 'baz'))) expected_packet = packet.Packet( packet.EVENT, @@ -334,22 +334,22 @@ def test_emit_two_arguments(self): data=['foo', 'bar', 'baz'], id=None, ) - assert c._send_packet.mock.call_count == 1 + assert c._send_packet.await_count == 1 assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) def test_emit_namespace(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1'} - c._send_packet = AsyncMock() + c._send_packet = mock.AsyncMock() _run(c.emit('foo', namespace='/foo')) expected_packet = packet.Packet( packet.EVENT, namespace='/foo', data=['foo'], id=None) - assert c._send_packet.mock.call_count == 1 + assert c._send_packet.await_count == 1 assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) @@ -361,15 +361,15 @@ def test_emit_unknown_namespace(self): def test_emit_with_callback(self): c = async_client.AsyncClient() - c._send_packet = AsyncMock() + c._send_packet = mock.AsyncMock() c._generate_ack_id = mock.MagicMock(return_value=123) c.namespaces = {'/': '1'} _run(c.emit('foo', callback='cb')) expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=123) - assert c._send_packet.mock.call_count == 1 + assert c._send_packet.await_count == 1 assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) c._generate_ack_id.assert_called_once_with('/', 'cb') @@ -377,14 +377,14 @@ def test_emit_with_callback(self): def test_emit_namespace_with_callback(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1'} - c._send_packet = AsyncMock() + c._send_packet = mock.AsyncMock() c._generate_ack_id = mock.MagicMock(return_value=123) _run(c.emit('foo', namespace='/foo', callback='cb')) expected_packet = packet.Packet( packet.EVENT, namespace='/foo', data=['foo'], id=123) - assert c._send_packet.mock.call_count == 1 + assert c._send_packet.await_count == 1 assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) c._generate_ack_id.assert_called_once_with('/foo', 'cb') @@ -392,7 +392,7 @@ def test_emit_namespace_with_callback(self): def test_emit_binary(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} - c._send_packet = AsyncMock() + c._send_packet = mock.AsyncMock() _run(c.emit('foo', b'bar')) expected_packet = packet.Packet( packet.EVENT, @@ -400,16 +400,16 @@ def test_emit_binary(self): data=['foo', b'bar'], id=None, ) - assert c._send_packet.mock.call_count == 1 + assert c._send_packet.await_count == 1 assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) def test_emit_not_binary(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} - c._send_packet = AsyncMock() + c._send_packet = mock.AsyncMock() _run(c.emit('foo', 'bar')) expected_packet = packet.Packet( packet.EVENT, @@ -417,25 +417,25 @@ def test_emit_not_binary(self): data=['foo', 'bar'], id=None, ) - assert c._send_packet.mock.call_count == 1 + assert c._send_packet.await_count == 1 assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) def test_send(self): c = async_client.AsyncClient() - c.emit = AsyncMock() + c.emit = mock.AsyncMock() _run(c.send('data', 'namespace', 'callback')) - c.emit.mock.assert_called_once_with( + c.emit.assert_awaited_once_with( 'message', data='data', namespace='namespace', callback='callback' ) def test_send_with_defaults(self): c = async_client.AsyncClient() - c.emit = AsyncMock() + c.emit = mock.AsyncMock() _run(c.send('data')) - c.emit.mock.assert_called_once_with( + c.emit.assert_awaited_once_with( 'message', data='data', namespace=None, callback=None ) @@ -446,16 +446,16 @@ def test_call(self): async def fake_event_wait(): c._generate_ack_id.call_args_list[0][0][1]('foo', 321) - c._send_packet = AsyncMock() + c._send_packet = mock.AsyncMock() c._generate_ack_id = mock.MagicMock(return_value=123) c.eio = mock.MagicMock() c.eio.create_event.return_value.wait = fake_event_wait assert _run(c.call('foo')) == ('foo', 321) expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=123) - assert c._send_packet.mock.call_count == 1 + assert c._send_packet.await_count == 1 assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) @@ -466,7 +466,7 @@ def test_call_with_timeout(self): async def fake_event_wait(): await asyncio.sleep(1) - c._send_packet = AsyncMock() + c._send_packet = mock.AsyncMock() c._generate_ack_id = mock.MagicMock(return_value=123) c.eio = mock.MagicMock() c.eio.create_event.return_value.wait = fake_event_wait @@ -474,9 +474,9 @@ async def fake_event_wait(): _run(c.call('foo', timeout=0.01)) expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=123) - assert c._send_packet.mock.call_count == 1 + assert c._send_packet.await_count == 1 assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) @@ -484,41 +484,41 @@ def test_disconnect(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/': '1'} - c._trigger_event = AsyncMock() - c._send_packet = AsyncMock() + c._trigger_event = mock.AsyncMock() + c._send_packet = mock.AsyncMock() c.eio = mock.MagicMock() - c.eio.disconnect = AsyncMock() + c.eio.disconnect = mock.AsyncMock() c.eio.state = 'connected' _run(c.disconnect()) assert c.connected - assert c._trigger_event.mock.call_count == 0 - assert c._send_packet.mock.call_count == 1 + assert c._trigger_event.await_count == 0 + assert c._send_packet.await_count == 1 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/') assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) - c.eio.disconnect.mock.assert_called_once_with(abort=True) + c.eio.disconnect.assert_awaited_once_with(abort=True) def test_disconnect_namespaces(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1', '/bar': '2'} - c._trigger_event = AsyncMock() - c._send_packet = AsyncMock() + c._trigger_event = mock.AsyncMock() + c._send_packet = mock.AsyncMock() c.eio = mock.MagicMock() - c.eio.disconnect = AsyncMock() + c.eio.disconnect = mock.AsyncMock() c.eio.state = 'connected' _run(c.disconnect()) - assert c._trigger_event.mock.call_count == 0 - assert c._send_packet.mock.call_count == 2 + assert c._trigger_event.await_count == 0 + assert c._send_packet.await_count == 2 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/foo') assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) expected_packet = packet.Packet(packet.DISCONNECT, namespace='/bar') assert ( - c._send_packet.mock.call_args_list[1][0][0].encode() + c._send_packet.await_args_list[1][0][0].encode() == expected_packet.encode() ) @@ -532,103 +532,103 @@ def test_start_background_task(self): def test_sleep(self): c = async_client.AsyncClient() - c.eio.sleep = AsyncMock() + c.eio.sleep = mock.AsyncMock() _run(c.sleep(1.23)) - c.eio.sleep.mock.assert_called_once_with(1.23) + c.eio.sleep.assert_awaited_once_with(1.23) def test_send_packet(self): c = async_client.AsyncClient() - c.eio.send = AsyncMock() + c.eio.send = mock.AsyncMock() _run(c._send_packet(packet.Packet(packet.EVENT, 'foo'))) - c.eio.send.mock.assert_called_once_with('2"foo"') + c.eio.send.assert_awaited_once_with('2"foo"') def test_send_packet_binary(self): c = async_client.AsyncClient() - c.eio.send = AsyncMock() + c.eio.send = mock.AsyncMock() _run(c._send_packet(packet.Packet(packet.EVENT, b'foo'))) - assert c.eio.send.mock.call_args_list == [ + assert c.eio.send.await_args_list == [ mock.call('51-{"_placeholder":true,"num":0}'), mock.call(b'foo'), - ] or c.eio.send.mock.call_args_list == [ + ] or c.eio.send.await_args_list == [ mock.call('51-{"num":0,"_placeholder":true}'), mock.call(b'foo'), ] def test_send_packet_default_binary(self): c = async_client.AsyncClient() - c.eio.send = AsyncMock() + c.eio.send = mock.AsyncMock() _run(c._send_packet(packet.Packet(packet.EVENT, 'foo'))) - c.eio.send.mock.assert_called_once_with('2"foo"') + c.eio.send.assert_awaited_once_with('2"foo"') def test_handle_connect(self): c = async_client.AsyncClient() c._connect_event = mock.MagicMock() - c._trigger_event = AsyncMock() - c._send_packet = AsyncMock() + c._trigger_event = mock.AsyncMock() + c._send_packet = mock.AsyncMock() _run(c._handle_connect('/', {'sid': '123'})) c._connect_event.set.assert_called_once_with() - c._trigger_event.mock.assert_called_once_with('connect', namespace='/') - c._send_packet.mock.assert_not_called() + c._trigger_event.assert_awaited_once_with('connect', namespace='/') + c._send_packet.assert_not_awaited() def test_handle_connect_with_namespaces(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() - c._trigger_event = AsyncMock() - c._send_packet = AsyncMock() + c._trigger_event = mock.AsyncMock() + c._send_packet = mock.AsyncMock() _run(c._handle_connect('/', {'sid': '3'})) c._connect_event.set.assert_called_once_with() - c._trigger_event.mock.assert_called_once_with('connect', namespace='/') + c._trigger_event.assert_awaited_once_with('connect', namespace='/') assert c.namespaces == {'/': '3', '/foo': '1', '/bar': '2'} def test_handle_connect_namespace(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1'} c._connect_event = mock.MagicMock() - c._trigger_event = AsyncMock() - c._send_packet = AsyncMock() + c._trigger_event = mock.AsyncMock() + c._send_packet = mock.AsyncMock() _run(c._handle_connect('/foo', {'sid': '123'})) _run(c._handle_connect('/bar', {'sid': '2'})) - assert c._trigger_event.mock.call_count == 1 + assert c._trigger_event.await_count == 1 c._connect_event.set.assert_called_once_with() - c._trigger_event.mock.assert_called_once_with( + c._trigger_event.assert_awaited_once_with( 'connect', namespace='/bar') assert c.namespaces == {'/foo': '1', '/bar': '2'} def test_handle_disconnect(self): c = async_client.AsyncClient() c.connected = True - c._trigger_event = AsyncMock() + c._trigger_event = mock.AsyncMock() _run(c._handle_disconnect('/')) - c._trigger_event.mock.assert_any_call( + c._trigger_event.assert_any_await( 'disconnect', namespace='/' ) - c._trigger_event.mock.assert_any_call( + c._trigger_event.assert_any_await( '__disconnect_final', namespace='/' ) assert not c.connected _run(c._handle_disconnect('/')) - assert c._trigger_event.mock.call_count == 2 + assert c._trigger_event.await_count == 2 def test_handle_disconnect_namespace(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} - c._trigger_event = AsyncMock() + c._trigger_event = mock.AsyncMock() _run(c._handle_disconnect('/foo')) - c._trigger_event.mock.assert_any_call( + c._trigger_event.assert_any_await( 'disconnect', namespace='/foo' ) - c._trigger_event.mock.assert_any_call( + c._trigger_event.assert_any_await( '__disconnect_final', namespace='/foo' ) assert c.namespaces == {'/bar': '2'} assert c.connected _run(c._handle_disconnect('/bar')) - c._trigger_event.mock.assert_any_call( + c._trigger_event.assert_any_await( 'disconnect', namespace='/bar' ) - c._trigger_event.mock.assert_any_call( + c._trigger_event.assert_any_await( '__disconnect_final', namespace='/bar' ) assert c.namespaces == {} @@ -638,12 +638,12 @@ def test_handle_disconnect_unknown_namespace(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} - c._trigger_event = AsyncMock() + c._trigger_event = mock.AsyncMock() _run(c._handle_disconnect('/baz')) - c._trigger_event.mock.assert_any_call( + c._trigger_event.assert_any_await( 'disconnect', namespace='/baz' ) - c._trigger_event.mock.assert_any_call( + c._trigger_event.assert_any_await( '__disconnect_final', namespace='/baz' ) assert c.namespaces == {'/foo': '1', '/bar': '2'} @@ -653,83 +653,82 @@ def test_handle_disconnect_default_namespaces(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} - c._trigger_event = AsyncMock() + c._trigger_event = mock.AsyncMock() _run(c._handle_disconnect('/')) - c._trigger_event.mock.assert_any_call('disconnect', namespace='/') - c._trigger_event.mock.assert_any_call('__disconnect_final', - namespace='/') + c._trigger_event.assert_any_await('disconnect', namespace='/') + c._trigger_event.assert_any_await('__disconnect_final', namespace='/') assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected def test_handle_event(self): c = async_client.AsyncClient() - c._trigger_event = AsyncMock() + c._trigger_event = mock.AsyncMock() _run(c._handle_event('/', None, ['foo', ('bar', 'baz')])) - c._trigger_event.mock.assert_called_once_with( + c._trigger_event.assert_awaited_once_with( 'foo', '/', ('bar', 'baz') ) def test_handle_event_with_id_no_arguments(self): c = async_client.AsyncClient() - c._trigger_event = AsyncMock(return_value=None) - c._send_packet = AsyncMock() + c._trigger_event = mock.AsyncMock(return_value=None) + c._send_packet = mock.AsyncMock() _run(c._handle_event('/', 123, ['foo', ('bar', 'baz')])) - c._trigger_event.mock.assert_called_once_with( + c._trigger_event.assert_awaited_once_with( 'foo', '/', ('bar', 'baz') ) - assert c._send_packet.mock.call_count == 1 + assert c._send_packet.await_count == 1 expected_packet = packet.Packet( packet.ACK, namespace='/', id=123, data=[]) assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) def test_handle_event_with_id_one_argument(self): c = async_client.AsyncClient() - c._trigger_event = AsyncMock(return_value='ret') - c._send_packet = AsyncMock() + c._trigger_event = mock.AsyncMock(return_value='ret') + c._send_packet = mock.AsyncMock() _run(c._handle_event('/', 123, ['foo', ('bar', 'baz')])) - c._trigger_event.mock.assert_called_once_with( + c._trigger_event.assert_awaited_once_with( 'foo', '/', ('bar', 'baz') ) - assert c._send_packet.mock.call_count == 1 + assert c._send_packet.await_count == 1 expected_packet = packet.Packet( packet.ACK, namespace='/', id=123, data=['ret']) assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) def test_handle_event_with_id_one_list_argument(self): c = async_client.AsyncClient() - c._trigger_event = AsyncMock(return_value=['a', 'b']) - c._send_packet = AsyncMock() + c._trigger_event = mock.AsyncMock(return_value=['a', 'b']) + c._send_packet = mock.AsyncMock() _run(c._handle_event('/', 123, ['foo', ('bar', 'baz')])) - c._trigger_event.mock.assert_called_once_with( + c._trigger_event.assert_awaited_once_with( 'foo', '/', ('bar', 'baz') ) - assert c._send_packet.mock.call_count == 1 + assert c._send_packet.await_count == 1 expected_packet = packet.Packet( packet.ACK, namespace='/', id=123, data=[['a', 'b']]) assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) def test_handle_event_with_id_two_arguments(self): c = async_client.AsyncClient() - c._trigger_event = AsyncMock(return_value=('a', 'b')) - c._send_packet = AsyncMock() + c._trigger_event = mock.AsyncMock(return_value=('a', 'b')) + c._send_packet = mock.AsyncMock() _run(c._handle_event('/', 123, ['foo', ('bar', 'baz')])) - c._trigger_event.mock.assert_called_once_with( + c._trigger_event.assert_awaited_once_with( 'foo', '/', ('bar', 'baz') ) - assert c._send_packet.mock.call_count == 1 + assert c._send_packet.await_count == 1 expected_packet = packet.Packet( packet.ACK, namespace='/', id=123, data=['a', 'b']) assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) @@ -743,10 +742,10 @@ def test_handle_ack(self): def test_handle_ack_async(self): c = async_client.AsyncClient() - mock_cb = AsyncMock() + mock_cb = mock.AsyncMock() c.callbacks['/foo'] = {123: mock_cb} _run(c._handle_ack('/foo', 123, ['bar', 'baz'])) - mock_cb.mock.assert_called_once_with('bar', 'baz') + mock_cb.assert_awaited_once_with('bar', 'baz') assert 123 not in c.callbacks['/foo'] def test_handle_ack_not_found(self): @@ -761,13 +760,13 @@ def test_handle_error(self): c = async_client.AsyncClient() c.connected = True c._connect_event = mock.MagicMock() - c._trigger_event = AsyncMock() + c._trigger_event = mock.AsyncMock() c.namespaces = {'/foo': '1', '/bar': '2'} _run(c._handle_error('/', 'error')) assert c.namespaces == {} assert not c.connected c._connect_event.set.assert_called_once_with() - c._trigger_event.mock.assert_called_once_with( + c._trigger_event.assert_awaited_once_with( 'connect_error', '/', 'error' ) @@ -775,25 +774,25 @@ def test_handle_error_with_no_arguments(self): c = async_client.AsyncClient() c.connected = True c._connect_event = mock.MagicMock() - c._trigger_event = AsyncMock() + c._trigger_event = mock.AsyncMock() c.namespaces = {'/foo': '1', '/bar': '2'} _run(c._handle_error('/', None)) assert c.namespaces == {} assert not c.connected c._connect_event.set.assert_called_once_with() - c._trigger_event.mock.assert_called_once_with('connect_error', '/') + c._trigger_event.assert_awaited_once_with('connect_error', '/') def test_handle_error_namespace(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() - c._trigger_event = AsyncMock() + c._trigger_event = mock.AsyncMock() _run(c._handle_error('/bar', ['error', 'message'])) assert c.namespaces == {'/foo': '1'} assert c.connected c._connect_event.set.assert_called_once_with() - c._trigger_event.mock.assert_called_once_with( + c._trigger_event.assert_awaited_once_with( 'connect_error', '/bar', 'error', 'message' ) @@ -802,12 +801,12 @@ def test_handle_error_namespace_with_no_arguments(self): c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() - c._trigger_event = AsyncMock() + c._trigger_event = mock.AsyncMock() _run(c._handle_error('/bar', None)) assert c.namespaces == {'/foo': '1'} assert c.connected c._connect_event.set.assert_called_once_with() - c._trigger_event.mock.assert_called_once_with('connect_error', '/bar') + c._trigger_event.assert_awaited_once_with('connect_error', '/bar') def test_handle_error_unknown_namespace(self): c = async_client.AsyncClient() @@ -833,14 +832,14 @@ def test_trigger_event(self): def test_trigger_event_namespace(self): c = async_client.AsyncClient() - handler = AsyncMock() - catchall_handler = AsyncMock() + handler = mock.AsyncMock() + catchall_handler = mock.AsyncMock() c.on('foo', handler, namespace='/bar') c.on('*', catchall_handler, namespace='/bar') _run(c._trigger_event('foo', '/bar', 1, '2')) _run(c._trigger_event('bar', '/bar', 1, '2', 3)) - handler.mock.assert_called_once_with(1, '2') - catchall_handler.mock.assert_called_once_with('bar', 1, '2', 3) + handler.assert_awaited_once_with(1, '2') + catchall_handler.assert_awaited_once_with('bar', 1, '2', 3) def test_trigger_event_class_namespace(self): c = async_client.AsyncClient() @@ -902,19 +901,19 @@ def on_foo(self, a, b): @mock.patch( 'asyncio.wait_for', - new_callable=AsyncMock, + new_callable=mock.AsyncMock, side_effect=asyncio.TimeoutError, ) @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) def test_handle_reconnect(self, random, wait_for): c = async_client.AsyncClient() c._reconnect_task = 'foo' - c.connect = AsyncMock( + c.connect = mock.AsyncMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) _run(c._handle_reconnect()) - assert wait_for.mock.call_count == 3 - assert [x[0][1] for x in asyncio.wait_for.mock.call_args_list] == [ + assert wait_for.await_count == 3 + assert [x[0][1] for x in asyncio.wait_for.await_args_list] == [ 1.5, 1.5, 4.0, @@ -923,19 +922,19 @@ def test_handle_reconnect(self, random, wait_for): @mock.patch( 'asyncio.wait_for', - new_callable=AsyncMock, + new_callable=mock.AsyncMock, side_effect=asyncio.TimeoutError, ) @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) def test_handle_reconnect_max_delay(self, random, wait_for): c = async_client.AsyncClient(reconnection_delay_max=3) c._reconnect_task = 'foo' - c.connect = AsyncMock( + c.connect = mock.AsyncMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) _run(c._handle_reconnect()) - assert wait_for.mock.call_count == 3 - assert [x[0][1] for x in asyncio.wait_for.mock.call_args_list] == [ + assert wait_for.await_count == 3 + assert [x[0][1] for x in asyncio.wait_for.await_args_list] == [ 1.5, 1.5, 3.0, @@ -944,7 +943,7 @@ def test_handle_reconnect_max_delay(self, random, wait_for): @mock.patch( 'asyncio.wait_for', - new_callable=AsyncMock, + new_callable=mock.AsyncMock, side_effect=asyncio.TimeoutError, ) @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) @@ -952,23 +951,23 @@ def test_handle_reconnect_max_attempts(self, random, wait_for): c = async_client.AsyncClient(reconnection_attempts=2, logger=True) c.connection_namespaces = ['/'] c._reconnect_task = 'foo' - c._trigger_event = AsyncMock() - c.connect = AsyncMock( + c._trigger_event = mock.AsyncMock() + c.connect = mock.AsyncMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) _run(c._handle_reconnect()) - assert wait_for.mock.call_count == 2 - assert [x[0][1] for x in asyncio.wait_for.mock.call_args_list] == [ + assert wait_for.await_count == 2 + assert [x[0][1] for x in asyncio.wait_for.await_args_list] == [ 1.5, 1.5, ] assert c._reconnect_task == 'foo' - c._trigger_event.mock.assert_called_once_with('__disconnect_final', - namespace='/') + c._trigger_event.assert_awaited_once_with('__disconnect_final', + namespace='/') @mock.patch( 'asyncio.wait_for', - new_callable=AsyncMock, + new_callable=mock.AsyncMock, side_effect=[asyncio.TimeoutError, None], ) @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) @@ -976,59 +975,59 @@ def test_handle_reconnect_aborted(self, random, wait_for): c = async_client.AsyncClient(logger=True) c.connection_namespaces = ['/'] c._reconnect_task = 'foo' - c._trigger_event = AsyncMock() - c.connect = AsyncMock( + c._trigger_event = mock.AsyncMock() + c.connect = mock.AsyncMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) _run(c._handle_reconnect()) - assert wait_for.mock.call_count == 2 - assert [x[0][1] for x in asyncio.wait_for.mock.call_args_list] == [ + assert wait_for.await_count == 2 + assert [x[0][1] for x in asyncio.wait_for.await_args_list] == [ 1.5, 1.5, ] assert c._reconnect_task == 'foo' - c._trigger_event.mock.assert_called_once_with('__disconnect_final', - namespace='/') + c._trigger_event.assert_awaited_once_with('__disconnect_final', + namespace='/') def test_shutdown_disconnect(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/': '1'} - c._trigger_event = AsyncMock() - c._send_packet = AsyncMock() + c._trigger_event = mock.AsyncMock() + c._send_packet = mock.AsyncMock() c.eio = mock.MagicMock() - c.eio.disconnect = AsyncMock() + c.eio.disconnect = mock.AsyncMock() c.eio.state = 'connected' _run(c.shutdown()) - assert c._trigger_event.mock.call_count == 0 - assert c._send_packet.mock.call_count == 1 + assert c._trigger_event.await_count == 0 + assert c._send_packet.await_count == 1 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/') assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) - c.eio.disconnect.mock.assert_called_once_with(abort=True) + c.eio.disconnect.assert_awaited_once_with(abort=True) def test_shutdown_disconnect_namespaces(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} - c._trigger_event = AsyncMock() - c._send_packet = AsyncMock() + c._trigger_event = mock.AsyncMock() + c._send_packet = mock.AsyncMock() c.eio = mock.MagicMock() - c.eio.disconnect = AsyncMock() + c.eio.disconnect = mock.AsyncMock() c.eio.state = 'connected' _run(c.shutdown()) - assert c._trigger_event.mock.call_count == 0 - assert c._send_packet.mock.call_count == 2 + assert c._trigger_event.await_count == 0 + assert c._send_packet.await_count == 2 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/foo') assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) expected_packet = packet.Packet(packet.DISCONNECT, namespace='/bar') assert ( - c._send_packet.mock.call_args_list[1][0][0].encode() + c._send_packet.await_args_list[1][0][0].encode() == expected_packet.encode() ) @@ -1036,9 +1035,9 @@ def test_shutdown_disconnect_namespaces(self): def test_shutdown_reconnect(self, random): c = async_client.AsyncClient() c.connection_namespaces = ['/'] - c._reconnect_task = AsyncMock()() - c._trigger_event = AsyncMock() - c.connect = AsyncMock(side_effect=exceptions.ConnectionError) + c._reconnect_task = mock.AsyncMock()() + c._trigger_event = mock.AsyncMock() + c.connect = mock.AsyncMock(side_effect=exceptions.ConnectionError) async def r(): task = c.start_background_task(c._handle_reconnect) @@ -1047,29 +1046,29 @@ async def r(): await task _run(r()) - c._trigger_event.mock.assert_called_once_with('__disconnect_final', - namespace='/') + c._trigger_event.assert_awaited_once_with('__disconnect_final', + namespace='/') def test_handle_eio_connect(self): c = async_client.AsyncClient() c.connection_namespaces = ['/', '/foo'] c.connection_auth = 'auth' - c._send_packet = AsyncMock() + c._send_packet = mock.AsyncMock() c.eio.sid = 'foo' assert c.sid is None _run(c._handle_eio_connect()) assert c.sid == 'foo' - assert c._send_packet.mock.call_count == 2 + assert c._send_packet.await_count == 2 expected_packet = packet.Packet( packet.CONNECT, data='auth', namespace='/') assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) expected_packet = packet.Packet( packet.CONNECT, data='auth', namespace='/foo') assert ( - c._send_packet.mock.call_args_list[1][0][0].encode() + c._send_packet.await_args_list[1][0][0].encode() == expected_packet.encode() ) @@ -1077,59 +1076,59 @@ def test_handle_eio_connect_function(self): c = async_client.AsyncClient() c.connection_namespaces = ['/', '/foo'] c.connection_auth = lambda: 'auth' - c._send_packet = AsyncMock() + c._send_packet = mock.AsyncMock() c.eio.sid = 'foo' assert c.sid is None _run(c._handle_eio_connect()) assert c.sid == 'foo' - assert c._send_packet.mock.call_count == 2 + assert c._send_packet.await_count == 2 expected_packet = packet.Packet( packet.CONNECT, data='auth', namespace='/') assert ( - c._send_packet.mock.call_args_list[0][0][0].encode() + c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) expected_packet = packet.Packet( packet.CONNECT, data='auth', namespace='/foo') assert ( - c._send_packet.mock.call_args_list[1][0][0].encode() + c._send_packet.await_args_list[1][0][0].encode() == expected_packet.encode() ) def test_handle_eio_message(self): c = async_client.AsyncClient() - c._handle_connect = AsyncMock() - c._handle_disconnect = AsyncMock() - c._handle_event = AsyncMock() - c._handle_ack = AsyncMock() - c._handle_error = AsyncMock() + c._handle_connect = mock.AsyncMock() + c._handle_disconnect = mock.AsyncMock() + c._handle_event = mock.AsyncMock() + c._handle_ack = mock.AsyncMock() + c._handle_error = mock.AsyncMock() _run(c._handle_eio_message('0{"sid":"123"}')) - c._handle_connect.mock.assert_called_with(None, {'sid': '123'}) + c._handle_connect.assert_awaited_with(None, {'sid': '123'}) _run(c._handle_eio_message('0/foo,{"sid":"123"}')) - c._handle_connect.mock.assert_called_with('/foo', {'sid': '123'}) + c._handle_connect.assert_awaited_with('/foo', {'sid': '123'}) _run(c._handle_eio_message('1')) - c._handle_disconnect.mock.assert_called_with(None) + c._handle_disconnect.assert_awaited_with(None) _run(c._handle_eio_message('1/foo')) - c._handle_disconnect.mock.assert_called_with('/foo') + c._handle_disconnect.assert_awaited_with('/foo') _run(c._handle_eio_message('2["foo"]')) - c._handle_event.mock.assert_called_with(None, None, ['foo']) + c._handle_event.assert_awaited_with(None, None, ['foo']) _run(c._handle_eio_message('3/foo,["bar"]')) - c._handle_ack.mock.assert_called_with('/foo', None, ['bar']) + c._handle_ack.assert_awaited_with('/foo', None, ['bar']) _run(c._handle_eio_message('4')) - c._handle_error.mock.assert_called_with(None, None) + c._handle_error.assert_awaited_with(None, None) _run(c._handle_eio_message('4"foo"')) - c._handle_error.mock.assert_called_with(None, 'foo') + c._handle_error.assert_awaited_with(None, 'foo') _run(c._handle_eio_message('4["foo"]')) - c._handle_error.mock.assert_called_with(None, ['foo']) + c._handle_error.assert_awaited_with(None, ['foo']) _run(c._handle_eio_message('4/foo')) - c._handle_error.mock.assert_called_with('/foo', None) + c._handle_error.assert_awaited_with('/foo', None) _run(c._handle_eio_message('4/foo,["foo","bar"]')) - c._handle_error.mock.assert_called_with('/foo', ['foo', 'bar']) + c._handle_error.assert_awaited_with('/foo', ['foo', 'bar']) _run(c._handle_eio_message('51-{"_placeholder":true,"num":0}')) assert c._binary_packet.packet_type == packet.BINARY_EVENT _run(c._handle_eio_message(b'foo')) - c._handle_event.mock.assert_called_with(None, None, b'foo') + c._handle_event.assert_awaited_with(None, None, b'foo') _run( c._handle_eio_message( '62-/foo,{"1":{"_placeholder":true,"num":1},' @@ -1139,7 +1138,7 @@ def test_handle_eio_message(self): assert c._binary_packet.packet_type == packet.BINARY_ACK _run(c._handle_eio_message(b'bar')) _run(c._handle_eio_message(b'foo')) - c._handle_ack.mock.assert_called_with( + c._handle_ack.assert_awaited_with( '/foo', None, {'1': b'foo', '2': b'bar'} ) with pytest.raises(ValueError): @@ -1149,12 +1148,12 @@ def test_eio_disconnect(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} c.connected = True - c._trigger_event = AsyncMock() + c._trigger_event = mock.AsyncMock() c.start_background_task = mock.MagicMock() c.sid = 'foo' c.eio.state = 'connected' _run(c._handle_eio_disconnect()) - c._trigger_event.mock.assert_called_once_with( + c._trigger_event.assert_awaited_once_with( 'disconnect', namespace='/' ) assert c.sid is None @@ -1164,12 +1163,12 @@ def test_eio_disconnect_namespaces(self): c = async_client.AsyncClient(reconnection=False) c.namespaces = {'/foo': '1', '/bar': '2'} c.connected = True - c._trigger_event = AsyncMock() + c._trigger_event = mock.AsyncMock() c.sid = 'foo' c.eio.state = 'connected' _run(c._handle_eio_disconnect()) - c._trigger_event.mock.assert_any_call('disconnect', namespace='/foo') - c._trigger_event.mock.assert_any_call('disconnect', namespace='/bar') + c._trigger_event.assert_any_await('disconnect', namespace='/foo') + c._trigger_event.assert_any_await('disconnect', namespace='/bar') assert c.sid is None assert not c.connected @@ -1191,15 +1190,15 @@ def test_eio_disconnect_no_reconnect(self): c = async_client.AsyncClient(reconnection=False) c.namespaces = {'/': '1'} c.connected = True - c._trigger_event = AsyncMock() + c._trigger_event = mock.AsyncMock() c.start_background_task = mock.MagicMock() c.sid = 'foo' c.eio.state = 'connected' _run(c._handle_eio_disconnect()) - c._trigger_event.mock.assert_any_call( + c._trigger_event.assert_any_await( 'disconnect', namespace='/' ) - c._trigger_event.mock.assert_any_call( + c._trigger_event.assert_any_await( '__disconnect_final', namespace='/' ) assert c.sid is None diff --git a/tests/async/test_manager.py b/tests/async/test_manager.py index 50d1dd9d..5d60e039 100644 --- a/tests/async/test_manager.py +++ b/tests/async/test_manager.py @@ -2,7 +2,7 @@ from socketio import async_manager from socketio import packet -from .helpers import AsyncMock, _run +from .helpers import _run class TestAsyncManager: @@ -15,8 +15,8 @@ def generate_id(): return str(id) mock_server = mock.MagicMock() - mock_server._send_packet = AsyncMock() - mock_server._send_eio_packet = AsyncMock() + mock_server._send_packet = mock.AsyncMock() + mock_server._send_eio_packet = mock.AsyncMock() mock_server.eio.generate_id = generate_id mock_server.packet_class = packet.Packet self.bm = async_manager.AsyncManager() @@ -128,14 +128,14 @@ def test_trigger_sync_callback(self): def test_trigger_async_callback(self): sid1 = _run(self.bm.connect('123', '/')) sid2 = _run(self.bm.connect('123', '/foo')) - cb = AsyncMock() + cb = mock.AsyncMock() id1 = self.bm._generate_ack_id(sid1, cb) id2 = self.bm._generate_ack_id(sid2, cb) _run(self.bm.trigger_callback(sid1, id1, ['foo'])) _run(self.bm.trigger_callback(sid2, id2, ['bar', 'baz'])) - assert cb.mock.call_count == 2 - cb.mock.assert_any_call('foo') - cb.mock.assert_any_call('bar', 'baz') + assert cb.await_count == 2 + cb.assert_any_await('foo') + cb.assert_any_await('bar', 'baz') def test_invalid_callback(self): sid = _run(self.bm.connect('123', '/')) @@ -145,7 +145,7 @@ def test_invalid_callback(self): # these should not raise an exception _run(self.bm.trigger_callback('xxx', id, ['foo'])) _run(self.bm.trigger_callback(sid, id + 1, ['foo'])) - assert cb.mock.call_count == 0 + assert cb.call_count == 0 def test_get_namespaces(self): assert list(self.bm.get_namespaces()) == [] @@ -207,10 +207,10 @@ def test_emit_to_sid(self): 'my event', {'foo': 'bar'}, namespace='/foo', to=sid ) ) - assert self.bm.server._send_eio_packet.mock.call_count == 1 - assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + assert self.bm.server._send_eio_packet.await_count == 1 + assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' - pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_to_room(self): @@ -224,13 +224,13 @@ def test_emit_to_room(self): 'my event', {'foo': 'bar'}, namespace='/foo', room='bar' ) ) - assert self.bm.server._send_eio_packet.mock.call_count == 2 - assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + assert self.bm.server._send_eio_packet.await_count == 2 + assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' - assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][0] \ + assert self.bm.server._send_eio_packet.await_args_list[1][0][0] \ == '456' - pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] - assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][1] \ + pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] + assert self.bm.server._send_eio_packet.await_args_list[1][0][1] \ == pkt assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' @@ -246,17 +246,17 @@ def test_emit_to_rooms(self): self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', room=['bar', 'baz']) ) - assert self.bm.server._send_eio_packet.mock.call_count == 3 - assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + assert self.bm.server._send_eio_packet.await_count == 3 + assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' - assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][0] \ + assert self.bm.server._send_eio_packet.await_args_list[1][0][0] \ == '456' - assert self.bm.server._send_eio_packet.mock.call_args_list[2][0][0] \ + assert self.bm.server._send_eio_packet.await_args_list[2][0][0] \ == '789' - pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] - assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][1] \ + pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] + assert self.bm.server._send_eio_packet.await_args_list[1][0][1] \ == pkt - assert self.bm.server._send_eio_packet.mock.call_args_list[2][0][1] \ + assert self.bm.server._send_eio_packet.await_args_list[2][0][1] \ == pkt assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' @@ -268,17 +268,17 @@ def test_emit_to_all(self): _run(self.bm.connect('789', '/foo')) _run(self.bm.connect('abc', '/bar')) _run(self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo')) - assert self.bm.server._send_eio_packet.mock.call_count == 3 - assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + assert self.bm.server._send_eio_packet.await_count == 3 + assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' - assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][0] \ + assert self.bm.server._send_eio_packet.await_args_list[1][0][0] \ == '456' - assert self.bm.server._send_eio_packet.mock.call_args_list[2][0][0] \ + assert self.bm.server._send_eio_packet.await_args_list[2][0][0] \ == '789' - pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] - assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][1] \ + pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] + assert self.bm.server._send_eio_packet.await_args_list[1][0][1] \ == pkt - assert self.bm.server._send_eio_packet.mock.call_args_list[2][0][1] \ + assert self.bm.server._send_eio_packet.await_args_list[2][0][1] \ == pkt assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' @@ -294,13 +294,13 @@ def test_emit_to_all_skip_one(self): 'my event', {'foo': 'bar'}, namespace='/foo', skip_sid=sid2 ) ) - assert self.bm.server._send_eio_packet.mock.call_count == 2 - assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + assert self.bm.server._send_eio_packet.await_count == 2 + assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' - assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][0] \ + assert self.bm.server._send_eio_packet.await_args_list[1][0][0] \ == '789' - pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] - assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][1] \ + pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] + assert self.bm.server._send_eio_packet.await_args_list[1][0][1] \ == pkt assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' @@ -319,10 +319,10 @@ def test_emit_to_all_skip_two(self): skip_sid=[sid1, sid3], ) ) - assert self.bm.server._send_eio_packet.mock.call_count == 1 - assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + assert self.bm.server._send_eio_packet.await_count == 1 + assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '456' - pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' def test_emit_with_callback(self): @@ -335,10 +335,10 @@ def test_emit_with_callback(self): ) ) self.bm._generate_ack_id.assert_called_once_with(sid, 'cb') - assert self.bm.server._send_packet.mock.call_count == 1 - assert self.bm.server._send_packet.mock.call_args_list[0][0][0] \ + assert self.bm.server._send_packet.await_count == 1 + assert self.bm.server._send_packet.await_args_list[0][0][0] \ == '123' - pkt = self.bm.server._send_packet.mock.call_args_list[0][0][1] + pkt = self.bm.server._send_packet.await_args_list[0][0][1] assert pkt.encode() == '2/foo,11["my event",{"foo":"bar"}]' def test_emit_to_invalid_room(self): @@ -356,10 +356,10 @@ def test_emit_with_tuple(self): 'my event', ('foo', 'bar'), namespace='/foo', room=sid ) ) - assert self.bm.server._send_eio_packet.mock.call_count == 1 - assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + assert self.bm.server._send_eio_packet.await_count == 1 + assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' - pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event","foo","bar"]' def test_emit_with_list(self): @@ -369,10 +369,10 @@ def test_emit_with_list(self): 'my event', ['foo', 'bar'], namespace='/foo', room=sid ) ) - assert self.bm.server._send_eio_packet.mock.call_count == 1 - assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + assert self.bm.server._send_eio_packet.await_count == 1 + assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' - pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event",["foo","bar"]]' def test_emit_with_none(self): @@ -382,10 +382,10 @@ def test_emit_with_none(self): 'my event', None, namespace='/foo', room=sid ) ) - assert self.bm.server._send_eio_packet.mock.call_count == 1 - assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + assert self.bm.server._send_eio_packet.await_count == 1 + assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' - pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event"]' def test_emit_binary(self): @@ -395,12 +395,12 @@ def test_emit_binary(self): u'my event', b'my binary data', namespace='/', room=sid ) ) - assert self.bm.server._send_eio_packet.mock.call_count == 2 - assert self.bm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + assert self.bm.server._send_eio_packet.await_count == 2 + assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' - pkt = self.bm.server._send_eio_packet.mock.call_args_list[0][0][1] + pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '451-["my event",{"_placeholder":true,"num":0}]' - assert self.bm.server._send_eio_packet.mock.call_args_list[1][0][0] \ + assert self.bm.server._send_eio_packet.await_args_list[1][0][0] \ == '123' - pkt = self.bm.server._send_eio_packet.mock.call_args_list[1][0][1] + pkt = self.bm.server._send_eio_packet.await_args_list[1][0][1] assert pkt.encode() == b'my binary data' diff --git a/tests/async/test_namespace.py b/tests/async/test_namespace.py index 873f4791..62560159 100644 --- a/tests/async/test_namespace.py +++ b/tests/async/test_namespace.py @@ -1,7 +1,7 @@ from unittest import mock from socketio import async_namespace -from .helpers import AsyncMock, _run +from .helpers import _run class TestAsyncNamespace: @@ -70,14 +70,14 @@ async def on_custom_message(self, sid, data): def test_emit(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() - mock_server.emit = AsyncMock() + mock_server.emit = mock.AsyncMock() ns._set_server(mock_server) _run( ns.emit( 'ev', data='data', to='room', skip_sid='skip', callback='cb' ) ) - ns.server.emit.mock.assert_called_with( + ns.server.emit.assert_awaited_with( 'ev', data='data', to='room', @@ -98,7 +98,7 @@ def test_emit(self): ignore_queue=True, ) ) - ns.server.emit.mock.assert_called_with( + ns.server.emit.assert_awaited_with( 'ev', data='data', to=None, @@ -112,10 +112,10 @@ def test_emit(self): def test_send(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() - mock_server.send = AsyncMock() + mock_server.send = mock.AsyncMock() ns._set_server(mock_server) _run(ns.send(data='data', to='room', skip_sid='skip', callback='cb')) - ns.server.send.mock.assert_called_with( + ns.server.send.assert_awaited_with( 'data', to='room', room=None, @@ -134,7 +134,7 @@ def test_send(self): ignore_queue=True, ) ) - ns.server.send.mock.assert_called_with( + ns.server.send.assert_awaited_with( 'data', to=None, room='room', @@ -147,10 +147,10 @@ def test_send(self): def test_call(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() - mock_server.call = AsyncMock() + mock_server.call = mock.AsyncMock() ns._set_server(mock_server) _run(ns.call('ev', data='data', to='sid')) - ns.server.call.mock.assert_called_with( + ns.server.call.assert_awaited_with( 'ev', data='data', to='sid', @@ -161,7 +161,7 @@ def test_call(self): ) _run(ns.call('ev', data='data', sid='sid', namespace='/bar', timeout=45, ignore_queue=True)) - ns.server.call.mock.assert_called_with( + ns.server.call.assert_awaited_with( 'ev', data='data', to=None, @@ -174,40 +174,40 @@ def test_call(self): def test_enter_room(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() - mock_server.enter_room = AsyncMock() + mock_server.enter_room = mock.AsyncMock() ns._set_server(mock_server) _run(ns.enter_room('sid', 'room')) - ns.server.enter_room.mock.assert_called_with( + ns.server.enter_room.assert_awaited_with( 'sid', 'room', namespace='/foo' ) _run(ns.enter_room('sid', 'room', namespace='/bar')) - ns.server.enter_room.mock.assert_called_with( + ns.server.enter_room.assert_awaited_with( 'sid', 'room', namespace='/bar' ) def test_leave_room(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() - mock_server.leave_room = AsyncMock() + mock_server.leave_room = mock.AsyncMock() ns._set_server(mock_server) _run(ns.leave_room('sid', 'room')) - ns.server.leave_room.mock.assert_called_with( + ns.server.leave_room.assert_awaited_with( 'sid', 'room', namespace='/foo' ) _run(ns.leave_room('sid', 'room', namespace='/bar')) - ns.server.leave_room.mock.assert_called_with( + ns.server.leave_room.assert_awaited_with( 'sid', 'room', namespace='/bar' ) def test_close_room(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() - mock_server.close_room = AsyncMock() + mock_server.close_room = mock.AsyncMock() ns._set_server(mock_server) _run(ns.close_room('room')) - ns.server.close_room.mock.assert_called_with('room', namespace='/foo') + ns.server.close_room.assert_awaited_with('room', namespace='/foo') _run(ns.close_room('room', namespace='/bar')) - ns.server.close_room.mock.assert_called_with('room', namespace='/bar') + ns.server.close_room.assert_awaited_with('room', namespace='/bar') def test_rooms(self): ns = async_namespace.AsyncNamespace('/foo') @@ -220,19 +220,19 @@ def test_rooms(self): def test_session(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() - mock_server.get_session = AsyncMock() - mock_server.save_session = AsyncMock() + mock_server.get_session = mock.AsyncMock() + mock_server.save_session = mock.AsyncMock() ns._set_server(mock_server) _run(ns.get_session('sid')) - ns.server.get_session.mock.assert_called_with('sid', namespace='/foo') + ns.server.get_session.assert_awaited_with('sid', namespace='/foo') _run(ns.get_session('sid', namespace='/bar')) - ns.server.get_session.mock.assert_called_with('sid', namespace='/bar') + ns.server.get_session.assert_awaited_with('sid', namespace='/bar') _run(ns.save_session('sid', {'a': 'b'})) - ns.server.save_session.mock.assert_called_with( + ns.server.save_session.assert_awaited_with( 'sid', {'a': 'b'}, namespace='/foo' ) _run(ns.save_session('sid', {'a': 'b'}, namespace='/bar')) - ns.server.save_session.mock.assert_called_with( + ns.server.save_session.assert_awaited_with( 'sid', {'a': 'b'}, namespace='/bar' ) ns.session('sid') @@ -243,12 +243,12 @@ def test_session(self): def test_disconnect(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() - mock_server.disconnect = AsyncMock() + mock_server.disconnect = mock.AsyncMock() ns._set_server(mock_server) _run(ns.disconnect('sid')) - ns.server.disconnect.mock.assert_called_with('sid', namespace='/foo') + ns.server.disconnect.assert_awaited_with('sid', namespace='/foo') _run(ns.disconnect('sid', namespace='/bar')) - ns.server.disconnect.mock.assert_called_with('sid', namespace='/bar') + ns.server.disconnect.assert_awaited_with('sid', namespace='/bar') def test_sync_event_client(self): result = {} @@ -291,49 +291,49 @@ async def on_custom_message(self, sid, data): def test_emit_client(self): ns = async_namespace.AsyncClientNamespace('/foo') mock_client = mock.MagicMock() - mock_client.emit = AsyncMock() + mock_client.emit = mock.AsyncMock() ns._set_client(mock_client) _run(ns.emit('ev', data='data', callback='cb')) - ns.client.emit.mock.assert_called_with( + ns.client.emit.assert_awaited_with( 'ev', data='data', namespace='/foo', callback='cb' ) _run(ns.emit('ev', data='data', namespace='/bar', callback='cb')) - ns.client.emit.mock.assert_called_with( + ns.client.emit.assert_awaited_with( 'ev', data='data', namespace='/bar', callback='cb' ) def test_send_client(self): ns = async_namespace.AsyncClientNamespace('/foo') mock_client = mock.MagicMock() - mock_client.send = AsyncMock() + mock_client.send = mock.AsyncMock() ns._set_client(mock_client) _run(ns.send(data='data', callback='cb')) - ns.client.send.mock.assert_called_with( + ns.client.send.assert_awaited_with( 'data', namespace='/foo', callback='cb' ) _run(ns.send(data='data', namespace='/bar', callback='cb')) - ns.client.send.mock.assert_called_with( + ns.client.send.assert_awaited_with( 'data', namespace='/bar', callback='cb' ) def test_call_client(self): ns = async_namespace.AsyncClientNamespace('/foo') mock_client = mock.MagicMock() - mock_client.call = AsyncMock() + mock_client.call = mock.AsyncMock() ns._set_client(mock_client) _run(ns.call('ev', data='data')) - ns.client.call.mock.assert_called_with( + ns.client.call.assert_awaited_with( 'ev', data='data', namespace='/foo', timeout=None ) _run(ns.call('ev', data='data', namespace='/bar', timeout=45)) - ns.client.call.mock.assert_called_with( + ns.client.call.assert_awaited_with( 'ev', data='data', namespace='/bar', timeout=45 ) def test_disconnect_client(self): ns = async_namespace.AsyncClientNamespace('/foo') mock_client = mock.MagicMock() - mock_client.disconnect = AsyncMock() + mock_client.disconnect = mock.AsyncMock() ns._set_client(mock_client) _run(ns.disconnect()) - ns.client.disconnect.mock.assert_called_with() + ns.client.disconnect.assert_awaited_with() diff --git a/tests/async/test_pubsub_manager.py b/tests/async/test_pubsub_manager.py index 8a509d23..46928827 100644 --- a/tests/async/test_pubsub_manager.py +++ b/tests/async/test_pubsub_manager.py @@ -7,7 +7,7 @@ from socketio import async_manager from socketio import async_pubsub_manager from socketio import packet -from .helpers import AsyncMock, _run +from .helpers import _run class TestAsyncPubSubManager: @@ -22,11 +22,11 @@ def generate_id(): mock_server = mock.MagicMock() mock_server.eio.generate_id = generate_id mock_server.packet_class = packet.Packet - mock_server._send_packet = AsyncMock() - mock_server._send_eio_packet = AsyncMock() - mock_server.disconnect = AsyncMock() + mock_server._send_packet = mock.AsyncMock() + mock_server._send_eio_packet = mock.AsyncMock() + mock_server.disconnect = mock.AsyncMock() self.pm = async_pubsub_manager.AsyncPubSubManager() - self.pm._publish = AsyncMock() + self.pm._publish = mock.AsyncMock() self.pm.set_server(mock_server) self.pm.host_id = '123456' self.pm.initialize() @@ -53,7 +53,7 @@ def test_write_only_init(self): def test_emit(self): _run(self.pm.emit('foo', 'bar')) - self.pm._publish.mock.assert_called_once_with( + self.pm._publish.assert_awaited_once_with( { 'method': 'emit', 'event': 'foo', @@ -69,7 +69,7 @@ def test_emit(self): def test_emit_with_to(self): sid = 'room-mate' _run(self.pm.emit('foo', 'bar', to=sid)) - self.pm._publish.mock.assert_called_once_with( + self.pm._publish.assert_awaited_once_with( { 'method': 'emit', 'event': 'foo', @@ -84,7 +84,7 @@ def test_emit_with_to(self): def test_emit_with_namespace(self): _run(self.pm.emit('foo', 'bar', namespace='/baz')) - self.pm._publish.mock.assert_called_once_with( + self.pm._publish.assert_awaited_once_with( { 'method': 'emit', 'event': 'foo', @@ -99,7 +99,7 @@ def test_emit_with_namespace(self): def test_emit_with_room(self): _run(self.pm.emit('foo', 'bar', room='baz')) - self.pm._publish.mock.assert_called_once_with( + self.pm._publish.assert_awaited_once_with( { 'method': 'emit', 'event': 'foo', @@ -114,7 +114,7 @@ def test_emit_with_room(self): def test_emit_with_skip_sid(self): _run(self.pm.emit('foo', 'bar', skip_sid='baz')) - self.pm._publish.mock.assert_called_once_with( + self.pm._publish.assert_awaited_once_with( { 'method': 'emit', 'event': 'foo', @@ -132,7 +132,7 @@ def test_emit_with_callback(self): self.pm, '_generate_ack_id', return_value='123' ): _run(self.pm.emit('foo', 'bar', room='baz', callback='cb')) - self.pm._publish.mock.assert_called_once_with( + self.pm._publish.assert_awaited_once_with( { 'method': 'emit', 'event': 'foo', @@ -164,25 +164,25 @@ def test_emit_with_ignore_queue(self): 'foo', 'bar', room=sid, namespace='/', ignore_queue=True ) ) - self.pm._publish.mock.assert_not_called() - assert self.pm.server._send_eio_packet.mock.call_count == 1 - assert self.pm.server._send_eio_packet.mock.call_args_list[0][0][0] \ + self.pm._publish.assert_not_awaited() + assert self.pm.server._send_eio_packet.await_count == 1 + assert self.pm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' - pkt = self.pm.server._send_eio_packet.mock.call_args_list[0][0][1] + pkt = self.pm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42["foo","bar"]' def test_can_disconnect(self): sid = _run(self.pm.connect('123', '/')) assert _run(self.pm.can_disconnect(sid, '/')) is True _run(self.pm.can_disconnect(sid, '/foo')) - self.pm._publish.mock.assert_called_once_with( + self.pm._publish.assert_awaited_once_with( {'method': 'disconnect', 'sid': sid, 'namespace': '/foo', 'host_id': '123456'} ) def test_disconnect(self): _run(self.pm.disconnect('foo', '/')) - self.pm._publish.mock.assert_called_once_with( + self.pm._publish.assert_awaited_once_with( {'method': 'disconnect', 'sid': 'foo', 'namespace': '/', 'host_id': '123456'} ) @@ -191,7 +191,7 @@ def test_disconnect_ignore_queue(self): sid = _run(self.pm.connect('123', '/')) self.pm.pre_disconnect(sid, '/') _run(self.pm.disconnect(sid, '/', ignore_queue=True)) - self.pm._publish.mock.assert_not_called() + self.pm._publish.assert_not_awaited() assert self.pm.is_connected(sid, '/') is False def test_enter_room(self): @@ -200,7 +200,7 @@ def test_enter_room(self): _run(self.pm.enter_room('456', '/', 'foo')) assert sid in self.pm.rooms['/']['foo'] assert self.pm.rooms['/']['foo'][sid] == '123' - self.pm._publish.mock.assert_called_once_with( + self.pm._publish.assert_awaited_once_with( {'method': 'enter_room', 'sid': '456', 'room': 'foo', 'namespace': '/', 'host_id': '123456'} ) @@ -210,32 +210,31 @@ def test_leave_room(self): _run(self.pm.leave_room(sid, '/', 'foo')) _run(self.pm.leave_room('456', '/', 'foo')) assert 'foo' not in self.pm.rooms['/'] - self.pm._publish.mock.assert_called_once_with( + self.pm._publish.assert_awaited_once_with( {'method': 'leave_room', 'sid': '456', 'room': 'foo', 'namespace': '/', 'host_id': '123456'} ) def test_close_room(self): _run(self.pm.close_room('foo')) - self.pm._publish.mock.assert_called_once_with( + self.pm._publish.assert_awaited_once_with( {'method': 'close_room', 'room': 'foo', 'namespace': '/', 'host_id': '123456'} ) def test_close_room_with_namespace(self): _run(self.pm.close_room('foo', '/bar')) - self.pm._publish.mock.assert_called_once_with( + self.pm._publish.assert_awaited_once_with( {'method': 'close_room', 'room': 'foo', 'namespace': '/bar', 'host_id': '123456'} ) def test_handle_emit(self): with mock.patch.object( - async_manager.AsyncManager, 'emit', new=AsyncMock() + async_manager.AsyncManager, 'emit' ) as super_emit: _run(self.pm._handle_emit({'event': 'foo', 'data': 'bar'})) - super_emit.mock.assert_called_once_with( - self.pm, + super_emit.assert_awaited_once_with( 'foo', 'bar', namespace=None, @@ -246,15 +245,14 @@ def test_handle_emit(self): def test_handle_emit_with_namespace(self): with mock.patch.object( - async_manager.AsyncManager, 'emit', new=AsyncMock() + async_manager.AsyncManager, 'emit' ) as super_emit: _run( self.pm._handle_emit( {'event': 'foo', 'data': 'bar', 'namespace': '/baz'} ) ) - super_emit.mock.assert_called_once_with( - self.pm, + super_emit.assert_awaited_once_with( 'foo', 'bar', namespace='/baz', @@ -265,15 +263,14 @@ def test_handle_emit_with_namespace(self): def test_handle_emit_with_room(self): with mock.patch.object( - async_manager.AsyncManager, 'emit', new=AsyncMock() + async_manager.AsyncManager, 'emit' ) as super_emit: _run( self.pm._handle_emit( {'event': 'foo', 'data': 'bar', 'room': 'baz'} ) ) - super_emit.mock.assert_called_once_with( - self.pm, + super_emit.assert_awaited_once_with( 'foo', 'bar', namespace=None, @@ -284,15 +281,14 @@ def test_handle_emit_with_room(self): def test_handle_emit_with_skip_sid(self): with mock.patch.object( - async_manager.AsyncManager, 'emit', new=AsyncMock() + async_manager.AsyncManager, 'emit' ) as super_emit: _run( self.pm._handle_emit( {'event': 'foo', 'data': 'bar', 'skip_sid': '123'} ) ) - super_emit.mock.assert_called_once_with( - self.pm, + super_emit.assert_awaited_once_with( 'foo', 'bar', namespace=None, @@ -303,7 +299,7 @@ def test_handle_emit_with_skip_sid(self): def test_handle_emit_with_remote_callback(self): with mock.patch.object( - async_manager.AsyncManager, 'emit', new=AsyncMock() + async_manager.AsyncManager, 'emit' ) as super_emit: _run( self.pm._handle_emit( @@ -316,16 +312,16 @@ def test_handle_emit_with_remote_callback(self): } ) ) - assert super_emit.mock.call_count == 1 - assert super_emit.mock.call_args[0] == (self.pm, 'foo', 'bar') - assert super_emit.mock.call_args[1]['namespace'] == '/baz' - assert super_emit.mock.call_args[1]['room'] is None - assert super_emit.mock.call_args[1]['skip_sid'] is None + assert super_emit.await_count == 1 + assert super_emit.await_args[0] == ('foo', 'bar') + assert super_emit.await_args[1]['namespace'] == '/baz' + assert super_emit.await_args[1]['room'] is None + assert super_emit.await_args[1]['skip_sid'] is None assert isinstance( - super_emit.mock.call_args[1]['callback'], functools.partial + super_emit.await_args[1]['callback'], functools.partial ) - _run(super_emit.mock.call_args[1]['callback']('one', 2, 'three')) - self.pm._publish.mock.assert_called_once_with( + _run(super_emit.await_args[1]['callback']('one', 2, 'three')) + self.pm._publish.assert_awaited_once_with( { 'method': 'callback', 'host_id': 'x', @@ -338,7 +334,7 @@ def test_handle_emit_with_remote_callback(self): def test_handle_emit_with_local_callback(self): with mock.patch.object( - async_manager.AsyncManager, 'emit', new=AsyncMock() + async_manager.AsyncManager, 'emit' ) as super_emit: _run( self.pm._handle_emit( @@ -351,21 +347,21 @@ def test_handle_emit_with_local_callback(self): } ) ) - assert super_emit.mock.call_count == 1 - assert super_emit.mock.call_args[0] == (self.pm, 'foo', 'bar') - assert super_emit.mock.call_args[1]['namespace'] == '/baz' - assert super_emit.mock.call_args[1]['room'] is None - assert super_emit.mock.call_args[1]['skip_sid'] is None + assert super_emit.await_count == 1 + assert super_emit.await_args[0] == ('foo', 'bar') + assert super_emit.await_args[1]['namespace'] == '/baz' + assert super_emit.await_args[1]['room'] is None + assert super_emit.await_args[1]['skip_sid'] is None assert isinstance( - super_emit.mock.call_args[1]['callback'], functools.partial + super_emit.await_args[1]['callback'], functools.partial ) - _run(super_emit.mock.call_args[1]['callback']('one', 2, 'three')) - self.pm._publish.mock.assert_not_called() + _run(super_emit.await_args[1]['callback']('one', 2, 'three')) + self.pm._publish.assert_not_awaited() def test_handle_callback(self): host_id = self.pm.host_id with mock.patch.object( - self.pm, 'trigger_callback', new=AsyncMock() + self.pm, 'trigger_callback' ) as trigger: _run( self.pm._handle_callback( @@ -379,11 +375,11 @@ def test_handle_callback(self): } ) ) - trigger.mock.assert_called_once_with('sid', 123, ('one', 2)) + trigger.assert_awaited_once_with('sid', 123, ('one', 2)) def test_handle_callback_bad_host_id(self): with mock.patch.object( - self.pm, 'trigger_callback', new=AsyncMock() + self.pm, 'trigger_callback' ) as trigger: _run( self.pm._handle_callback( @@ -397,12 +393,12 @@ def test_handle_callback_bad_host_id(self): } ) ) - assert trigger.mock.call_count == 0 + assert trigger.await_count == 0 def test_handle_callback_missing_args(self): host_id = self.pm.host_id with mock.patch.object( - self.pm, 'trigger_callback', new=AsyncMock() + self.pm, 'trigger_callback' ) as trigger: _run( self.pm._handle_callback( @@ -435,7 +431,7 @@ def test_handle_callback_missing_args(self): {'method': 'callback', 'host_id': host_id} ) ) - assert trigger.mock.call_count == 0 + assert trigger.await_count == 0 def test_handle_disconnect(self): _run( @@ -443,14 +439,14 @@ def test_handle_disconnect(self): {'method': 'disconnect', 'sid': '123', 'namespace': '/foo'} ) ) - self.pm.server.disconnect.mock.assert_called_once_with( + self.pm.server.disconnect.assert_awaited_once_with( sid='123', namespace='/foo', ignore_queue=True ) def test_handle_enter_room(self): sid = _run(self.pm.connect('123', '/')) with mock.patch.object( - async_manager.AsyncManager, 'enter_room', new=AsyncMock() + async_manager.AsyncManager, 'enter_room' ) as super_enter_room: _run( self.pm._handle_enter_room( @@ -464,14 +460,12 @@ def test_handle_enter_room(self): 'room': 'foo'} ) ) - super_enter_room.mock.assert_called_once_with( - self.pm, sid, '/', 'foo' - ) + super_enter_room.assert_awaited_once_with(sid, '/', 'foo') def test_handle_leave_room(self): sid = _run(self.pm.connect('123', '/')) with mock.patch.object( - async_manager.AsyncManager, 'leave_room', new=AsyncMock() + async_manager.AsyncManager, 'leave_room' ) as super_leave_room: _run( self.pm._handle_leave_room( @@ -485,26 +479,24 @@ def test_handle_leave_room(self): 'room': 'foo'} ) ) - super_leave_room.mock.assert_called_once_with( - self.pm, sid, '/', 'foo' - ) + super_leave_room.assert_awaited_once_with(sid, '/', 'foo') def test_handle_close_room(self): with mock.patch.object( - async_manager.AsyncManager, 'close_room', new=AsyncMock() + async_manager.AsyncManager, 'close_room' ) as super_close_room: _run( self.pm._handle_close_room( {'method': 'close_room', 'room': 'foo'} ) ) - super_close_room.mock.assert_called_once_with( - self.pm, room='foo', namespace=None + super_close_room.assert_awaited_once_with( + room='foo', namespace=None ) def test_handle_close_room_with_namespace(self): with mock.patch.object( - async_manager.AsyncManager, 'close_room', new=AsyncMock() + async_manager.AsyncManager, 'close_room' ) as super_close_room: _run( self.pm._handle_close_room( @@ -515,17 +507,17 @@ def test_handle_close_room_with_namespace(self): } ) ) - super_close_room.mock.assert_called_once_with( - self.pm, room='foo', namespace='/bar' + super_close_room.assert_awaited_once_with( + room='foo', namespace='/bar' ) def test_background_thread(self): - self.pm._handle_emit = AsyncMock() - self.pm._handle_callback = AsyncMock() - self.pm._handle_disconnect = AsyncMock() - self.pm._handle_enter_room = AsyncMock() - self.pm._handle_leave_room = AsyncMock() - self.pm._handle_close_room = AsyncMock() + self.pm._handle_emit = mock.AsyncMock() + self.pm._handle_callback = mock.AsyncMock() + self.pm._handle_disconnect = mock.AsyncMock() + self.pm._handle_enter_room = mock.AsyncMock() + self.pm._handle_leave_room = mock.AsyncMock() + self.pm._handle_close_room = mock.AsyncMock() host_id = self.pm.host_id async def messages(): @@ -558,34 +550,34 @@ async def messages(): self.pm._listen = messages _run(self.pm._thread()) - self.pm._handle_emit.mock.assert_called_once_with( + self.pm._handle_emit.assert_awaited_once_with( {'method': 'emit', 'value': 'foo', 'host_id': 'x'} ) - self.pm._handle_callback.mock.assert_any_call( + self.pm._handle_callback.assert_any_await( {'method': 'callback', 'value': 'bar', 'host_id': 'x'} ) - self.pm._handle_callback.mock.assert_any_call( + self.pm._handle_callback.assert_any_await( {'method': 'callback', 'value': 'bar', 'host_id': host_id} ) - self.pm._handle_disconnect.mock.assert_called_once_with( + self.pm._handle_disconnect.assert_awaited_once_with( {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', 'host_id': 'x'} ) - self.pm._handle_enter_room.mock.assert_called_once_with( + self.pm._handle_enter_room.assert_awaited_once_with( {'method': 'enter_room', 'sid': '123', 'namespace': '/foo', 'room': 'room', 'host_id': 'x'} ) - self.pm._handle_leave_room.mock.assert_called_once_with( + self.pm._handle_leave_room.assert_awaited_once_with( {'method': 'leave_room', 'sid': '123', 'namespace': '/foo', 'room': 'room', 'host_id': 'x'} ) - self.pm._handle_close_room.mock.assert_called_once_with( + self.pm._handle_close_room.assert_awaited_once_with( {'method': 'close_room', 'value': 'baz', 'host_id': 'x'} ) def test_background_thread_exception(self): - self.pm._handle_emit = AsyncMock(side_effect=[ValueError(), - asyncio.CancelledError]) + self.pm._handle_emit = mock.AsyncMock(side_effect=[ + ValueError(), asyncio.CancelledError]) async def messages(): yield {'method': 'emit', 'value': 'foo', 'host_id': 'x'} @@ -594,9 +586,9 @@ async def messages(): self.pm._listen = messages _run(self.pm._thread()) - self.pm._handle_emit.mock.assert_any_call( + self.pm._handle_emit.assert_any_await( {'method': 'emit', 'value': 'foo', 'host_id': 'x'} ) - self.pm._handle_emit.mock.assert_called_with( + self.pm._handle_emit.assert_awaited_with( {'method': 'emit', 'value': 'bar', 'host_id': 'x'} ) diff --git a/tests/async/test_server.py b/tests/async/test_server.py index 8b2dfe28..704982bd 100644 --- a/tests/async/test_server.py +++ b/tests/async/test_server.py @@ -11,12 +11,12 @@ from socketio import exceptions from socketio import namespace from socketio import packet -from .helpers import AsyncMock, _run +from .helpers import _run @mock.patch('socketio.server.engineio.AsyncServer', **{ 'return_value.generate_id.side_effect': [str(i) for i in range(1, 10)], - 'return_value.send_packet': AsyncMock()}) + 'return_value.send_packet': mock.AsyncMock()}) class TestAsyncServer: def teardown_method(self): # restore JSON encoder, in case a test changed it @@ -24,16 +24,16 @@ def teardown_method(self): def _get_mock_manager(self): mgr = mock.MagicMock() - mgr.can_disconnect = AsyncMock() - mgr.emit = AsyncMock() - mgr.enter_room = AsyncMock() - mgr.leave_room = AsyncMock() - mgr.close_room = AsyncMock() - mgr.trigger_callback = AsyncMock() + mgr.can_disconnect = mock.AsyncMock() + mgr.emit = mock.AsyncMock() + mgr.enter_room = mock.AsyncMock() + mgr.leave_room = mock.AsyncMock() + mgr.close_room = mock.AsyncMock() + mgr.trigger_callback = mock.AsyncMock() return mgr def test_create(self, eio): - eio.return_value.handle_request = AsyncMock() + eio.return_value.handle_request = mock.AsyncMock() mgr = self._get_mock_manager() s = async_server.AsyncServer( client_manager=mgr, async_handlers=True, foo='bar' @@ -80,7 +80,7 @@ def test_emit(self, eio): callback='cb', ) ) - s.manager.emit.mock.assert_called_once_with( + s.manager.emit.assert_awaited_once_with( 'my event', {'foo': 'bar'}, '/foo', @@ -100,7 +100,7 @@ def test_emit(self, eio): ignore_queue=True, ) ) - s.manager.emit.mock.assert_called_with( + s.manager.emit.assert_awaited_with( 'my event', {'foo': 'bar'}, '/foo', @@ -122,7 +122,7 @@ def test_emit_default_namespace(self, eio): callback='cb', ) ) - s.manager.emit.mock.assert_called_once_with( + s.manager.emit.assert_awaited_once_with( 'my event', {'foo': 'bar'}, '/', @@ -141,7 +141,7 @@ def test_emit_default_namespace(self, eio): ignore_queue=True, ) ) - s.manager.emit.mock.assert_called_with( + s.manager.emit.assert_awaited_with( 'my event', {'foo': 'bar'}, '/', @@ -163,7 +163,7 @@ def test_send(self, eio): callback='cb', ) ) - s.manager.emit.mock.assert_called_once_with( + s.manager.emit.assert_awaited_once_with( 'message', 'foo', '/foo', @@ -182,7 +182,7 @@ def test_send(self, eio): ignore_queue=True, ) ) - s.manager.emit.mock.assert_called_with( + s.manager.emit.assert_awaited_with( 'message', 'foo', '/foo', @@ -197,7 +197,7 @@ def test_call(self, eio): s = async_server.AsyncServer(client_manager=mgr) async def fake_event_wait(): - s.manager.emit.mock.call_args_list[0][1]['callback']('foo', 321) + s.manager.emit.await_args_list[0][1]['callback']('foo', 321) return True s.eio.create_event.return_value.wait = fake_event_wait @@ -231,39 +231,37 @@ def test_enter_room(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) _run(s.enter_room('123', 'room', namespace='/foo')) - s.manager.enter_room.mock.assert_called_once_with('123', '/foo', - 'room') + s.manager.enter_room.assert_awaited_once_with('123', '/foo', 'room') def test_enter_room_default_namespace(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) _run(s.enter_room('123', 'room')) - s.manager.enter_room.mock.assert_called_once_with('123', '/', 'room') + s.manager.enter_room.assert_awaited_once_with('123', '/', 'room') def test_leave_room(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) _run(s.leave_room('123', 'room', namespace='/foo')) - s.manager.leave_room.mock.assert_called_once_with('123', '/foo', - 'room') + s.manager.leave_room.assert_awaited_once_with('123', '/foo', 'room') def test_leave_room_default_namespace(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) _run(s.leave_room('123', 'room')) - s.manager.leave_room.mock.assert_called_once_with('123', '/', 'room') + s.manager.leave_room.assert_awaited_once_with('123', '/', 'room') def test_close_room(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) _run(s.close_room('room', namespace='/foo')) - s.manager.close_room.mock.assert_called_once_with('room', '/foo') + s.manager.close_room.assert_awaited_once_with('room', '/foo') def test_close_room_default_namespace(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) _run(s.close_room('room')) - s.manager.close_room.mock.assert_called_once_with('room', '/') + s.manager.close_room.assert_awaited_once_with('room', '/') def test_rooms(self, eio): mgr = self._get_mock_manager() @@ -278,32 +276,32 @@ def test_rooms_default_namespace(self, eio): s.manager.get_rooms.assert_called_once_with('123', '/') def test_handle_request(self, eio): - eio.return_value.handle_request = AsyncMock() + eio.return_value.handle_request = mock.AsyncMock() s = async_server.AsyncServer() _run(s.handle_request('environ')) - s.eio.handle_request.mock.assert_called_once_with('environ') + s.eio.handle_request.assert_awaited_once_with('environ') def test_send_packet(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() _run(s._send_packet('123', packet.Packet( packet.EVENT, ['my event', 'my data'], namespace='/foo'))) - s.eio.send.mock.assert_called_once_with( + s.eio.send.assert_awaited_once_with( '123', '2/foo,["my event","my data"]' ) def test_send_eio_packet(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() _run(s._send_eio_packet('123', eio_packet.Packet( eio_packet.MESSAGE, 'hello'))) - assert s.eio.send_packet.mock.call_count == 1 - assert s.eio.send_packet.mock.call_args_list[0][0][0] == '123' - pkt = s.eio.send_packet.mock.call_args_list[0][0][1] + assert s.eio.send_packet.await_count == 1 + assert s.eio.send_packet.await_args_list[0][0][0] == '123' + pkt = s.eio.send_packet.await_args_list[0][0][1] assert pkt.encode() == '4hello' def test_transport(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.eio.transport = mock.MagicMock(return_value='polling') sid_foo = _run(s.manager.connect('123', '/foo')) @@ -311,7 +309,7 @@ def test_transport(self, eio): s.eio.transport.assert_called_once_with('123') def test_handle_connect(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.initialize = mock.MagicMock() handler = mock.MagicMock() @@ -320,14 +318,14 @@ def test_handle_connect(self, eio): _run(s._handle_eio_message('123', '0')) assert s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') - s.eio.send.mock.assert_called_once_with('123', '0{"sid":"1"}') + s.eio.send.assert_awaited_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 _run(s._handle_eio_connect('456', 'environ')) _run(s._handle_eio_message('456', '0')) assert s.manager.initialize.call_count == 1 def test_handle_connect_with_auth(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.initialize = mock.MagicMock() handler = mock.MagicMock() @@ -336,14 +334,14 @@ def test_handle_connect_with_auth(self, eio): _run(s._handle_eio_message('123', '0{"token":"abc"}')) assert s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ', {'token': 'abc'}) - s.eio.send.mock.assert_called_once_with('123', '0{"sid":"1"}') + s.eio.send.assert_awaited_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 _run(s._handle_eio_connect('456', 'environ')) _run(s._handle_eio_message('456', '0')) assert s.manager.initialize.call_count == 1 def test_handle_connect_with_auth_none(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.initialize = mock.MagicMock() handler = mock.MagicMock(side_effect=[TypeError, None, None]) @@ -352,30 +350,30 @@ def test_handle_connect_with_auth_none(self, eio): _run(s._handle_eio_message('123', '0')) assert s.manager.is_connected('1', '/') handler.assert_called_with('1', 'environ', None) - s.eio.send.mock.assert_called_once_with('123', '0{"sid":"1"}') + s.eio.send.assert_awaited_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 _run(s._handle_eio_connect('456', 'environ')) _run(s._handle_eio_message('456', '0')) assert s.manager.initialize.call_count == 1 def test_handle_connect_async(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.initialize = mock.MagicMock() - handler = AsyncMock() + handler = mock.AsyncMock() s.on('connect', handler) _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0')) assert s.manager.is_connected('1', '/') - handler.mock.assert_called_once_with('1', 'environ') - s.eio.send.mock.assert_called_once_with('123', '0{"sid":"1"}') + handler.assert_awaited_once_with('1', 'environ') + s.eio.send.assert_awaited_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 _run(s._handle_eio_connect('456', 'environ')) _run(s._handle_eio_message('456', '0')) assert s.manager.initialize.call_count == 1 def test_handle_connect_with_default_implied_namespaces(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0')) @@ -384,7 +382,7 @@ def test_handle_connect_with_default_implied_namespaces(self, eio): assert not s.manager.is_connected('2', '/foo') def test_handle_connect_with_implied_namespaces(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(namespaces=['/foo']) _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0')) @@ -393,7 +391,7 @@ def test_handle_connect_with_implied_namespaces(self, eio): assert s.manager.is_connected('1', '/foo') def test_handle_connect_with_all_implied_namespaces(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(namespaces='*') _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0')) @@ -402,7 +400,7 @@ def test_handle_connect_with_all_implied_namespaces(self, eio): assert s.manager.is_connected('2', '/foo') def test_handle_connect_namespace(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock() s.on('connect', handler, namespace='/foo') @@ -410,10 +408,10 @@ def test_handle_connect_namespace(self, eio): _run(s._handle_eio_message('123', '0/foo,')) assert s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') - s.eio.send.mock.assert_called_once_with('123', '0/foo,{"sid":"1"}') + s.eio.send.assert_awaited_once_with('123', '0/foo,{"sid":"1"}') def test_handle_connect_always_connect(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(always_connect=True) s.manager.initialize = mock.MagicMock() handler = mock.MagicMock() @@ -422,14 +420,14 @@ def test_handle_connect_always_connect(self, eio): _run(s._handle_eio_message('123', '0')) assert s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') - s.eio.send.mock.assert_called_once_with('123', '0{"sid":"1"}') + s.eio.send.assert_awaited_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 _run(s._handle_eio_connect('456', 'environ')) _run(s._handle_eio_message('456', '0')) assert s.manager.initialize.call_count == 1 def test_handle_connect_rejected(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock(return_value=False) s.on('connect', handler) @@ -437,12 +435,12 @@ def test_handle_connect_rejected(self, eio): _run(s._handle_eio_message('123', '0')) assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') - s.eio.send.mock.assert_called_once_with( + s.eio.send.assert_awaited_once_with( '123', '4{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} def test_handle_connect_namespace_rejected(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock(return_value=False) s.on('connect', handler, namespace='/foo') @@ -450,12 +448,12 @@ def test_handle_connect_namespace_rejected(self, eio): _run(s._handle_eio_message('123', '0/foo,')) assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') - s.eio.send.mock.assert_any_call( + s.eio.send.assert_any_await( '123', '4/foo,{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} def test_handle_connect_rejected_always_connect(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(always_connect=True) handler = mock.MagicMock(return_value=False) s.on('connect', handler) @@ -463,13 +461,13 @@ def test_handle_connect_rejected_always_connect(self, eio): _run(s._handle_eio_message('123', '0')) assert not s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') - s.eio.send.mock.assert_any_call('123', '0{"sid":"1"}') - s.eio.send.mock.assert_any_call( + s.eio.send.assert_any_await('123', '0{"sid":"1"}') + s.eio.send.assert_any_await( '123', '1{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} def test_handle_connect_namespace_rejected_always_connect(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(always_connect=True) handler = mock.MagicMock(return_value=False) s.on('connect', handler, namespace='/foo') @@ -477,13 +475,13 @@ def test_handle_connect_namespace_rejected_always_connect(self, eio): _run(s._handle_eio_message('123', '0/foo,')) assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') - s.eio.send.mock.assert_any_call('123', '0/foo,{"sid":"1"}') - s.eio.send.mock.assert_any_call( + s.eio.send.assert_any_await('123', '0/foo,{"sid":"1"}') + s.eio.send.assert_any_await( '123', '1/foo,{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} def test_handle_connect_rejected_with_exception(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError('fail_reason') @@ -493,12 +491,12 @@ def test_handle_connect_rejected_with_exception(self, eio): _run(s._handle_eio_message('123', '0')) assert not s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') - s.eio.send.mock.assert_called_once_with( + s.eio.send.assert_awaited_once_with( '123', '4{"message":"fail_reason"}') assert s.environ == {'123': 'environ'} def test_handle_connect_rejected_with_empty_exception(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError() @@ -508,12 +506,12 @@ def test_handle_connect_rejected_with_empty_exception(self, eio): _run(s._handle_eio_message('123', '0')) assert not s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') - s.eio.send.mock.assert_called_once_with( + s.eio.send.assert_awaited_once_with( '123', '4{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} def test_handle_connect_namespace_rejected_with_exception(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError( @@ -524,12 +522,12 @@ def test_handle_connect_namespace_rejected_with_exception(self, eio): _run(s._handle_eio_message('123', '0/foo,')) assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') - s.eio.send.mock.assert_called_once_with( + s.eio.send.assert_awaited_once_with( '123', '4/foo,{"message":"fail_reason","data":[1,"2"]}') assert s.environ == {'123': 'environ'} def test_handle_connect_namespace_rejected_with_empty_exception(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError() @@ -539,26 +537,26 @@ def test_handle_connect_namespace_rejected_with_empty_exception(self, eio): _run(s._handle_eio_message('123', '0/foo,')) assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') - s.eio.send.mock.assert_called_once_with( + s.eio.send.assert_awaited_once_with( '123', '4/foo,{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} def test_handle_disconnect(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() - s.manager.disconnect = AsyncMock() + s.manager.disconnect = mock.AsyncMock() handler = mock.MagicMock() s.on('disconnect', handler) _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0')) _run(s._handle_eio_disconnect('123')) handler.assert_called_once_with('1') - s.manager.disconnect.mock.assert_called_once_with( + s.manager.disconnect.assert_awaited_once_with( '1', '/', ignore_queue=True) assert s.environ == {} def test_handle_disconnect_namespace(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock() s.on('disconnect', handler) @@ -572,7 +570,7 @@ def test_handle_disconnect_namespace(self, eio): assert s.environ == {} def test_handle_disconnect_only_namespace(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock() s.on('disconnect', handler) @@ -591,21 +589,21 @@ def test_handle_disconnect_unknown_client(self, eio): _run(s._handle_eio_disconnect('123')) def test_handle_event(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = _run(s.manager.connect('123', '/')) - handler = AsyncMock() - catchall_handler = AsyncMock() + handler = mock.AsyncMock() + catchall_handler = mock.AsyncMock() s.on('msg', handler) s.on('*', catchall_handler) _run(s._handle_eio_message('123', '2["msg","a","b"]')) _run(s._handle_eio_message('123', '2["my message","a","b","c"]')) - handler.mock.assert_called_once_with(sid, 'a', 'b') - catchall_handler.mock.assert_called_once_with( + handler.assert_awaited_once_with(sid, 'a', 'b') + catchall_handler.assert_awaited_once_with( 'my message', sid, 'a', 'b', 'c') def test_handle_event_with_namespace(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = _run(s.manager.connect('123', '/foo')) handler = mock.MagicMock() @@ -619,7 +617,7 @@ def test_handle_event_with_namespace(self, eio): 'my message', sid, 'a', 'b', 'c') def test_handle_event_with_catchall_namespace(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid_foo = _run(s.manager.connect('123', '/foo')) sid_bar = _run(s.manager.connect('123', '/bar')) @@ -648,7 +646,7 @@ def test_handle_event_with_catchall_namespace(self, eio): 'my message', '/bar', sid_bar, 'a', 'b', 'c') def test_handle_event_with_disconnected_namespace(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) _run(s.manager.connect('123', '/foo')) handler = mock.MagicMock() @@ -657,7 +655,7 @@ def test_handle_event_with_disconnected_namespace(self, eio): handler.assert_not_called() def test_handle_event_binary(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = _run(s.manager.connect('123', '/')) handler = mock.MagicMock() @@ -675,9 +673,9 @@ def test_handle_event_binary(self, eio): handler.assert_called_once_with(sid, 'a', b'bar', b'foo') def test_handle_event_binary_ack(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) - s.manager.trigger_callback = AsyncMock() + s.manager.trigger_callback = mock.AsyncMock() sid = _run(s.manager.connect('123', '/')) _run( s._handle_eio_message( @@ -686,67 +684,67 @@ def test_handle_event_binary_ack(self, eio): ) ) _run(s._handle_eio_message('123', b'foo')) - s.manager.trigger_callback.mock.assert_called_once_with( + s.manager.trigger_callback.assert_awaited_once_with( sid, 321, ['my message', 'a', b'foo'] ) def test_handle_event_with_ack(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = _run(s.manager.connect('123', '/')) handler = mock.MagicMock(return_value='foo') s.on('my message', handler) _run(s._handle_eio_message('123', '21000["my message","foo"]')) handler.assert_called_once_with(sid, 'foo') - s.eio.send.mock.assert_called_once_with( + s.eio.send.assert_awaited_once_with( '123', '31000["foo"]' ) def test_handle_unknown_event_with_ack(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) _run(s.manager.connect('123', '/')) handler = mock.MagicMock(return_value='foo') s.on('my message', handler) _run(s._handle_eio_message('123', '21000["another message","foo"]')) - s.eio.send.mock.assert_not_called() + s.eio.send.assert_not_awaited() def test_handle_event_with_ack_none(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = _run(s.manager.connect('123', '/')) handler = mock.MagicMock(return_value=None) s.on('my message', handler) _run(s._handle_eio_message('123', '21000["my message","foo"]')) handler.assert_called_once_with(sid, 'foo') - s.eio.send.mock.assert_called_once_with('123', '31000[]') + s.eio.send.assert_awaited_once_with('123', '31000[]') def test_handle_event_with_ack_tuple(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = _run(s.manager.connect('123', '/')) handler = mock.MagicMock(return_value=(1, '2', True)) s.on('my message', handler) _run(s._handle_eio_message('123', '21000["my message","a","b","c"]')) handler.assert_called_once_with(sid, 'a', 'b', 'c') - s.eio.send.mock.assert_called_once_with( + s.eio.send.assert_awaited_once_with( '123', '31000[1,"2",true]' ) def test_handle_event_with_ack_list(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = _run(s.manager.connect('123', '/')) handler = mock.MagicMock(return_value=[1, '2', True]) s.on('my message', handler) _run(s._handle_eio_message('123', '21000["my message","a","b","c"]')) handler.assert_called_once_with(sid, 'a', 'b', 'c') - s.eio.send.mock.assert_called_once_with( + s.eio.send.assert_awaited_once_with( '123', '31000[[1,"2",true]]' ) def test_handle_event_with_ack_binary(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) sid = _run(s.manager.connect('123', '/')) handler = mock.MagicMock(return_value=b'foo') @@ -765,7 +763,7 @@ def test_handle_invalid_packet(self, eio): _run(s._handle_eio_message('123', '9')) def test_send_with_ack(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/'] = {} _run(s._handle_eio_connect('123', 'environ')) @@ -781,7 +779,7 @@ def test_send_with_ack(self, eio): cb.assert_called_once_with('foo', 2) def test_send_with_ack_namespace(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/foo'] = {} _run(s._handle_eio_connect('123', 'environ')) @@ -809,7 +807,7 @@ async def fake_save_session(eio_sid, session): assert eio_sid == '123' fake_session = session - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/'] = {} s.handlers['/ns'] = {} @@ -841,63 +839,63 @@ async def _test(): _run(_test()) def test_disconnect(self, eio): - eio.return_value.send = AsyncMock() - eio.return_value.disconnect = AsyncMock() + eio.return_value.send = mock.AsyncMock() + eio.return_value.disconnect = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/'] = {} _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0')) _run(s.disconnect('1')) - s.eio.send.mock.assert_any_call('123', '1') + s.eio.send.assert_any_await('123', '1') assert not s.manager.is_connected('1', '/') def test_disconnect_ignore_queue(self, eio): - eio.return_value.send = AsyncMock() - eio.return_value.disconnect = AsyncMock() + eio.return_value.send = mock.AsyncMock() + eio.return_value.disconnect = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/'] = {} _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0')) _run(s.disconnect('1', ignore_queue=True)) - s.eio.send.mock.assert_any_call('123', '1') + s.eio.send.assert_any_await('123', '1') assert not s.manager.is_connected('1', '/') def test_disconnect_namespace(self, eio): - eio.return_value.send = AsyncMock() - eio.return_value.disconnect = AsyncMock() + eio.return_value.send = mock.AsyncMock() + eio.return_value.disconnect = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/foo'] = {} _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0/foo,')) _run(s.disconnect('1', namespace='/foo')) - s.eio.send.mock.assert_any_call('123', '1/foo,') + s.eio.send.assert_any_await('123', '1/foo,') assert not s.manager.is_connected('1', '/foo') def test_disconnect_twice(self, eio): - eio.return_value.send = AsyncMock() - eio.return_value.disconnect = AsyncMock() + eio.return_value.send = mock.AsyncMock() + eio.return_value.disconnect = mock.AsyncMock() s = async_server.AsyncServer() _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0')) _run(s.disconnect('1')) - calls = s.eio.send.mock.call_count + calls = s.eio.send.await_count assert not s.manager.is_connected('1', '/') _run(s.disconnect('1')) - assert calls == s.eio.send.mock.call_count + assert calls == s.eio.send.await_count def test_disconnect_twice_namespace(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() _run(s._handle_eio_connect('123', 'environ')) _run(s._handle_eio_message('123', '0/foo,')) _run(s.disconnect('1', namespace='/foo')) - calls = s.eio.send.mock.call_count + calls = s.eio.send.await_count assert not s.manager.is_connected('1', '/foo') _run(s.disconnect('1', namespace='/foo')) - assert calls == s.eio.send.mock.call_count + assert calls == s.eio.send.await_count def test_namespace_handler(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() result = {} class MyNamespace(async_namespace.AsyncNamespace): @@ -931,7 +929,7 @@ async def on_baz(self, sid, data1, data2): assert result['result'] == ('disconnect', '1') def test_catchall_namespace_handler(self, eio): - eio.return_value.send = AsyncMock() + eio.return_value.send = mock.AsyncMock() result = {} class MyNamespace(async_namespace.AsyncNamespace): @@ -1047,9 +1045,9 @@ def test_async_handlers(self, eio): def test_shutdown(self, eio): s = async_server.AsyncServer() - s.eio.shutdown = AsyncMock() + s.eio.shutdown = mock.AsyncMock() _run(s.shutdown()) - s.eio.shutdown.mock.assert_called_once_with() + s.eio.shutdown.assert_awaited_once_with() def test_start_background_task(self, eio): s = async_server.AsyncServer() @@ -1059,7 +1057,7 @@ def test_start_background_task(self, eio): ) def test_sleep(self, eio): - eio.return_value.sleep = AsyncMock() + eio.return_value.sleep = mock.AsyncMock() s = async_server.AsyncServer() _run(s.sleep(1.23)) - s.eio.sleep.mock.assert_called_once_with(1.23) + s.eio.sleep.assert_awaited_once_with(1.23) diff --git a/tests/async/test_simple_client.py b/tests/async/test_simple_client.py index 6a2eb7ad..a8d08f41 100644 --- a/tests/async/test_simple_client.py +++ b/tests/async/test_simple_client.py @@ -4,7 +4,7 @@ from socketio import AsyncSimpleClient from socketio.exceptions import SocketIOError, TimeoutError, DisconnectedError -from .helpers import AsyncMock, _run +from .helpers import _run class TestAsyncAsyncSimpleClient: @@ -20,14 +20,14 @@ def test_connect(self): client = AsyncSimpleClient(123, a='b') with mock.patch('socketio.async_simple_client.AsyncClient') \ as mock_client: - mock_client.return_value.connect = AsyncMock() + mock_client.return_value.connect = mock.AsyncMock() _run(client.connect('url', headers='h', auth='a', transports='t', namespace='n', socketio_path='s', wait_timeout='w')) mock_client.assert_called_once_with(123, a='b') assert client.client == mock_client() - mock_client().connect.mock.assert_called_once_with( + mock_client().connect.assert_awaited_once_with( 'url', headers='h', auth='a', transports='t', namespaces=['n'], socketio_path='s', wait_timeout='w') mock_client().event.call_count == 3 @@ -40,14 +40,14 @@ async def _t(): async with AsyncSimpleClient(123, a='b') as client: with mock.patch('socketio.async_simple_client.AsyncClient') \ as mock_client: - mock_client.return_value.connect = AsyncMock() + mock_client.return_value.connect = mock.AsyncMock() await client.connect('url', headers='h', auth='a', transports='t', namespace='n', socketio_path='s', wait_timeout='w') mock_client.assert_called_once_with(123, a='b') assert client.client == mock_client() - mock_client().connect.mock.assert_called_once_with( + mock_client().connect.assert_awaited_once_with( 'url', headers='h', auth='a', transports='t', namespaces=['n'], socketio_path='s', wait_timeout='w') mock_client().event.call_count == 3 @@ -79,14 +79,14 @@ def test_properties(self): def test_emit(self): client = AsyncSimpleClient() client.client = mock.MagicMock() - client.client.emit = AsyncMock() + client.client.emit = mock.AsyncMock() client.namespace = '/ns' client.connected_event.set() client.connected = True _run(client.emit('foo', 'bar')) - client.client.emit.mock.assert_called_once_with('foo', 'bar', - namespace='/ns') + client.client.emit.assert_awaited_once_with('foo', 'bar', + namespace='/ns') def test_emit_disconnected(self): client = AsyncSimpleClient() @@ -100,23 +100,23 @@ def test_emit_retries(self): client.connected_event.set() client.connected = True client.client = mock.MagicMock() - client.client.emit = AsyncMock() - client.client.emit.mock.side_effect = [SocketIOError(), None] + client.client.emit = mock.AsyncMock() + client.client.emit.side_effect = [SocketIOError(), None] _run(client.emit('foo', 'bar')) - client.client.emit.mock.assert_called_with('foo', 'bar', namespace='/') + client.client.emit.assert_awaited_with('foo', 'bar', namespace='/') def test_call(self): client = AsyncSimpleClient() client.client = mock.MagicMock() - client.client.call = AsyncMock() - client.client.call.mock.return_value = 'result' + client.client.call = mock.AsyncMock() + client.client.call.return_value = 'result' client.namespace = '/ns' client.connected_event.set() client.connected = True assert _run(client.call('foo', 'bar')) == 'result' - client.client.call.mock.assert_called_once_with( + client.client.call.assert_awaited_once_with( 'foo', 'bar', namespace='/ns', timeout=60) def test_call_disconnected(self): @@ -131,12 +131,12 @@ def test_call_retries(self): client.connected_event.set() client.connected = True client.client = mock.MagicMock() - client.client.call = AsyncMock() - client.client.call.mock.side_effect = [SocketIOError(), 'result'] + client.client.call = mock.AsyncMock() + client.client.call.side_effect = [SocketIOError(), 'result'] assert _run(client.call('foo', 'bar')) == 'result' - client.client.call.mock.assert_called_with('foo', 'bar', namespace='/', - timeout=60) + client.client.call.assert_awaited_with('foo', 'bar', namespace='/', + timeout=60) def test_receive_with_input_buffer(self): client = AsyncSimpleClient() @@ -180,10 +180,10 @@ def test_receive_disconnected(self): def test_disconnect(self): client = AsyncSimpleClient() mc = mock.MagicMock() - mc.disconnect = AsyncMock() + mc.disconnect = mock.AsyncMock() client.client = mc client.connected = True _run(client.disconnect()) _run(client.disconnect()) - mc.disconnect.mock.assert_called_once_with() + mc.disconnect.assert_awaited_once_with() assert client.client is None From 3e95bb5ca8711dcd9e6b4cb077576c67980f7bba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:25:49 +0000 Subject: [PATCH 478/571] Bump path-to-regexp and express in /examples/client/javascript (#1416) #nolog Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) to 0.1.12 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `path-to-regexp` from 0.1.10 to 0.1.12 - [Release notes](https://github.com/pillarjs/path-to-regexp/releases) - [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md) - [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.10...v0.1.12) Updates `express` from 4.21.1 to 4.21.2 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md) - [Commits](https://github.com/expressjs/express/compare/4.21.1...4.21.2) --- updated-dependencies: - dependency-name: path-to-regexp dependency-type: indirect - dependency-name: express dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/client/javascript/package-lock.json | 34 +++++++++++--------- examples/client/javascript/package.json | 2 +- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/examples/client/javascript/package-lock.json b/examples/client/javascript/package-lock.json index ca02f7af..5a2ec0ca 100644 --- a/examples/client/javascript/package-lock.json +++ b/examples/client/javascript/package-lock.json @@ -8,7 +8,7 @@ "name": "socketio-examples", "version": "0.1.0", "dependencies": { - "express": "^4.21.1", + "express": "^4.21.2", "smoothie": "1.19.0", "socket.io": "^4.8.0", "socket.io-client": "^4.6.1" @@ -335,9 +335,9 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -358,7 +358,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -373,6 +373,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/encodeurl": { @@ -650,9 +654,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -1286,9 +1290,9 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -1309,7 +1313,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -1509,9 +1513,9 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "proxy-addr": { "version": "2.0.7", diff --git a/examples/client/javascript/package.json b/examples/client/javascript/package.json index 6e806da3..6bea0d87 100644 --- a/examples/client/javascript/package.json +++ b/examples/client/javascript/package.json @@ -2,7 +2,7 @@ "name": "socketio-examples", "version": "0.1.0", "dependencies": { - "express": "^4.21.1", + "express": "^4.21.2", "smoothie": "1.19.0", "socket.io": "^4.8.0", "socket.io-client": "^4.6.1" From f5b686c1044d3e9a0fe54d7270f0963dbd21f486 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Dec 2024 00:16:45 +0000 Subject: [PATCH 479/571] Bump django in /examples/server/wsgi/django_socketio (#1417) #nolog Bumps [django](https://github.com/django/django) from 4.2.16 to 4.2.17. - [Commits](https://github.com/django/django/compare/4.2.16...4.2.17) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/django_socketio/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/django_socketio/requirements.txt b/examples/server/wsgi/django_socketio/requirements.txt index 53a28118..98d2cf1f 100644 --- a/examples/server/wsgi/django_socketio/requirements.txt +++ b/examples/server/wsgi/django_socketio/requirements.txt @@ -1,6 +1,6 @@ asgiref==3.6.0 bidict==0.22.1 -Django==4.2.16 +Django==4.2.17 gunicorn==22.0.0 h11==0.14.0 python-engineio From b75d7309b75936017bc45eebfd5093b96f132e69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:02:49 +0000 Subject: [PATCH 480/571] Bump path-to-regexp and express in /examples/server/javascript (#1418) #nolog Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) to 0.1.12 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `path-to-regexp` from 0.1.10 to 0.1.12 - [Release notes](https://github.com/pillarjs/path-to-regexp/releases) - [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md) - [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.10...v0.1.12) Updates `express` from 4.21.1 to 4.21.2 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md) - [Commits](https://github.com/expressjs/express/compare/4.21.1...4.21.2) --- updated-dependencies: - dependency-name: path-to-regexp dependency-type: indirect - dependency-name: express dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/javascript/package-lock.json | 34 +++++++++++--------- examples/server/javascript/package.json | 2 +- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/examples/server/javascript/package-lock.json b/examples/server/javascript/package-lock.json index befb6751..4bae767a 100644 --- a/examples/server/javascript/package-lock.json +++ b/examples/server/javascript/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@socket.io/admin-ui": "^0.5.1", - "express": "^4.21.1", + "express": "^4.21.2", "smoothie": "1.19.0", "socket.io": "^4.8.0", "socket.io-client": "^4.6.1" @@ -338,9 +338,9 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -361,7 +361,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -376,6 +376,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -679,9 +683,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -1251,9 +1255,9 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -1274,7 +1278,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -1500,9 +1504,9 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "proxy-addr": { "version": "2.0.7", diff --git a/examples/server/javascript/package.json b/examples/server/javascript/package.json index b93cdb63..1e0e150c 100644 --- a/examples/server/javascript/package.json +++ b/examples/server/javascript/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "dependencies": { "@socket.io/admin-ui": "^0.5.1", - "express": "^4.21.1", + "express": "^4.21.2", "smoothie": "1.19.0", "socket.io": "^4.8.0", "socket.io-client": "^4.6.1" From bf5a05ae9bf94b2fbd1367a04884ef5a39cd2671 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 12 Dec 2024 01:54:15 -0800 Subject: [PATCH 481/571] server.py: teeny docstring typo fix (#1421) Noticed while perusing the documentation, so submitting a micropatch. --- src/socketio/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/socketio/server.py b/src/socketio/server.py index a40dcd90..883b37ab 100644 --- a/src/socketio/server.py +++ b/src/socketio/server.py @@ -87,7 +87,7 @@ class Server(base_server.BaseServer): is greater than this value. The default is 1024 bytes. :param cookie: If set to a string, it is the name of the HTTP cookie the - server sends back tot he client containing the client + server sends back to the client containing the client session id. If set to a dictionary, the ``'name'`` key contains the cookie name and other keys define cookie attributes, where the value of each attribute can be a From db642bb2bd9794eeceddd54abc47665c69e85406 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Thu, 12 Dec 2024 19:23:05 +0000 Subject: [PATCH 482/571] Upgrade the code to more recent Python versions --- examples/client/async/latency_client.py | 2 +- examples/client/sync/latency_client.py | 2 +- examples/simple-client/async/latency_client.py | 2 +- examples/simple-client/sync/latency_client.py | 2 +- src/socketio/admin.py | 2 +- src/socketio/async_client.py | 2 +- src/socketio/async_server.py | 2 +- src/socketio/base_manager.py | 3 +-- src/socketio/base_namespace.py | 2 +- src/socketio/client.py | 2 +- src/socketio/kafka_manager.py | 3 +-- src/socketio/packet.py | 2 +- src/socketio/redis_manager.py | 3 +-- src/socketio/server.py | 2 +- src/socketio/zmq_manager.py | 2 +- tests/async/test_manager.py | 2 +- tests/async/test_server.py | 4 ++-- tests/common/test_client.py | 2 +- tests/common/test_manager.py | 2 +- tests/common/test_server.py | 4 ++-- 20 files changed, 22 insertions(+), 25 deletions(-) diff --git a/examples/client/async/latency_client.py b/examples/client/async/latency_client.py index 00d25392..8f39549b 100644 --- a/examples/client/async/latency_client.py +++ b/examples/client/async/latency_client.py @@ -23,7 +23,7 @@ async def connect(): async def pong_from_server(): global start_timer latency = time.time() - start_timer - print('latency is {0:.2f} ms'.format(latency * 1000)) + print(f'latency is {latency * 1000:.2f} ms') await sio.sleep(1) if sio.connected: await send_ping() diff --git a/examples/client/sync/latency_client.py b/examples/client/sync/latency_client.py index 0328d100..240d214c 100644 --- a/examples/client/sync/latency_client.py +++ b/examples/client/sync/latency_client.py @@ -21,7 +21,7 @@ def connect(): def pong_from_server(): global start_timer latency = time.time() - start_timer - print('latency is {0:.2f} ms'.format(latency * 1000)) + print(f'latency is {latency * 1000:.2f} ms') sio.sleep(1) if sio.connected: send_ping() diff --git a/examples/simple-client/async/latency_client.py b/examples/simple-client/async/latency_client.py index 96387c65..8d69a850 100644 --- a/examples/simple-client/async/latency_client.py +++ b/examples/simple-client/async/latency_client.py @@ -12,7 +12,7 @@ async def main(): while (await sio.receive()) != ['pong_from_server']: pass latency = time.time() - start_timer - print('latency is {0:.2f} ms'.format(latency * 1000)) + print(f'latency is {latency * 1000:.2f} ms') await asyncio.sleep(1) diff --git a/examples/simple-client/sync/latency_client.py b/examples/simple-client/sync/latency_client.py index d5cd853e..c4dea110 100644 --- a/examples/simple-client/sync/latency_client.py +++ b/examples/simple-client/sync/latency_client.py @@ -11,7 +11,7 @@ def main(): while sio.receive() != ['pong_from_server']: pass latency = time.time() - start_timer - print('latency is {0:.2f} ms'.format(latency * 1000)) + print(f'latency is {latency * 1000:.2f} ms') time.sleep(1) diff --git a/src/socketio/admin.py b/src/socketio/admin.py index f317ea26..58c8aff9 100644 --- a/src/socketio/admin.py +++ b/src/socketio/admin.py @@ -16,7 +16,7 @@ def __init__(self): def push(self, type, count=1): timestamp = int(time.time()) * 1000 - key = '{};{}'.format(timestamp, type) + key = f'{timestamp};{type}' if key not in self.buffer: self.buffer[key] = { 'timestamp': timestamp, diff --git a/src/socketio/async_client.py b/src/socketio/async_client.py index e79fce0e..7bfd1ac3 100644 --- a/src/socketio/async_client.py +++ b/src/socketio/async_client.py @@ -386,7 +386,7 @@ async def _send_packet(self, pkt): async def _handle_connect(self, namespace, data): namespace = namespace or '/' if namespace not in self.namespaces: - self.logger.info('Namespace {} is connected'.format(namespace)) + self.logger.info(f'Namespace {namespace} is connected') self.namespaces[namespace] = (data or {}).get('sid', self.sid) await self._trigger_event('connect', namespace=namespace) self._connect_event.set() diff --git a/src/socketio/async_server.py b/src/socketio/async_server.py index 1e523ff1..9b0e9774 100644 --- a/src/socketio/async_server.py +++ b/src/socketio/async_server.py @@ -385,7 +385,7 @@ def on_message(sid, msg): async with eio.session(sid) as session: print('received message from ', session['username']) """ - class _session_context_manager(object): + class _session_context_manager: def __init__(self, server, sid, namespace): self.server = server self.sid = sid diff --git a/src/socketio/base_manager.py b/src/socketio/base_manager.py index ca4b0b95..dafa60ac 100644 --- a/src/socketio/base_manager.py +++ b/src/socketio/base_manager.py @@ -37,8 +37,7 @@ def get_participants(self, namespace, room): participants.update(ns[r]._fwdm if r in ns else {}) else: participants = ns[room]._fwdm.copy() if room in ns else {} - for sid, eio_sid in participants.items(): - yield sid, eio_sid + yield from participants.items() def connect(self, eio_sid, namespace): """Register a client connection to a namespace.""" diff --git a/src/socketio/base_namespace.py b/src/socketio/base_namespace.py index 354f75ac..14b5d8fb 100644 --- a/src/socketio/base_namespace.py +++ b/src/socketio/base_namespace.py @@ -1,4 +1,4 @@ -class BaseNamespace(object): +class BaseNamespace: def __init__(self, namespace=None): self.namespace = namespace or '/' diff --git a/src/socketio/client.py b/src/socketio/client.py index d7af4070..b5fa0b4e 100644 --- a/src/socketio/client.py +++ b/src/socketio/client.py @@ -363,7 +363,7 @@ def _send_packet(self, pkt): def _handle_connect(self, namespace, data): namespace = namespace or '/' if namespace not in self.namespaces: - self.logger.info('Namespace {} is connected'.format(namespace)) + self.logger.info(f'Namespace {namespace} is connected') self.namespaces[namespace] = (data or {}).get('sid', self.sid) self._trigger_event('connect', namespace=namespace) self._connect_event.set() diff --git a/src/socketio/kafka_manager.py b/src/socketio/kafka_manager.py index 4d87d46f..11b87ad8 100644 --- a/src/socketio/kafka_manager.py +++ b/src/socketio/kafka_manager.py @@ -57,8 +57,7 @@ def _publish(self, data): self.producer.flush() def _kafka_listen(self): - for message in self.consumer: - yield message + yield from self.consumer def _listen(self): for message in self._kafka_listen(): diff --git a/src/socketio/packet.py b/src/socketio/packet.py index ec1b364c..f7ad87e6 100644 --- a/src/socketio/packet.py +++ b/src/socketio/packet.py @@ -7,7 +7,7 @@ 'BINARY_EVENT', 'BINARY_ACK'] -class Packet(object): +class Packet: """Socket.IO packet.""" # the format of the Socket.IO packet is as follows: diff --git a/src/socketio/redis_manager.py b/src/socketio/redis_manager.py index ae9fa292..a16fb2c7 100644 --- a/src/socketio/redis_manager.py +++ b/src/socketio/redis_manager.py @@ -94,8 +94,7 @@ def _redis_listen_with_retries(self): self._redis_connect() self.pubsub.subscribe(self.channel) retry_sleep = 1 - for message in self.pubsub.listen(): - yield message + yield from self.pubsub.listen() except redis.exceptions.RedisError: logger.error('Cannot receive from redis... ' 'retrying in {} secs'.format(retry_sleep)) diff --git a/src/socketio/server.py b/src/socketio/server.py index 883b37ab..ae73df6a 100644 --- a/src/socketio/server.py +++ b/src/socketio/server.py @@ -363,7 +363,7 @@ def on_message(sid, msg): with sio.session(sid) as session: print('received message from ', session['username']) """ - class _session_context_manager(object): + class _session_context_manager: def __init__(self, server, sid, namespace): self.server = server self.sid = sid diff --git a/src/socketio/zmq_manager.py b/src/socketio/zmq_manager.py index 760fbc38..468dc268 100644 --- a/src/socketio/zmq_manager.py +++ b/src/socketio/zmq_manager.py @@ -66,7 +66,7 @@ def __init__(self, url='zmq+tcp://localhost:5555+5556', sink.connect(sink_url) sub = zmq.Context().socket(zmq.SUB) - sub.setsockopt_string(zmq.SUBSCRIBE, u'') + sub.setsockopt_string(zmq.SUBSCRIBE, '') sub.connect(sub_url) self.sink = sink diff --git a/tests/async/test_manager.py b/tests/async/test_manager.py index 5d60e039..77b9bdb7 100644 --- a/tests/async/test_manager.py +++ b/tests/async/test_manager.py @@ -392,7 +392,7 @@ def test_emit_binary(self): sid = _run(self.bm.connect('123', '/')) _run( self.bm.emit( - u'my event', b'my binary data', namespace='/', room=sid + 'my event', b'my binary data', namespace='/', room=sid ) ) assert self.bm.server._send_eio_packet.await_count == 2 diff --git a/tests/async/test_server.py b/tests/async/test_server.py index 704982bd..b2de48a9 100644 --- a/tests/async/test_server.py +++ b/tests/async/test_server.py @@ -963,7 +963,7 @@ async def on_baz(self, ns, sid, data1, data2): assert result['result'] == ('disconnect', '1', '/foo') def test_bad_namespace_handler(self, eio): - class Dummy(object): + class Dummy: pass class SyncNS(namespace.Namespace): @@ -1004,7 +1004,7 @@ def test_custom_json(self, eio): # Warning: this test cannot run in parallel with other tests, as it # changes the JSON encoding/decoding functions - class CustomJSON(object): + class CustomJSON: @staticmethod def dumps(*args, **kwargs): return '*** encoded ***' diff --git a/tests/common/test_client.py b/tests/common/test_client.py index c7de4b98..c7399dc1 100644 --- a/tests/common/test_client.py +++ b/tests/common/test_client.py @@ -145,7 +145,7 @@ class MyNamespace(namespace.ClientNamespace): assert c.namespace_handlers['/foo'] == n def test_namespace_handler_wrong_class(self): - class MyNamespace(object): + class MyNamespace: def __init__(self, n): pass diff --git a/tests/common/test_manager.py b/tests/common/test_manager.py index 65ed1cb2..5634c8a9 100644 --- a/tests/common/test_manager.py +++ b/tests/common/test_manager.py @@ -341,7 +341,7 @@ def test_emit_with_none(self): def test_emit_binary(self): sid = self.bm.connect('123', '/') - self.bm.emit(u'my event', b'my binary data', namespace='/', room=sid) + self.bm.emit('my event', b'my binary data', namespace='/', room=sid) assert self.bm.server._send_eio_packet.call_count == 2 assert self.bm.server._send_eio_packet.call_args_list[0][0][0] == '123' pkt = self.bm.server._send_eio_packet.call_args_list[0][0][1] diff --git a/tests/common/test_server.py b/tests/common/test_server.py index e6b02e5a..57ddc2f4 100644 --- a/tests/common/test_server.py +++ b/tests/common/test_server.py @@ -885,7 +885,7 @@ def on_baz(self, ns, sid, data1, data2): assert result['result'] == ('disconnect', '1', '/foo') def test_bad_namespace_handler(self, eio): - class Dummy(object): + class Dummy: pass class AsyncNS(namespace.Namespace): @@ -947,7 +947,7 @@ def test_custom_json(self, eio): # Warning: this test cannot run in parallel with other tests, as it # changes the JSON encoding/decoding functions - class CustomJSON(object): + class CustomJSON: @staticmethod def dumps(*args, **kwargs): return '*** encoded ***' From 78d1124c50cff149051fb89bf6f08000bf184da5 Mon Sep 17 00:00:00 2001 From: Arseny <36441601+arkuzo@users.noreply.github.com> Date: Sat, 14 Dec 2024 03:21:21 +0300 Subject: [PATCH 483/571] fix AsyncClient::wait unexpected return after success reconnect (#1407) * fix AsyncClient::wait unexpected return after success reconnect AsyncClient::wait use sleep(1) call to wait to start reconnect task. Sometimes reconnect is faster then 1 second, and wait returns while connection to server is established. Added one check to avoid this situation * Making added check easier to understand in source code * fix Client::wait unexpected return after success reconnect * fixes --------- Co-authored-by: Miguel Grinberg --- src/socketio/async_client.py | 3 +++ src/socketio/client.py | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/socketio/async_client.py b/src/socketio/async_client.py index 7bfd1ac3..0d85aa18 100644 --- a/src/socketio/async_client.py +++ b/src/socketio/async_client.py @@ -189,6 +189,9 @@ async def wait(self): await self.eio.wait() await self.sleep(1) # give the reconnect task time to start up if not self._reconnect_task: + if self.eio.state == 'connected': # pragma: no cover + # connected while sleeping above + continue break await self._reconnect_task if self.eio.state != 'connected': diff --git a/src/socketio/client.py b/src/socketio/client.py index b5fa0b4e..00d7961e 100644 --- a/src/socketio/client.py +++ b/src/socketio/client.py @@ -180,7 +180,12 @@ def wait(self): self.eio.wait() self.sleep(1) # give the reconnect task time to start up if not self._reconnect_task: - break + if self.eio.state == 'connected': # pragma: no cover + # connected while sleeping above + continue + else: + # the reconnect task gave up + break self._reconnect_task.join() if self.eio.state != 'connected': break From b6ee33e56cf2679664c1b894bf7e5d33a30976db Mon Sep 17 00:00:00 2001 From: humayunsr Date: Sat, 14 Dec 2024 15:42:51 +0500 Subject: [PATCH 484/571] Prevent multiple tasks for reconnection (#1369) * Prevent multiple taks for reconnection As discussed here. https://github.com/miguelgrinberg/python-socketio/discussions/1367 In certain scenarios, this library creates multiple reconnection tasks. A check is added to make sure that reconnection task starts only when this task is not running. Signed-off-by: Humayun Ajmal * async client --------- Signed-off-by: Humayun Ajmal Co-authored-by: Miguel Grinberg --- src/socketio/async_client.py | 2 +- src/socketio/client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/socketio/async_client.py b/src/socketio/async_client.py index 0d85aa18..fb1abc14 100644 --- a/src/socketio/async_client.py +++ b/src/socketio/async_client.py @@ -581,7 +581,7 @@ async def _handle_eio_disconnect(self): self.callbacks = {} self._binary_packet = None self.sid = None - if will_reconnect: + if will_reconnect and not self._reconnect_task: self._reconnect_task = self.start_background_task( self._handle_reconnect) diff --git a/src/socketio/client.py b/src/socketio/client.py index 00d7961e..c4f9eaa7 100644 --- a/src/socketio/client.py +++ b/src/socketio/client.py @@ -539,7 +539,7 @@ def _handle_eio_disconnect(self): self.callbacks = {} self._binary_packet = None self.sid = None - if will_reconnect: + if will_reconnect and not self._reconnect_task: self._reconnect_task = self.start_background_task( self._handle_reconnect) From 0b5c4638e5e4bff06fcf46476d218ae5ad4ada14 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 17 Dec 2024 20:24:37 +0000 Subject: [PATCH 485/571] Adopted pyenv-asyncio for async unit tests --- pyproject.toml | 4 + tests/async/helpers.py | 6 - tests/async/test_client.py | 477 +++++++++++------------ tests/async/test_manager.py | 336 ++++++++--------- tests/async/test_namespace.py | 151 ++++---- tests/async/test_pubsub_manager.py | 335 ++++++++--------- tests/async/test_server.py | 582 ++++++++++++++--------------- tests/async/test_simple_client.py | 69 ++-- tox.ini | 1 + 9 files changed, 923 insertions(+), 1038 deletions(-) delete mode 100644 tests/async/helpers.py diff --git a/pyproject.toml b/pyproject.toml index 54ba9d93..8a5453a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,3 +54,7 @@ namespaces = false [build-system] requires = ["setuptools>=61.2"] build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "session" diff --git a/tests/async/helpers.py b/tests/async/helpers.py deleted file mode 100644 index c9b708c9..00000000 --- a/tests/async/helpers.py +++ /dev/null @@ -1,6 +0,0 @@ -import asyncio - - -def _run(coro): - """Run the given coroutine.""" - return asyncio.get_event_loop().run_until_complete(coro) diff --git a/tests/async/test_client.py b/tests/async/test_client.py index 289abd94..26681b76 100644 --- a/tests/async/test_client.py +++ b/tests/async/test_client.py @@ -8,27 +8,24 @@ from engineio import exceptions as engineio_exceptions from socketio import exceptions from socketio import packet -from .helpers import _run class TestAsyncClient: - def test_is_asyncio_based(self): + async def test_is_asyncio_based(self): c = async_client.AsyncClient() assert c.is_asyncio_based() - def test_connect(self): + async def test_connect(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() - _run( - c.connect( - 'url', - headers='headers', - auth='auth', - transports='transports', - namespaces=['/foo', '/', '/bar'], - socketio_path='path', - wait=False, - ) + await c.connect( + 'url', + headers='headers', + auth='auth', + transports='transports', + namespaces=['/foo', '/', '/bar'], + socketio_path='path', + wait=False, ) assert c.connection_url == 'url' assert c.connection_headers == 'headers' @@ -43,22 +40,20 @@ def test_connect(self): engineio_path='path', ) - def test_connect_functions(self): + async def test_connect_functions(self): async def headers(): return 'headers' c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() - _run( - c.connect( - lambda: 'url', - headers=headers, - auth='auth', - transports='transports', - namespaces=['/foo', '/', '/bar'], - socketio_path='path', - wait=False, - ) + await c.connect( + lambda: 'url', + headers=headers, + auth='auth', + transports='transports', + namespaces=['/foo', '/', '/bar'], + socketio_path='path', + wait=False, ) c.eio.connect.assert_awaited_once_with( 'url', @@ -67,18 +62,16 @@ async def headers(): engineio_path='path', ) - def test_connect_one_namespace(self): + async def test_connect_one_namespace(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() - _run( - c.connect( - 'url', - headers='headers', - transports='transports', - namespaces='/foo', - socketio_path='path', - wait=False, - ) + await c.connect( + 'url', + headers='headers', + transports='transports', + namespaces='/foo', + socketio_path='path', + wait=False, ) assert c.connection_url == 'url' assert c.connection_headers == 'headers' @@ -92,20 +85,18 @@ def test_connect_one_namespace(self): engineio_path='path', ) - def test_connect_default_namespaces(self): + async def test_connect_default_namespaces(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() c.on('foo', mock.MagicMock(), namespace='/foo') c.on('bar', mock.MagicMock(), namespace='/') c.on('baz', mock.MagicMock(), namespace='*') - _run( - c.connect( - 'url', - headers='headers', - transports='transports', - socketio_path='path', - wait=False, - ) + await c.connect( + 'url', + headers='headers', + transports='transports', + socketio_path='path', + wait=False, ) assert c.connection_url == 'url' assert c.connection_headers == 'headers' @@ -120,17 +111,15 @@ def test_connect_default_namespaces(self): engineio_path='path', ) - def test_connect_no_namespaces(self): + async def test_connect_no_namespaces(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() - _run( - c.connect( - 'url', - headers='headers', - transports='transports', - socketio_path='path', - wait=False, - ) + await c.connect( + 'url', + headers='headers', + transports='transports', + socketio_path='path', + wait=False, ) assert c.connection_url == 'url' assert c.connection_headers == 'headers' @@ -144,7 +133,7 @@ def test_connect_no_namespaces(self): engineio_path='path', ) - def test_connect_error(self): + async def test_connect_error(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock( side_effect=engineio_exceptions.ConnectionError('foo') @@ -152,34 +141,28 @@ def test_connect_error(self): c.on('foo', mock.MagicMock(), namespace='/foo') c.on('bar', mock.MagicMock(), namespace='/') with pytest.raises(exceptions.ConnectionError): - _run( - c.connect( - 'url', - headers='headers', - transports='transports', - socketio_path='path', - wait=False, - ) + await c.connect( + 'url', + headers='headers', + transports='transports', + socketio_path='path', + wait=False, ) - def test_connect_twice(self): + async def test_connect_twice(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() - _run( - c.connect( - 'url', - wait=False, - ) + await c.connect( + 'url', + wait=False, ) with pytest.raises(exceptions.ConnectionError): - _run( - c.connect( - 'url', - wait=False, - ) + await c.connect( + 'url', + wait=False, ) - def test_connect_wait_single_namespace(self): + async def test_connect_wait_single_namespace(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() c._connect_event = mock.MagicMock() @@ -189,16 +172,14 @@ async def mock_connect(): return True c._connect_event.wait = mock_connect - _run( - c.connect( - 'url', - wait=True, - wait_timeout=0.01, - ) + await c.connect( + 'url', + wait=True, + wait_timeout=0.01, ) assert c.connected is True - def test_connect_wait_two_namespaces(self): + async def test_connect_wait_two_namespaces(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() c._connect_event = mock.MagicMock() @@ -213,41 +194,37 @@ async def mock_connect(): return False c._connect_event.wait = mock_connect - _run( - c.connect( - 'url', - namespaces=['/foo', '/bar'], - wait=True, - wait_timeout=0.01, - ) + await c.connect( + 'url', + namespaces=['/foo', '/bar'], + wait=True, + wait_timeout=0.01, ) assert c.connected is True assert c.namespaces == {'/bar': '123', '/foo': '456'} - def test_connect_timeout(self): + async def test_connect_timeout(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() c.disconnect = mock.AsyncMock() with pytest.raises(exceptions.ConnectionError): - _run( - c.connect( - 'url', - wait=True, - wait_timeout=0.01, - ) + await c.connect( + 'url', + wait=True, + wait_timeout=0.01, ) c.disconnect.assert_awaited_once_with() - def test_wait_no_reconnect(self): + async def test_wait_no_reconnect(self): c = async_client.AsyncClient() c.eio.wait = mock.AsyncMock() c.sleep = mock.AsyncMock() c._reconnect_task = None - _run(c.wait()) + await c.wait() c.eio.wait.assert_awaited_once_with() c.sleep.assert_awaited_once_with(1) - def test_wait_reconnect_failed(self): + async def test_wait_reconnect_failed(self): c = async_client.AsyncClient() c.eio.wait = mock.AsyncMock() c.sleep = mock.AsyncMock() @@ -257,11 +234,11 @@ async def fake_wait(): c.eio.state = states.pop(0) c._reconnect_task = fake_wait() - _run(c.wait()) + await c.wait() c.eio.wait.assert_awaited_once_with() c.sleep.assert_awaited_once_with(1) - def test_wait_reconnect_successful(self): + async def test_wait_reconnect_successful(self): c = async_client.AsyncClient() c.eio.wait = mock.AsyncMock() c.sleep = mock.AsyncMock() @@ -272,15 +249,15 @@ async def fake_wait(): c._reconnect_task = fake_wait() c._reconnect_task = fake_wait() - _run(c.wait()) + await c.wait() assert c.eio.wait.await_count == 2 assert c.sleep.await_count == 2 - def test_emit_no_arguments(self): + async def test_emit_no_arguments(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} c._send_packet = mock.AsyncMock() - _run(c.emit('foo')) + await c.emit('foo') expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=None) assert c._send_packet.await_count == 1 @@ -289,11 +266,11 @@ def test_emit_no_arguments(self): == expected_packet.encode() ) - def test_emit_one_argument(self): + async def test_emit_one_argument(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} c._send_packet = mock.AsyncMock() - _run(c.emit('foo', 'bar')) + await c.emit('foo', 'bar') expected_packet = packet.Packet( packet.EVENT, namespace='/', @@ -306,11 +283,11 @@ def test_emit_one_argument(self): == expected_packet.encode() ) - def test_emit_one_argument_list(self): + async def test_emit_one_argument_list(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} c._send_packet = mock.AsyncMock() - _run(c.emit('foo', ['bar', 'baz'])) + await c.emit('foo', ['bar', 'baz']) expected_packet = packet.Packet( packet.EVENT, namespace='/', @@ -323,11 +300,11 @@ def test_emit_one_argument_list(self): == expected_packet.encode() ) - def test_emit_two_arguments(self): + async def test_emit_two_arguments(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} c._send_packet = mock.AsyncMock() - _run(c.emit('foo', ('bar', 'baz'))) + await c.emit('foo', ('bar', 'baz')) expected_packet = packet.Packet( packet.EVENT, namespace='/', @@ -340,11 +317,11 @@ def test_emit_two_arguments(self): == expected_packet.encode() ) - def test_emit_namespace(self): + async def test_emit_namespace(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1'} c._send_packet = mock.AsyncMock() - _run(c.emit('foo', namespace='/foo')) + await c.emit('foo', namespace='/foo') expected_packet = packet.Packet( packet.EVENT, namespace='/foo', data=['foo'], id=None) assert c._send_packet.await_count == 1 @@ -353,18 +330,18 @@ def test_emit_namespace(self): == expected_packet.encode() ) - def test_emit_unknown_namespace(self): + async def test_emit_unknown_namespace(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1'} with pytest.raises(exceptions.BadNamespaceError): - _run(c.emit('foo', namespace='/bar')) + await c.emit('foo', namespace='/bar') - def test_emit_with_callback(self): + async def test_emit_with_callback(self): c = async_client.AsyncClient() c._send_packet = mock.AsyncMock() c._generate_ack_id = mock.MagicMock(return_value=123) c.namespaces = {'/': '1'} - _run(c.emit('foo', callback='cb')) + await c.emit('foo', callback='cb') expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=123) assert c._send_packet.await_count == 1 @@ -374,12 +351,12 @@ def test_emit_with_callback(self): ) c._generate_ack_id.assert_called_once_with('/', 'cb') - def test_emit_namespace_with_callback(self): + async def test_emit_namespace_with_callback(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1'} c._send_packet = mock.AsyncMock() c._generate_ack_id = mock.MagicMock(return_value=123) - _run(c.emit('foo', namespace='/foo', callback='cb')) + await c.emit('foo', namespace='/foo', callback='cb') expected_packet = packet.Packet( packet.EVENT, namespace='/foo', data=['foo'], id=123) assert c._send_packet.await_count == 1 @@ -389,11 +366,11 @@ def test_emit_namespace_with_callback(self): ) c._generate_ack_id.assert_called_once_with('/foo', 'cb') - def test_emit_binary(self): + async def test_emit_binary(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} c._send_packet = mock.AsyncMock() - _run(c.emit('foo', b'bar')) + await c.emit('foo', b'bar') expected_packet = packet.Packet( packet.EVENT, namespace='/', @@ -406,11 +383,11 @@ def test_emit_binary(self): == expected_packet.encode() ) - def test_emit_not_binary(self): + async def test_emit_not_binary(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} c._send_packet = mock.AsyncMock() - _run(c.emit('foo', 'bar')) + await c.emit('foo', 'bar') expected_packet = packet.Packet( packet.EVENT, namespace='/', @@ -423,23 +400,23 @@ def test_emit_not_binary(self): == expected_packet.encode() ) - def test_send(self): + async def test_send(self): c = async_client.AsyncClient() c.emit = mock.AsyncMock() - _run(c.send('data', 'namespace', 'callback')) + await c.send('data', 'namespace', 'callback') c.emit.assert_awaited_once_with( 'message', data='data', namespace='namespace', callback='callback' ) - def test_send_with_defaults(self): + async def test_send_with_defaults(self): c = async_client.AsyncClient() c.emit = mock.AsyncMock() - _run(c.send('data')) + await c.send('data') c.emit.assert_awaited_once_with( 'message', data='data', namespace=None, callback=None ) - def test_call(self): + async def test_call(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} @@ -450,7 +427,7 @@ async def fake_event_wait(): c._generate_ack_id = mock.MagicMock(return_value=123) c.eio = mock.MagicMock() c.eio.create_event.return_value.wait = fake_event_wait - assert _run(c.call('foo')) == ('foo', 321) + assert await c.call('foo') == ('foo', 321) expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=123) assert c._send_packet.await_count == 1 @@ -459,7 +436,7 @@ async def fake_event_wait(): == expected_packet.encode() ) - def test_call_with_timeout(self): + async def test_call_with_timeout(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} @@ -471,7 +448,7 @@ async def fake_event_wait(): c.eio = mock.MagicMock() c.eio.create_event.return_value.wait = fake_event_wait with pytest.raises(exceptions.TimeoutError): - _run(c.call('foo', timeout=0.01)) + await c.call('foo', timeout=0.01) expected_packet = packet.Packet( packet.EVENT, namespace='/', data=['foo'], id=123) assert c._send_packet.await_count == 1 @@ -480,7 +457,7 @@ async def fake_event_wait(): == expected_packet.encode() ) - def test_disconnect(self): + async def test_disconnect(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/': '1'} @@ -489,7 +466,7 @@ def test_disconnect(self): c.eio = mock.MagicMock() c.eio.disconnect = mock.AsyncMock() c.eio.state = 'connected' - _run(c.disconnect()) + await c.disconnect() assert c.connected assert c._trigger_event.await_count == 0 assert c._send_packet.await_count == 1 @@ -500,7 +477,7 @@ def test_disconnect(self): ) c.eio.disconnect.assert_awaited_once_with(abort=True) - def test_disconnect_namespaces(self): + async def test_disconnect_namespaces(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.AsyncMock() @@ -508,7 +485,7 @@ def test_disconnect_namespaces(self): c.eio = mock.MagicMock() c.eio.disconnect = mock.AsyncMock() c.eio.state = 'connected' - _run(c.disconnect()) + await c.disconnect() assert c._trigger_event.await_count == 0 assert c._send_packet.await_count == 2 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/foo') @@ -522,7 +499,7 @@ def test_disconnect_namespaces(self): == expected_packet.encode() ) - def test_start_background_task(self): + async def test_start_background_task(self): c = async_client.AsyncClient() c.eio.start_background_task = mock.MagicMock(return_value='foo') assert c.start_background_task('foo', 'bar', baz='baz') == 'foo' @@ -530,22 +507,22 @@ def test_start_background_task(self): 'foo', 'bar', baz='baz' ) - def test_sleep(self): + async def test_sleep(self): c = async_client.AsyncClient() c.eio.sleep = mock.AsyncMock() - _run(c.sleep(1.23)) + await c.sleep(1.23) c.eio.sleep.assert_awaited_once_with(1.23) - def test_send_packet(self): + async def test_send_packet(self): c = async_client.AsyncClient() c.eio.send = mock.AsyncMock() - _run(c._send_packet(packet.Packet(packet.EVENT, 'foo'))) + await c._send_packet(packet.Packet(packet.EVENT, 'foo')) c.eio.send.assert_awaited_once_with('2"foo"') - def test_send_packet_binary(self): + async def test_send_packet_binary(self): c = async_client.AsyncClient() c.eio.send = mock.AsyncMock() - _run(c._send_packet(packet.Packet(packet.EVENT, b'foo'))) + await c._send_packet(packet.Packet(packet.EVENT, b'foo')) assert c.eio.send.await_args_list == [ mock.call('51-{"_placeholder":true,"num":0}'), mock.call(b'foo'), @@ -554,52 +531,52 @@ def test_send_packet_binary(self): mock.call(b'foo'), ] - def test_send_packet_default_binary(self): + async def test_send_packet_default_binary(self): c = async_client.AsyncClient() c.eio.send = mock.AsyncMock() - _run(c._send_packet(packet.Packet(packet.EVENT, 'foo'))) + await c._send_packet(packet.Packet(packet.EVENT, 'foo')) c.eio.send.assert_awaited_once_with('2"foo"') - def test_handle_connect(self): + async def test_handle_connect(self): c = async_client.AsyncClient() c._connect_event = mock.MagicMock() c._trigger_event = mock.AsyncMock() c._send_packet = mock.AsyncMock() - _run(c._handle_connect('/', {'sid': '123'})) + await c._handle_connect('/', {'sid': '123'}) c._connect_event.set.assert_called_once_with() c._trigger_event.assert_awaited_once_with('connect', namespace='/') c._send_packet.assert_not_awaited() - def test_handle_connect_with_namespaces(self): + async def test_handle_connect_with_namespaces(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() c._trigger_event = mock.AsyncMock() c._send_packet = mock.AsyncMock() - _run(c._handle_connect('/', {'sid': '3'})) + await c._handle_connect('/', {'sid': '3'}) c._connect_event.set.assert_called_once_with() c._trigger_event.assert_awaited_once_with('connect', namespace='/') assert c.namespaces == {'/': '3', '/foo': '1', '/bar': '2'} - def test_handle_connect_namespace(self): + async def test_handle_connect_namespace(self): c = async_client.AsyncClient() c.namespaces = {'/foo': '1'} c._connect_event = mock.MagicMock() c._trigger_event = mock.AsyncMock() c._send_packet = mock.AsyncMock() - _run(c._handle_connect('/foo', {'sid': '123'})) - _run(c._handle_connect('/bar', {'sid': '2'})) + await c._handle_connect('/foo', {'sid': '123'}) + await c._handle_connect('/bar', {'sid': '2'}) assert c._trigger_event.await_count == 1 c._connect_event.set.assert_called_once_with() c._trigger_event.assert_awaited_once_with( 'connect', namespace='/bar') assert c.namespaces == {'/foo': '1', '/bar': '2'} - def test_handle_disconnect(self): + async def test_handle_disconnect(self): c = async_client.AsyncClient() c.connected = True c._trigger_event = mock.AsyncMock() - _run(c._handle_disconnect('/')) + await c._handle_disconnect('/') c._trigger_event.assert_any_await( 'disconnect', namespace='/' ) @@ -607,15 +584,15 @@ def test_handle_disconnect(self): '__disconnect_final', namespace='/' ) assert not c.connected - _run(c._handle_disconnect('/')) + await c._handle_disconnect('/') assert c._trigger_event.await_count == 2 - def test_handle_disconnect_namespace(self): + async def test_handle_disconnect_namespace(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.AsyncMock() - _run(c._handle_disconnect('/foo')) + await c._handle_disconnect('/foo') c._trigger_event.assert_any_await( 'disconnect', namespace='/foo' ) @@ -624,7 +601,7 @@ def test_handle_disconnect_namespace(self): ) assert c.namespaces == {'/bar': '2'} assert c.connected - _run(c._handle_disconnect('/bar')) + await c._handle_disconnect('/bar') c._trigger_event.assert_any_await( 'disconnect', namespace='/bar' ) @@ -634,12 +611,12 @@ def test_handle_disconnect_namespace(self): assert c.namespaces == {} assert not c.connected - def test_handle_disconnect_unknown_namespace(self): + async def test_handle_disconnect_unknown_namespace(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.AsyncMock() - _run(c._handle_disconnect('/baz')) + await c._handle_disconnect('/baz') c._trigger_event.assert_any_await( 'disconnect', namespace='/baz' ) @@ -649,30 +626,30 @@ def test_handle_disconnect_unknown_namespace(self): assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected - def test_handle_disconnect_default_namespaces(self): + async def test_handle_disconnect_default_namespaces(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.AsyncMock() - _run(c._handle_disconnect('/')) + await c._handle_disconnect('/') c._trigger_event.assert_any_await('disconnect', namespace='/') c._trigger_event.assert_any_await('__disconnect_final', namespace='/') assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected - def test_handle_event(self): + async def test_handle_event(self): c = async_client.AsyncClient() c._trigger_event = mock.AsyncMock() - _run(c._handle_event('/', None, ['foo', ('bar', 'baz')])) + await c._handle_event('/', None, ['foo', ('bar', 'baz')]) c._trigger_event.assert_awaited_once_with( 'foo', '/', ('bar', 'baz') ) - def test_handle_event_with_id_no_arguments(self): + async def test_handle_event_with_id_no_arguments(self): c = async_client.AsyncClient() c._trigger_event = mock.AsyncMock(return_value=None) c._send_packet = mock.AsyncMock() - _run(c._handle_event('/', 123, ['foo', ('bar', 'baz')])) + await c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_awaited_once_with( 'foo', '/', ('bar', 'baz') ) @@ -684,11 +661,11 @@ def test_handle_event_with_id_no_arguments(self): == expected_packet.encode() ) - def test_handle_event_with_id_one_argument(self): + async def test_handle_event_with_id_one_argument(self): c = async_client.AsyncClient() c._trigger_event = mock.AsyncMock(return_value='ret') c._send_packet = mock.AsyncMock() - _run(c._handle_event('/', 123, ['foo', ('bar', 'baz')])) + await c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_awaited_once_with( 'foo', '/', ('bar', 'baz') ) @@ -700,11 +677,11 @@ def test_handle_event_with_id_one_argument(self): == expected_packet.encode() ) - def test_handle_event_with_id_one_list_argument(self): + async def test_handle_event_with_id_one_list_argument(self): c = async_client.AsyncClient() c._trigger_event = mock.AsyncMock(return_value=['a', 'b']) c._send_packet = mock.AsyncMock() - _run(c._handle_event('/', 123, ['foo', ('bar', 'baz')])) + await c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_awaited_once_with( 'foo', '/', ('bar', 'baz') ) @@ -716,11 +693,11 @@ def test_handle_event_with_id_one_list_argument(self): == expected_packet.encode() ) - def test_handle_event_with_id_two_arguments(self): + async def test_handle_event_with_id_two_arguments(self): c = async_client.AsyncClient() c._trigger_event = mock.AsyncMock(return_value=('a', 'b')) c._send_packet = mock.AsyncMock() - _run(c._handle_event('/', 123, ['foo', ('bar', 'baz')])) + await c._handle_event('/', 123, ['foo', ('bar', 'baz')]) c._trigger_event.assert_awaited_once_with( 'foo', '/', ('bar', 'baz') ) @@ -732,37 +709,37 @@ def test_handle_event_with_id_two_arguments(self): == expected_packet.encode() ) - def test_handle_ack(self): + async def test_handle_ack(self): c = async_client.AsyncClient() mock_cb = mock.MagicMock() c.callbacks['/foo'] = {123: mock_cb} - _run(c._handle_ack('/foo', 123, ['bar', 'baz'])) + await c._handle_ack('/foo', 123, ['bar', 'baz']) mock_cb.assert_called_once_with('bar', 'baz') assert 123 not in c.callbacks['/foo'] - def test_handle_ack_async(self): + async def test_handle_ack_async(self): c = async_client.AsyncClient() mock_cb = mock.AsyncMock() c.callbacks['/foo'] = {123: mock_cb} - _run(c._handle_ack('/foo', 123, ['bar', 'baz'])) + await c._handle_ack('/foo', 123, ['bar', 'baz']) mock_cb.assert_awaited_once_with('bar', 'baz') assert 123 not in c.callbacks['/foo'] - def test_handle_ack_not_found(self): + async def test_handle_ack_not_found(self): c = async_client.AsyncClient() mock_cb = mock.MagicMock() c.callbacks['/foo'] = {123: mock_cb} - _run(c._handle_ack('/foo', 124, ['bar', 'baz'])) + await c._handle_ack('/foo', 124, ['bar', 'baz']) mock_cb.assert_not_called() assert 123 in c.callbacks['/foo'] - def test_handle_error(self): + async def test_handle_error(self): c = async_client.AsyncClient() c.connected = True c._connect_event = mock.MagicMock() c._trigger_event = mock.AsyncMock() c.namespaces = {'/foo': '1', '/bar': '2'} - _run(c._handle_error('/', 'error')) + await c._handle_error('/', 'error') assert c.namespaces == {} assert not c.connected c._connect_event.set.assert_called_once_with() @@ -770,25 +747,25 @@ def test_handle_error(self): 'connect_error', '/', 'error' ) - def test_handle_error_with_no_arguments(self): + async def test_handle_error_with_no_arguments(self): c = async_client.AsyncClient() c.connected = True c._connect_event = mock.MagicMock() c._trigger_event = mock.AsyncMock() c.namespaces = {'/foo': '1', '/bar': '2'} - _run(c._handle_error('/', None)) + await c._handle_error('/', None) assert c.namespaces == {} assert not c.connected c._connect_event.set.assert_called_once_with() c._trigger_event.assert_awaited_once_with('connect_error', '/') - def test_handle_error_namespace(self): + async def test_handle_error_namespace(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() c._trigger_event = mock.AsyncMock() - _run(c._handle_error('/bar', ['error', 'message'])) + await c._handle_error('/bar', ['error', 'message']) assert c.namespaces == {'/foo': '1'} assert c.connected c._connect_event.set.assert_called_once_with() @@ -796,52 +773,52 @@ def test_handle_error_namespace(self): 'connect_error', '/bar', 'error', 'message' ) - def test_handle_error_namespace_with_no_arguments(self): + async def test_handle_error_namespace_with_no_arguments(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() c._trigger_event = mock.AsyncMock() - _run(c._handle_error('/bar', None)) + await c._handle_error('/bar', None) assert c.namespaces == {'/foo': '1'} assert c.connected c._connect_event.set.assert_called_once_with() c._trigger_event.assert_awaited_once_with('connect_error', '/bar') - def test_handle_error_unknown_namespace(self): + async def test_handle_error_unknown_namespace(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} c._connect_event = mock.MagicMock() - _run(c._handle_error('/baz', 'error')) + await c._handle_error('/baz', 'error') assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected c._connect_event.set.assert_called_once_with() - def test_trigger_event(self): + async def test_trigger_event(self): c = async_client.AsyncClient() handler = mock.MagicMock() catchall_handler = mock.MagicMock() c.on('foo', handler) c.on('*', catchall_handler) - _run(c._trigger_event('foo', '/', 1, '2')) - _run(c._trigger_event('bar', '/', 1, '2', 3)) - _run(c._trigger_event('connect', '/')) # should not trigger + await c._trigger_event('foo', '/', 1, '2') + await c._trigger_event('bar', '/', 1, '2', 3) + await c._trigger_event('connect', '/') # should not trigger handler.assert_called_once_with(1, '2') catchall_handler.assert_called_once_with('bar', 1, '2', 3) - def test_trigger_event_namespace(self): + async def test_trigger_event_namespace(self): c = async_client.AsyncClient() handler = mock.AsyncMock() catchall_handler = mock.AsyncMock() c.on('foo', handler, namespace='/bar') c.on('*', catchall_handler, namespace='/bar') - _run(c._trigger_event('foo', '/bar', 1, '2')) - _run(c._trigger_event('bar', '/bar', 1, '2', 3)) + await c._trigger_event('foo', '/bar', 1, '2') + await c._trigger_event('bar', '/bar', 1, '2', 3) handler.assert_awaited_once_with(1, '2') catchall_handler.assert_awaited_once_with('bar', 1, '2', 3) - def test_trigger_event_class_namespace(self): + async def test_trigger_event_class_namespace(self): c = async_client.AsyncClient() result = [] @@ -851,10 +828,10 @@ def on_foo(self, a, b): result.append(b) c.register_namespace(MyNamespace('/')) - _run(c._trigger_event('foo', '/', 1, '2')) + await c._trigger_event('foo', '/', 1, '2') assert result == [1, '2'] - def test_trigger_event_with_catchall_class_namespace(self): + async def test_trigger_event_with_catchall_class_namespace(self): result = {} class MyNamespace(async_namespace.AsyncClientNamespace): @@ -875,18 +852,18 @@ def on_baz(self, ns, data1, data2): c = async_client.AsyncClient() c.register_namespace(MyNamespace('*')) - _run(c._trigger_event('connect', '/foo')) + await c._trigger_event('connect', '/foo') assert result['result'] == ('/foo',) - _run(c._trigger_event('foo', '/foo', 'a')) + await c._trigger_event('foo', '/foo', 'a') assert result['result'] == ('/foo', 'a') - _run(c._trigger_event('bar', '/foo')) + await c._trigger_event('bar', '/foo') assert result['result'] == 'bar/foo' - _run(c._trigger_event('baz', '/foo', 'a', 'b')) + await c._trigger_event('baz', '/foo', 'a', 'b') assert result['result'] == ('/foo', 'a', 'b') - _run(c._trigger_event('disconnect', '/foo')) + await c._trigger_event('disconnect', '/foo') assert result['result'] == ('disconnect', '/foo') - def test_trigger_event_unknown_namespace(self): + async def test_trigger_event_unknown_namespace(self): c = async_client.AsyncClient() result = [] @@ -896,7 +873,7 @@ def on_foo(self, a, b): result.append(b) c.register_namespace(MyNamespace('/')) - _run(c._trigger_event('foo', '/bar', 1, '2')) + await c._trigger_event('foo', '/bar', 1, '2') assert result == [] @mock.patch( @@ -905,13 +882,13 @@ def on_foo(self, a, b): side_effect=asyncio.TimeoutError, ) @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) - def test_handle_reconnect(self, random, wait_for): + async def test_handle_reconnect(self, random, wait_for): c = async_client.AsyncClient() c._reconnect_task = 'foo' c.connect = mock.AsyncMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) - _run(c._handle_reconnect()) + await c._handle_reconnect() assert wait_for.await_count == 3 assert [x[0][1] for x in asyncio.wait_for.await_args_list] == [ 1.5, @@ -926,13 +903,13 @@ def test_handle_reconnect(self, random, wait_for): side_effect=asyncio.TimeoutError, ) @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) - def test_handle_reconnect_max_delay(self, random, wait_for): + async def test_handle_reconnect_max_delay(self, random, wait_for): c = async_client.AsyncClient(reconnection_delay_max=3) c._reconnect_task = 'foo' c.connect = mock.AsyncMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) - _run(c._handle_reconnect()) + await c._handle_reconnect() assert wait_for.await_count == 3 assert [x[0][1] for x in asyncio.wait_for.await_args_list] == [ 1.5, @@ -947,7 +924,7 @@ def test_handle_reconnect_max_delay(self, random, wait_for): side_effect=asyncio.TimeoutError, ) @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) - def test_handle_reconnect_max_attempts(self, random, wait_for): + async def test_handle_reconnect_max_attempts(self, random, wait_for): c = async_client.AsyncClient(reconnection_attempts=2, logger=True) c.connection_namespaces = ['/'] c._reconnect_task = 'foo' @@ -955,7 +932,7 @@ def test_handle_reconnect_max_attempts(self, random, wait_for): c.connect = mock.AsyncMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) - _run(c._handle_reconnect()) + await c._handle_reconnect() assert wait_for.await_count == 2 assert [x[0][1] for x in asyncio.wait_for.await_args_list] == [ 1.5, @@ -971,7 +948,7 @@ def test_handle_reconnect_max_attempts(self, random, wait_for): side_effect=[asyncio.TimeoutError, None], ) @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) - def test_handle_reconnect_aborted(self, random, wait_for): + async def test_handle_reconnect_aborted(self, random, wait_for): c = async_client.AsyncClient(logger=True) c.connection_namespaces = ['/'] c._reconnect_task = 'foo' @@ -979,7 +956,7 @@ def test_handle_reconnect_aborted(self, random, wait_for): c.connect = mock.AsyncMock( side_effect=[ValueError, exceptions.ConnectionError, None] ) - _run(c._handle_reconnect()) + await c._handle_reconnect() assert wait_for.await_count == 2 assert [x[0][1] for x in asyncio.wait_for.await_args_list] == [ 1.5, @@ -989,7 +966,7 @@ def test_handle_reconnect_aborted(self, random, wait_for): c._trigger_event.assert_awaited_once_with('__disconnect_final', namespace='/') - def test_shutdown_disconnect(self): + async def test_shutdown_disconnect(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/': '1'} @@ -998,7 +975,7 @@ def test_shutdown_disconnect(self): c.eio = mock.MagicMock() c.eio.disconnect = mock.AsyncMock() c.eio.state = 'connected' - _run(c.shutdown()) + await c.shutdown() assert c._trigger_event.await_count == 0 assert c._send_packet.await_count == 1 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/') @@ -1008,7 +985,7 @@ def test_shutdown_disconnect(self): ) c.eio.disconnect.assert_awaited_once_with(abort=True) - def test_shutdown_disconnect_namespaces(self): + async def test_shutdown_disconnect_namespaces(self): c = async_client.AsyncClient() c.connected = True c.namespaces = {'/foo': '1', '/bar': '2'} @@ -1017,7 +994,7 @@ def test_shutdown_disconnect_namespaces(self): c.eio = mock.MagicMock() c.eio.disconnect = mock.AsyncMock() c.eio.state = 'connected' - _run(c.shutdown()) + await c.shutdown() assert c._trigger_event.await_count == 0 assert c._send_packet.await_count == 2 expected_packet = packet.Packet(packet.DISCONNECT, namespace='/foo') @@ -1032,7 +1009,7 @@ def test_shutdown_disconnect_namespaces(self): ) @mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5]) - def test_shutdown_reconnect(self, random): + async def test_shutdown_reconnect(self, random): c = async_client.AsyncClient() c.connection_namespaces = ['/'] c._reconnect_task = mock.AsyncMock()() @@ -1045,18 +1022,18 @@ async def r(): await c.shutdown() await task - _run(r()) + await r() c._trigger_event.assert_awaited_once_with('__disconnect_final', namespace='/') - def test_handle_eio_connect(self): + async def test_handle_eio_connect(self): c = async_client.AsyncClient() c.connection_namespaces = ['/', '/foo'] c.connection_auth = 'auth' c._send_packet = mock.AsyncMock() c.eio.sid = 'foo' assert c.sid is None - _run(c._handle_eio_connect()) + await c._handle_eio_connect() assert c.sid == 'foo' assert c._send_packet.await_count == 2 expected_packet = packet.Packet( @@ -1072,14 +1049,14 @@ def test_handle_eio_connect(self): == expected_packet.encode() ) - def test_handle_eio_connect_function(self): + async def test_handle_eio_connect_function(self): c = async_client.AsyncClient() c.connection_namespaces = ['/', '/foo'] c.connection_auth = lambda: 'auth' c._send_packet = mock.AsyncMock() c.eio.sid = 'foo' assert c.sid is None - _run(c._handle_eio_connect()) + await c._handle_eio_connect() assert c.sid == 'foo' assert c._send_packet.await_count == 2 expected_packet = packet.Packet( @@ -1095,7 +1072,7 @@ def test_handle_eio_connect_function(self): == expected_packet.encode() ) - def test_handle_eio_message(self): + async def test_handle_eio_message(self): c = async_client.AsyncClient() c._handle_connect = mock.AsyncMock() c._handle_disconnect = mock.AsyncMock() @@ -1103,48 +1080,46 @@ def test_handle_eio_message(self): c._handle_ack = mock.AsyncMock() c._handle_error = mock.AsyncMock() - _run(c._handle_eio_message('0{"sid":"123"}')) + await c._handle_eio_message('0{"sid":"123"}') c._handle_connect.assert_awaited_with(None, {'sid': '123'}) - _run(c._handle_eio_message('0/foo,{"sid":"123"}')) + await c._handle_eio_message('0/foo,{"sid":"123"}') c._handle_connect.assert_awaited_with('/foo', {'sid': '123'}) - _run(c._handle_eio_message('1')) + await c._handle_eio_message('1') c._handle_disconnect.assert_awaited_with(None) - _run(c._handle_eio_message('1/foo')) + await c._handle_eio_message('1/foo') c._handle_disconnect.assert_awaited_with('/foo') - _run(c._handle_eio_message('2["foo"]')) + await c._handle_eio_message('2["foo"]') c._handle_event.assert_awaited_with(None, None, ['foo']) - _run(c._handle_eio_message('3/foo,["bar"]')) + await c._handle_eio_message('3/foo,["bar"]') c._handle_ack.assert_awaited_with('/foo', None, ['bar']) - _run(c._handle_eio_message('4')) + await c._handle_eio_message('4') c._handle_error.assert_awaited_with(None, None) - _run(c._handle_eio_message('4"foo"')) + await c._handle_eio_message('4"foo"') c._handle_error.assert_awaited_with(None, 'foo') - _run(c._handle_eio_message('4["foo"]')) + await c._handle_eio_message('4["foo"]') c._handle_error.assert_awaited_with(None, ['foo']) - _run(c._handle_eio_message('4/foo')) + await c._handle_eio_message('4/foo') c._handle_error.assert_awaited_with('/foo', None) - _run(c._handle_eio_message('4/foo,["foo","bar"]')) + await c._handle_eio_message('4/foo,["foo","bar"]') c._handle_error.assert_awaited_with('/foo', ['foo', 'bar']) - _run(c._handle_eio_message('51-{"_placeholder":true,"num":0}')) + await c._handle_eio_message('51-{"_placeholder":true,"num":0}') assert c._binary_packet.packet_type == packet.BINARY_EVENT - _run(c._handle_eio_message(b'foo')) + await c._handle_eio_message(b'foo') c._handle_event.assert_awaited_with(None, None, b'foo') - _run( - c._handle_eio_message( - '62-/foo,{"1":{"_placeholder":true,"num":1},' - '"2":{"_placeholder":true,"num":0}}' - ) + await c._handle_eio_message( + '62-/foo,{"1":{"_placeholder":true,"num":1},' + '"2":{"_placeholder":true,"num":0}}' ) assert c._binary_packet.packet_type == packet.BINARY_ACK - _run(c._handle_eio_message(b'bar')) - _run(c._handle_eio_message(b'foo')) + await c._handle_eio_message(b'bar') + await c._handle_eio_message(b'foo') c._handle_ack.assert_awaited_with( '/foo', None, {'1': b'foo', '2': b'bar'} ) with pytest.raises(ValueError): - _run(c._handle_eio_message('9')) + await c._handle_eio_message('9') - def test_eio_disconnect(self): + async def test_eio_disconnect(self): c = async_client.AsyncClient() c.namespaces = {'/': '1'} c.connected = True @@ -1152,41 +1127,41 @@ def test_eio_disconnect(self): c.start_background_task = mock.MagicMock() c.sid = 'foo' c.eio.state = 'connected' - _run(c._handle_eio_disconnect()) + await c._handle_eio_disconnect() c._trigger_event.assert_awaited_once_with( 'disconnect', namespace='/' ) assert c.sid is None assert not c.connected - def test_eio_disconnect_namespaces(self): + async def test_eio_disconnect_namespaces(self): c = async_client.AsyncClient(reconnection=False) c.namespaces = {'/foo': '1', '/bar': '2'} c.connected = True c._trigger_event = mock.AsyncMock() c.sid = 'foo' c.eio.state = 'connected' - _run(c._handle_eio_disconnect()) + await c._handle_eio_disconnect() c._trigger_event.assert_any_await('disconnect', namespace='/foo') c._trigger_event.assert_any_await('disconnect', namespace='/bar') assert c.sid is None assert not c.connected - def test_eio_disconnect_reconnect(self): + async def test_eio_disconnect_reconnect(self): c = async_client.AsyncClient(reconnection=True) c.start_background_task = mock.MagicMock() c.eio.state = 'connected' - _run(c._handle_eio_disconnect()) + await c._handle_eio_disconnect() c.start_background_task.assert_called_once_with(c._handle_reconnect) - def test_eio_disconnect_self_disconnect(self): + async def test_eio_disconnect_self_disconnect(self): c = async_client.AsyncClient(reconnection=True) c.start_background_task = mock.MagicMock() c.eio.state = 'disconnected' - _run(c._handle_eio_disconnect()) + await c._handle_eio_disconnect() c.start_background_task.assert_not_called() - def test_eio_disconnect_no_reconnect(self): + async def test_eio_disconnect_no_reconnect(self): c = async_client.AsyncClient(reconnection=False) c.namespaces = {'/': '1'} c.connected = True @@ -1194,7 +1169,7 @@ def test_eio_disconnect_no_reconnect(self): c.start_background_task = mock.MagicMock() c.sid = 'foo' c.eio.state = 'connected' - _run(c._handle_eio_disconnect()) + await c._handle_eio_disconnect() c._trigger_event.assert_any_await( 'disconnect', namespace='/' ) diff --git a/tests/async/test_manager.py b/tests/async/test_manager.py index 77b9bdb7..fd5fe816 100644 --- a/tests/async/test_manager.py +++ b/tests/async/test_manager.py @@ -2,7 +2,6 @@ from socketio import async_manager from socketio import packet -from .helpers import _run class TestAsyncManager: @@ -23,8 +22,8 @@ def generate_id(): self.bm.set_server(mock_server) self.bm.initialize() - def test_connect(self): - sid = _run(self.bm.connect('123', '/foo')) + async def test_connect(self): + sid = await self.bm.connect('123', '/foo') assert None in self.bm.rooms['/foo'] assert sid in self.bm.rooms['/foo'] assert sid in self.bm.rooms['/foo'][None] @@ -33,9 +32,9 @@ def test_connect(self): assert dict(self.bm.rooms['/foo'][sid]) == {sid: '123'} assert self.bm.sid_from_eio_sid('123', '/foo') == sid - def test_pre_disconnect(self): - sid1 = _run(self.bm.connect('123', '/foo')) - sid2 = _run(self.bm.connect('456', '/foo')) + async def test_pre_disconnect(self): + sid1 = await self.bm.connect('123', '/foo') + sid2 = await self.bm.connect('456', '/foo') assert self.bm.is_connected(sid1, '/foo') assert self.bm.pre_disconnect(sid1, '/foo') == '123' assert self.bm.pending_disconnect == {'/foo': [sid1]} @@ -43,124 +42,124 @@ def test_pre_disconnect(self): assert self.bm.pre_disconnect(sid2, '/foo') == '456' assert self.bm.pending_disconnect == {'/foo': [sid1, sid2]} assert not self.bm.is_connected(sid2, '/foo') - _run(self.bm.disconnect(sid1, '/foo')) + await self.bm.disconnect(sid1, '/foo') assert self.bm.pending_disconnect == {'/foo': [sid2]} - _run(self.bm.disconnect(sid2, '/foo')) + await self.bm.disconnect(sid2, '/foo') assert self.bm.pending_disconnect == {} - def test_disconnect(self): - sid1 = _run(self.bm.connect('123', '/foo')) - sid2 = _run(self.bm.connect('456', '/foo')) - _run(self.bm.enter_room(sid1, '/foo', 'bar')) - _run(self.bm.enter_room(sid2, '/foo', 'baz')) - _run(self.bm.disconnect(sid1, '/foo')) + async def test_disconnect(self): + sid1 = await self.bm.connect('123', '/foo') + sid2 = await self.bm.connect('456', '/foo') + await self.bm.enter_room(sid1, '/foo', 'bar') + await self.bm.enter_room(sid2, '/foo', 'baz') + await self.bm.disconnect(sid1, '/foo') assert dict(self.bm.rooms['/foo'][None]) == {sid2: '456'} assert dict(self.bm.rooms['/foo'][sid2]) == {sid2: '456'} assert dict(self.bm.rooms['/foo']['baz']) == {sid2: '456'} - def test_disconnect_default_namespace(self): - sid1 = _run(self.bm.connect('123', '/')) - sid2 = _run(self.bm.connect('123', '/foo')) - sid3 = _run(self.bm.connect('456', '/')) - sid4 = _run(self.bm.connect('456', '/foo')) + async def test_disconnect_default_namespace(self): + sid1 = await self.bm.connect('123', '/') + sid2 = await self.bm.connect('123', '/foo') + sid3 = await self.bm.connect('456', '/') + sid4 = await self.bm.connect('456', '/foo') assert self.bm.is_connected(sid1, '/') assert self.bm.is_connected(sid2, '/foo') assert not self.bm.is_connected(sid2, '/') assert not self.bm.is_connected(sid1, '/foo') - _run(self.bm.disconnect(sid1, '/')) + await self.bm.disconnect(sid1, '/') assert not self.bm.is_connected(sid1, '/') assert self.bm.is_connected(sid2, '/foo') - _run(self.bm.disconnect(sid2, '/foo')) + await self.bm.disconnect(sid2, '/foo') assert not self.bm.is_connected(sid2, '/foo') assert dict(self.bm.rooms['/'][None]) == {sid3: '456'} assert dict(self.bm.rooms['/'][sid3]) == {sid3: '456'} assert dict(self.bm.rooms['/foo'][None]) == {sid4: '456'} assert dict(self.bm.rooms['/foo'][sid4]) == {sid4: '456'} - def test_disconnect_twice(self): - sid1 = _run(self.bm.connect('123', '/')) - sid2 = _run(self.bm.connect('123', '/foo')) - sid3 = _run(self.bm.connect('456', '/')) - sid4 = _run(self.bm.connect('456', '/foo')) - _run(self.bm.disconnect(sid1, '/')) - _run(self.bm.disconnect(sid2, '/foo')) - _run(self.bm.disconnect(sid1, '/')) - _run(self.bm.disconnect(sid2, '/foo')) + async def test_disconnect_twice(self): + sid1 = await self.bm.connect('123', '/') + sid2 = await self.bm.connect('123', '/foo') + sid3 = await self.bm.connect('456', '/') + sid4 = await self.bm.connect('456', '/foo') + await self.bm.disconnect(sid1, '/') + await self.bm.disconnect(sid2, '/foo') + await self.bm.disconnect(sid1, '/') + await self.bm.disconnect(sid2, '/foo') assert dict(self.bm.rooms['/'][None]) == {sid3: '456'} assert dict(self.bm.rooms['/'][sid3]) == {sid3: '456'} assert dict(self.bm.rooms['/foo'][None]) == {sid4: '456'} assert dict(self.bm.rooms['/foo'][sid4]) == {sid4: '456'} - def test_disconnect_all(self): - sid1 = _run(self.bm.connect('123', '/foo')) - sid2 = _run(self.bm.connect('456', '/foo')) - _run(self.bm.enter_room(sid1, '/foo', 'bar')) - _run(self.bm.enter_room(sid2, '/foo', 'baz')) - _run(self.bm.disconnect(sid1, '/foo')) - _run(self.bm.disconnect(sid2, '/foo')) + async def test_disconnect_all(self): + sid1 = await self.bm.connect('123', '/foo') + sid2 = await self.bm.connect('456', '/foo') + await self.bm.enter_room(sid1, '/foo', 'bar') + await self.bm.enter_room(sid2, '/foo', 'baz') + await self.bm.disconnect(sid1, '/foo') + await self.bm.disconnect(sid2, '/foo') assert self.bm.rooms == {} - def test_disconnect_with_callbacks(self): - sid1 = _run(self.bm.connect('123', '/')) - sid2 = _run(self.bm.connect('123', '/foo')) - sid3 = _run(self.bm.connect('456', '/foo')) + async def test_disconnect_with_callbacks(self): + sid1 = await self.bm.connect('123', '/') + sid2 = await self.bm.connect('123', '/foo') + sid3 = await self.bm.connect('456', '/foo') self.bm._generate_ack_id(sid1, 'f') self.bm._generate_ack_id(sid2, 'g') self.bm._generate_ack_id(sid3, 'h') - _run(self.bm.disconnect(sid2, '/foo')) + await self.bm.disconnect(sid2, '/foo') assert sid2 not in self.bm.callbacks - _run(self.bm.disconnect(sid1, '/')) + await self.bm.disconnect(sid1, '/') assert sid1 not in self.bm.callbacks assert sid3 in self.bm.callbacks - def test_trigger_sync_callback(self): - sid1 = _run(self.bm.connect('123', '/')) - sid2 = _run(self.bm.connect('123', '/foo')) + async def test_trigger_sync_callback(self): + sid1 = await self.bm.connect('123', '/') + sid2 = await self.bm.connect('123', '/foo') cb = mock.MagicMock() id1 = self.bm._generate_ack_id(sid1, cb) id2 = self.bm._generate_ack_id(sid2, cb) - _run(self.bm.trigger_callback(sid1, id1, ['foo'])) - _run(self.bm.trigger_callback(sid2, id2, ['bar', 'baz'])) + await self.bm.trigger_callback(sid1, id1, ['foo']) + await self.bm.trigger_callback(sid2, id2, ['bar', 'baz']) assert cb.call_count == 2 cb.assert_any_call('foo') cb.assert_any_call('bar', 'baz') - def test_trigger_async_callback(self): - sid1 = _run(self.bm.connect('123', '/')) - sid2 = _run(self.bm.connect('123', '/foo')) + async def test_trigger_async_callback(self): + sid1 = await self.bm.connect('123', '/') + sid2 = await self.bm.connect('123', '/foo') cb = mock.AsyncMock() id1 = self.bm._generate_ack_id(sid1, cb) id2 = self.bm._generate_ack_id(sid2, cb) - _run(self.bm.trigger_callback(sid1, id1, ['foo'])) - _run(self.bm.trigger_callback(sid2, id2, ['bar', 'baz'])) + await self.bm.trigger_callback(sid1, id1, ['foo']) + await self.bm.trigger_callback(sid2, id2, ['bar', 'baz']) assert cb.await_count == 2 cb.assert_any_await('foo') cb.assert_any_await('bar', 'baz') - def test_invalid_callback(self): - sid = _run(self.bm.connect('123', '/')) + async def test_invalid_callback(self): + sid = await self.bm.connect('123', '/') cb = mock.MagicMock() id = self.bm._generate_ack_id(sid, cb) # these should not raise an exception - _run(self.bm.trigger_callback('xxx', id, ['foo'])) - _run(self.bm.trigger_callback(sid, id + 1, ['foo'])) + await self.bm.trigger_callback('xxx', id, ['foo']) + await self.bm.trigger_callback(sid, id + 1, ['foo']) assert cb.call_count == 0 - def test_get_namespaces(self): + async def test_get_namespaces(self): assert list(self.bm.get_namespaces()) == [] - _run(self.bm.connect('123', '/')) - _run(self.bm.connect('123', '/foo')) + await self.bm.connect('123', '/') + await self.bm.connect('123', '/foo') namespaces = list(self.bm.get_namespaces()) assert len(namespaces) == 2 assert '/' in namespaces assert '/foo' in namespaces - def test_get_participants(self): - sid1 = _run(self.bm.connect('123', '/')) - sid2 = _run(self.bm.connect('456', '/')) - sid3 = _run(self.bm.connect('789', '/')) - _run(self.bm.disconnect(sid3, '/')) + async def test_get_participants(self): + sid1 = await self.bm.connect('123', '/') + sid2 = await self.bm.connect('456', '/') + sid3 = await self.bm.connect('789', '/') + await self.bm.disconnect(sid3, '/') assert sid3 not in self.bm.rooms['/'][None] participants = list(self.bm.get_participants('/', None)) assert len(participants) == 2 @@ -168,44 +167,42 @@ def test_get_participants(self): assert (sid2, '456') in participants assert (sid3, '789') not in participants - def test_leave_invalid_room(self): - sid = _run(self.bm.connect('123', '/foo')) - _run(self.bm.leave_room(sid, '/foo', 'baz')) - _run(self.bm.leave_room(sid, '/bar', 'baz')) + async def test_leave_invalid_room(self): + sid = await self.bm.connect('123', '/foo') + await self.bm.leave_room(sid, '/foo', 'baz') + await self.bm.leave_room(sid, '/bar', 'baz') - def test_no_room(self): + async def test_no_room(self): rooms = self.bm.get_rooms('123', '/foo') assert [] == rooms - def test_close_room(self): - sid = _run(self.bm.connect('123', '/foo')) - _run(self.bm.connect('456', '/foo')) - _run(self.bm.connect('789', '/foo')) - _run(self.bm.enter_room(sid, '/foo', 'bar')) - _run(self.bm.enter_room(sid, '/foo', 'bar')) - _run(self.bm.close_room('bar', '/foo')) + async def test_close_room(self): + sid = await self.bm.connect('123', '/foo') + await self.bm.connect('456', '/foo') + await self.bm.connect('789', '/foo') + await self.bm.enter_room(sid, '/foo', 'bar') + await self.bm.enter_room(sid, '/foo', 'bar') + await self.bm.close_room('bar', '/foo') from pprint import pprint pprint(self.bm.rooms) assert 'bar' not in self.bm.rooms['/foo'] - def test_close_invalid_room(self): + async def test_close_invalid_room(self): self.bm.close_room('bar', '/foo') - def test_rooms(self): - sid = _run(self.bm.connect('123', '/foo')) - _run(self.bm.enter_room(sid, '/foo', 'bar')) + async def test_rooms(self): + sid = await self.bm.connect('123', '/foo') + await self.bm.enter_room(sid, '/foo', 'bar') r = self.bm.get_rooms(sid, '/foo') assert len(r) == 2 assert sid in r assert 'bar' in r - def test_emit_to_sid(self): - sid = _run(self.bm.connect('123', '/foo')) - _run(self.bm.connect('456', '/foo')) - _run( - self.bm.emit( - 'my event', {'foo': 'bar'}, namespace='/foo', to=sid - ) + async def test_emit_to_sid(self): + sid = await self.bm.connect('123', '/foo') + await self.bm.connect('456', '/foo') + await self.bm.emit( + 'my event', {'foo': 'bar'}, namespace='/foo', to=sid ) assert self.bm.server._send_eio_packet.await_count == 1 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ @@ -213,16 +210,14 @@ def test_emit_to_sid(self): pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' - def test_emit_to_room(self): - sid1 = _run(self.bm.connect('123', '/foo')) - _run(self.bm.enter_room(sid1, '/foo', 'bar')) - sid2 = _run(self.bm.connect('456', '/foo')) - _run(self.bm.enter_room(sid2, '/foo', 'bar')) - _run(self.bm.connect('789', '/foo')) - _run( - self.bm.emit( - 'my event', {'foo': 'bar'}, namespace='/foo', room='bar' - ) + async def test_emit_to_room(self): + sid1 = await self.bm.connect('123', '/foo') + await self.bm.enter_room(sid1, '/foo', 'bar') + sid2 = await self.bm.connect('456', '/foo') + await self.bm.enter_room(sid2, '/foo', 'bar') + await self.bm.connect('789', '/foo') + await self.bm.emit( + 'my event', {'foo': 'bar'}, namespace='/foo', room='bar' ) assert self.bm.server._send_eio_packet.await_count == 2 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ @@ -234,18 +229,16 @@ def test_emit_to_room(self): == pkt assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' - def test_emit_to_rooms(self): - sid1 = _run(self.bm.connect('123', '/foo')) - _run(self.bm.enter_room(sid1, '/foo', 'bar')) - sid2 = _run(self.bm.connect('456', '/foo')) - _run(self.bm.enter_room(sid2, '/foo', 'bar')) - _run(self.bm.enter_room(sid2, '/foo', 'baz')) - sid3 = _run(self.bm.connect('789', '/foo')) - _run(self.bm.enter_room(sid3, '/foo', 'baz')) - _run( - self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', - room=['bar', 'baz']) - ) + async def test_emit_to_rooms(self): + sid1 = await self.bm.connect('123', '/foo') + await self.bm.enter_room(sid1, '/foo', 'bar') + sid2 = await self.bm.connect('456', '/foo') + await self.bm.enter_room(sid2, '/foo', 'bar') + await self.bm.enter_room(sid2, '/foo', 'baz') + sid3 = await self.bm.connect('789', '/foo') + await self.bm.enter_room(sid3, '/foo', 'baz') + await self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', + room=['bar', 'baz']) assert self.bm.server._send_eio_packet.await_count == 3 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' @@ -260,14 +253,14 @@ def test_emit_to_rooms(self): == pkt assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' - def test_emit_to_all(self): - sid1 = _run(self.bm.connect('123', '/foo')) - _run(self.bm.enter_room(sid1, '/foo', 'bar')) - sid2 = _run(self.bm.connect('456', '/foo')) - _run(self.bm.enter_room(sid2, '/foo', 'bar')) - _run(self.bm.connect('789', '/foo')) - _run(self.bm.connect('abc', '/bar')) - _run(self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo')) + async def test_emit_to_all(self): + sid1 = await self.bm.connect('123', '/foo') + await self.bm.enter_room(sid1, '/foo', 'bar') + sid2 = await self.bm.connect('456', '/foo') + await self.bm.enter_room(sid2, '/foo', 'bar') + await self.bm.connect('789', '/foo') + await self.bm.connect('abc', '/bar') + await self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo') assert self.bm.server._send_eio_packet.await_count == 3 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ == '123' @@ -282,17 +275,15 @@ def test_emit_to_all(self): == pkt assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' - def test_emit_to_all_skip_one(self): - sid1 = _run(self.bm.connect('123', '/foo')) - _run(self.bm.enter_room(sid1, '/foo', 'bar')) - sid2 = _run(self.bm.connect('456', '/foo')) - _run(self.bm.enter_room(sid2, '/foo', 'bar')) - _run(self.bm.connect('789', '/foo')) - _run(self.bm.connect('abc', '/bar')) - _run( - self.bm.emit( - 'my event', {'foo': 'bar'}, namespace='/foo', skip_sid=sid2 - ) + async def test_emit_to_all_skip_one(self): + sid1 = await self.bm.connect('123', '/foo') + await self.bm.enter_room(sid1, '/foo', 'bar') + sid2 = await self.bm.connect('456', '/foo') + await self.bm.enter_room(sid2, '/foo', 'bar') + await self.bm.connect('789', '/foo') + await self.bm.connect('abc', '/bar') + await self.bm.emit( + 'my event', {'foo': 'bar'}, namespace='/foo', skip_sid=sid2 ) assert self.bm.server._send_eio_packet.await_count == 2 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ @@ -304,20 +295,18 @@ def test_emit_to_all_skip_one(self): == pkt assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' - def test_emit_to_all_skip_two(self): - sid1 = _run(self.bm.connect('123', '/foo')) - _run(self.bm.enter_room(sid1, '/foo', 'bar')) - sid2 = _run(self.bm.connect('456', '/foo')) - _run(self.bm.enter_room(sid2, '/foo', 'bar')) - sid3 = _run(self.bm.connect('789', '/foo')) - _run(self.bm.connect('abc', '/bar')) - _run( - self.bm.emit( - 'my event', - {'foo': 'bar'}, - namespace='/foo', - skip_sid=[sid1, sid3], - ) + async def test_emit_to_all_skip_two(self): + sid1 = await self.bm.connect('123', '/foo') + await self.bm.enter_room(sid1, '/foo', 'bar') + sid2 = await self.bm.connect('456', '/foo') + await self.bm.enter_room(sid2, '/foo', 'bar') + sid3 = await self.bm.connect('789', '/foo') + await self.bm.connect('abc', '/bar') + await self.bm.emit( + 'my event', + {'foo': 'bar'}, + namespace='/foo', + skip_sid=[sid1, sid3], ) assert self.bm.server._send_eio_packet.await_count == 1 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ @@ -325,14 +314,12 @@ def test_emit_to_all_skip_two(self): pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event",{"foo":"bar"}]' - def test_emit_with_callback(self): - sid = _run(self.bm.connect('123', '/foo')) + async def test_emit_with_callback(self): + sid = await self.bm.connect('123', '/foo') self.bm._generate_ack_id = mock.MagicMock() self.bm._generate_ack_id.return_value = 11 - _run( - self.bm.emit( - 'my event', {'foo': 'bar'}, namespace='/foo', callback='cb' - ) + await self.bm.emit( + 'my event', {'foo': 'bar'}, namespace='/foo', callback='cb' ) self.bm._generate_ack_id.assert_called_once_with(sid, 'cb') assert self.bm.server._send_packet.await_count == 1 @@ -341,20 +328,17 @@ def test_emit_with_callback(self): pkt = self.bm.server._send_packet.await_args_list[0][0][1] assert pkt.encode() == '2/foo,11["my event",{"foo":"bar"}]' - def test_emit_to_invalid_room(self): - _run( - self.bm.emit('my event', {'foo': 'bar'}, namespace='/', room='123') - ) + async def test_emit_to_invalid_room(self): + await self.bm.emit('my event', {'foo': 'bar'}, namespace='/', + room='123') - def test_emit_to_invalid_namespace(self): - _run(self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo')) + async def test_emit_to_invalid_namespace(self): + await self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo') - def test_emit_with_tuple(self): - sid = _run(self.bm.connect('123', '/foo')) - _run( - self.bm.emit( - 'my event', ('foo', 'bar'), namespace='/foo', room=sid - ) + async def test_emit_with_tuple(self): + sid = await self.bm.connect('123', '/foo') + await self.bm.emit( + 'my event', ('foo', 'bar'), namespace='/foo', room=sid ) assert self.bm.server._send_eio_packet.await_count == 1 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ @@ -362,12 +346,10 @@ def test_emit_with_tuple(self): pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event","foo","bar"]' - def test_emit_with_list(self): - sid = _run(self.bm.connect('123', '/foo')) - _run( - self.bm.emit( - 'my event', ['foo', 'bar'], namespace='/foo', room=sid - ) + async def test_emit_with_list(self): + sid = await self.bm.connect('123', '/foo') + await self.bm.emit( + 'my event', ['foo', 'bar'], namespace='/foo', room=sid ) assert self.bm.server._send_eio_packet.await_count == 1 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ @@ -375,12 +357,10 @@ def test_emit_with_list(self): pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event",["foo","bar"]]' - def test_emit_with_none(self): - sid = _run(self.bm.connect('123', '/foo')) - _run( - self.bm.emit( - 'my event', None, namespace='/foo', room=sid - ) + async def test_emit_with_none(self): + sid = await self.bm.connect('123', '/foo') + await self.bm.emit( + 'my event', None, namespace='/foo', room=sid ) assert self.bm.server._send_eio_packet.await_count == 1 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ @@ -388,12 +368,10 @@ def test_emit_with_none(self): pkt = self.bm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42/foo,["my event"]' - def test_emit_binary(self): - sid = _run(self.bm.connect('123', '/')) - _run( - self.bm.emit( - 'my event', b'my binary data', namespace='/', room=sid - ) + async def test_emit_binary(self): + sid = await self.bm.connect('123', '/') + await self.bm.emit( + 'my event', b'my binary data', namespace='/', room=sid ) assert self.bm.server._send_eio_packet.await_count == 2 assert self.bm.server._send_eio_packet.await_args_list[0][0][0] \ diff --git a/tests/async/test_namespace.py b/tests/async/test_namespace.py index 62560159..ad9b1a03 100644 --- a/tests/async/test_namespace.py +++ b/tests/async/test_namespace.py @@ -1,11 +1,10 @@ from unittest import mock from socketio import async_namespace -from .helpers import _run class TestAsyncNamespace: - def test_connect_event(self): + async def test_connect_event(self): result = {} class MyNamespace(async_namespace.AsyncNamespace): @@ -14,10 +13,10 @@ async def on_connect(self, sid, environ): ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) - _run(ns.trigger_event('connect', 'sid', {'foo': 'bar'})) + await ns.trigger_event('connect', 'sid', {'foo': 'bar'}) assert result['result'] == ('sid', {'foo': 'bar'}) - def test_disconnect_event(self): + async def test_disconnect_event(self): result = {} class MyNamespace(async_namespace.AsyncNamespace): @@ -26,10 +25,10 @@ async def on_disconnect(self, sid): ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) - _run(ns.trigger_event('disconnect', 'sid')) + await ns.trigger_event('disconnect', 'sid') assert result['result'] == 'sid' - def test_sync_event(self): + async def test_sync_event(self): result = {} class MyNamespace(async_namespace.AsyncNamespace): @@ -38,10 +37,10 @@ def on_custom_message(self, sid, data): ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) - _run(ns.trigger_event('custom_message', 'sid', {'data': 'data'})) + await ns.trigger_event('custom_message', 'sid', {'data': 'data'}) assert result['result'] == ('sid', {'data': 'data'}) - def test_async_event(self): + async def test_async_event(self): result = {} class MyNamespace(async_namespace.AsyncNamespace): @@ -50,10 +49,10 @@ async def on_custom_message(self, sid, data): ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) - _run(ns.trigger_event('custom_message', 'sid', {'data': 'data'})) + await ns.trigger_event('custom_message', 'sid', {'data': 'data'}) assert result['result'] == ('sid', {'data': 'data'}) - def test_event_not_found(self): + async def test_event_not_found(self): result = {} class MyNamespace(async_namespace.AsyncNamespace): @@ -62,20 +61,17 @@ async def on_custom_message(self, sid, data): ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) - _run( - ns.trigger_event('another_custom_message', 'sid', {'data': 'data'}) - ) + await ns.trigger_event('another_custom_message', 'sid', + {'data': 'data'}) assert result == {} - def test_emit(self): + async def test_emit(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.emit = mock.AsyncMock() ns._set_server(mock_server) - _run( - ns.emit( - 'ev', data='data', to='room', skip_sid='skip', callback='cb' - ) + await ns.emit( + 'ev', data='data', to='room', skip_sid='skip', callback='cb' ) ns.server.emit.assert_awaited_with( 'ev', @@ -87,16 +83,14 @@ def test_emit(self): callback='cb', ignore_queue=False, ) - _run( - ns.emit( - 'ev', - data='data', - room='room', - skip_sid='skip', - namespace='/bar', - callback='cb', - ignore_queue=True, - ) + await ns.emit( + 'ev', + data='data', + room='room', + skip_sid='skip', + namespace='/bar', + callback='cb', + ignore_queue=True, ) ns.server.emit.assert_awaited_with( 'ev', @@ -109,12 +103,12 @@ def test_emit(self): ignore_queue=True, ) - def test_send(self): + async def test_send(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.send = mock.AsyncMock() ns._set_server(mock_server) - _run(ns.send(data='data', to='room', skip_sid='skip', callback='cb')) + await ns.send(data='data', to='room', skip_sid='skip', callback='cb') ns.server.send.assert_awaited_with( 'data', to='room', @@ -124,15 +118,13 @@ def test_send(self): callback='cb', ignore_queue=False, ) - _run( - ns.send( - data='data', - room='room', - skip_sid='skip', - namespace='/bar', - callback='cb', - ignore_queue=True, - ) + await ns.send( + data='data', + room='room', + skip_sid='skip', + namespace='/bar', + callback='cb', + ignore_queue=True, ) ns.server.send.assert_awaited_with( 'data', @@ -144,12 +136,12 @@ def test_send(self): ignore_queue=True, ) - def test_call(self): + async def test_call(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.call = mock.AsyncMock() ns._set_server(mock_server) - _run(ns.call('ev', data='data', to='sid')) + await ns.call('ev', data='data', to='sid') ns.server.call.assert_awaited_with( 'ev', data='data', @@ -159,8 +151,8 @@ def test_call(self): timeout=None, ignore_queue=False, ) - _run(ns.call('ev', data='data', sid='sid', namespace='/bar', - timeout=45, ignore_queue=True)) + await ns.call('ev', data='data', sid='sid', namespace='/bar', + timeout=45, ignore_queue=True) ns.server.call.assert_awaited_with( 'ev', data='data', @@ -171,45 +163,45 @@ def test_call(self): ignore_queue=True, ) - def test_enter_room(self): + async def test_enter_room(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.enter_room = mock.AsyncMock() ns._set_server(mock_server) - _run(ns.enter_room('sid', 'room')) + await ns.enter_room('sid', 'room') ns.server.enter_room.assert_awaited_with( 'sid', 'room', namespace='/foo' ) - _run(ns.enter_room('sid', 'room', namespace='/bar')) + await ns.enter_room('sid', 'room', namespace='/bar') ns.server.enter_room.assert_awaited_with( 'sid', 'room', namespace='/bar' ) - def test_leave_room(self): + async def test_leave_room(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.leave_room = mock.AsyncMock() ns._set_server(mock_server) - _run(ns.leave_room('sid', 'room')) + await ns.leave_room('sid', 'room') ns.server.leave_room.assert_awaited_with( 'sid', 'room', namespace='/foo' ) - _run(ns.leave_room('sid', 'room', namespace='/bar')) + await ns.leave_room('sid', 'room', namespace='/bar') ns.server.leave_room.assert_awaited_with( 'sid', 'room', namespace='/bar' ) - def test_close_room(self): + async def test_close_room(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.close_room = mock.AsyncMock() ns._set_server(mock_server) - _run(ns.close_room('room')) + await ns.close_room('room') ns.server.close_room.assert_awaited_with('room', namespace='/foo') - _run(ns.close_room('room', namespace='/bar')) + await ns.close_room('room', namespace='/bar') ns.server.close_room.assert_awaited_with('room', namespace='/bar') - def test_rooms(self): + async def test_rooms(self): ns = async_namespace.AsyncNamespace('/foo') ns._set_server(mock.MagicMock()) ns.rooms('sid') @@ -217,21 +209,21 @@ def test_rooms(self): ns.rooms('sid', namespace='/bar') ns.server.rooms.assert_called_with('sid', namespace='/bar') - def test_session(self): + async def test_session(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.get_session = mock.AsyncMock() mock_server.save_session = mock.AsyncMock() ns._set_server(mock_server) - _run(ns.get_session('sid')) + await ns.get_session('sid') ns.server.get_session.assert_awaited_with('sid', namespace='/foo') - _run(ns.get_session('sid', namespace='/bar')) + await ns.get_session('sid', namespace='/bar') ns.server.get_session.assert_awaited_with('sid', namespace='/bar') - _run(ns.save_session('sid', {'a': 'b'})) + await ns.save_session('sid', {'a': 'b'}) ns.server.save_session.assert_awaited_with( 'sid', {'a': 'b'}, namespace='/foo' ) - _run(ns.save_session('sid', {'a': 'b'}, namespace='/bar')) + await ns.save_session('sid', {'a': 'b'}, namespace='/bar') ns.server.save_session.assert_awaited_with( 'sid', {'a': 'b'}, namespace='/bar' ) @@ -240,17 +232,17 @@ def test_session(self): ns.session('sid', namespace='/bar') ns.server.session.assert_called_with('sid', namespace='/bar') - def test_disconnect(self): + async def test_disconnect(self): ns = async_namespace.AsyncNamespace('/foo') mock_server = mock.MagicMock() mock_server.disconnect = mock.AsyncMock() ns._set_server(mock_server) - _run(ns.disconnect('sid')) + await ns.disconnect('sid') ns.server.disconnect.assert_awaited_with('sid', namespace='/foo') - _run(ns.disconnect('sid', namespace='/bar')) + await ns.disconnect('sid', namespace='/bar') ns.server.disconnect.assert_awaited_with('sid', namespace='/bar') - def test_sync_event_client(self): + async def test_sync_event_client(self): result = {} class MyNamespace(async_namespace.AsyncClientNamespace): @@ -259,10 +251,10 @@ def on_custom_message(self, sid, data): ns = MyNamespace('/foo') ns._set_client(mock.MagicMock()) - _run(ns.trigger_event('custom_message', 'sid', {'data': 'data'})) + await ns.trigger_event('custom_message', 'sid', {'data': 'data'}) assert result['result'] == ('sid', {'data': 'data'}) - def test_async_event_client(self): + async def test_async_event_client(self): result = {} class MyNamespace(async_namespace.AsyncClientNamespace): @@ -271,10 +263,10 @@ async def on_custom_message(self, sid, data): ns = MyNamespace('/foo') ns._set_client(mock.MagicMock()) - _run(ns.trigger_event('custom_message', 'sid', {'data': 'data'})) + await ns.trigger_event('custom_message', 'sid', {'data': 'data'}) assert result['result'] == ('sid', {'data': 'data'}) - def test_event_not_found_client(self): + async def test_event_not_found_client(self): result = {} class MyNamespace(async_namespace.AsyncClientNamespace): @@ -283,57 +275,56 @@ async def on_custom_message(self, sid, data): ns = MyNamespace('/foo') ns._set_client(mock.MagicMock()) - _run( - ns.trigger_event('another_custom_message', 'sid', {'data': 'data'}) - ) + await ns.trigger_event('another_custom_message', 'sid', + {'data': 'data'}) assert result == {} - def test_emit_client(self): + async def test_emit_client(self): ns = async_namespace.AsyncClientNamespace('/foo') mock_client = mock.MagicMock() mock_client.emit = mock.AsyncMock() ns._set_client(mock_client) - _run(ns.emit('ev', data='data', callback='cb')) + await ns.emit('ev', data='data', callback='cb') ns.client.emit.assert_awaited_with( 'ev', data='data', namespace='/foo', callback='cb' ) - _run(ns.emit('ev', data='data', namespace='/bar', callback='cb')) + await ns.emit('ev', data='data', namespace='/bar', callback='cb') ns.client.emit.assert_awaited_with( 'ev', data='data', namespace='/bar', callback='cb' ) - def test_send_client(self): + async def test_send_client(self): ns = async_namespace.AsyncClientNamespace('/foo') mock_client = mock.MagicMock() mock_client.send = mock.AsyncMock() ns._set_client(mock_client) - _run(ns.send(data='data', callback='cb')) + await ns.send(data='data', callback='cb') ns.client.send.assert_awaited_with( 'data', namespace='/foo', callback='cb' ) - _run(ns.send(data='data', namespace='/bar', callback='cb')) + await ns.send(data='data', namespace='/bar', callback='cb') ns.client.send.assert_awaited_with( 'data', namespace='/bar', callback='cb' ) - def test_call_client(self): + async def test_call_client(self): ns = async_namespace.AsyncClientNamespace('/foo') mock_client = mock.MagicMock() mock_client.call = mock.AsyncMock() ns._set_client(mock_client) - _run(ns.call('ev', data='data')) + await ns.call('ev', data='data') ns.client.call.assert_awaited_with( 'ev', data='data', namespace='/foo', timeout=None ) - _run(ns.call('ev', data='data', namespace='/bar', timeout=45)) + await ns.call('ev', data='data', namespace='/bar', timeout=45) ns.client.call.assert_awaited_with( 'ev', data='data', namespace='/bar', timeout=45 ) - def test_disconnect_client(self): + async def test_disconnect_client(self): ns = async_namespace.AsyncClientNamespace('/foo') mock_client = mock.MagicMock() mock_client.disconnect = mock.AsyncMock() ns._set_client(mock_client) - _run(ns.disconnect()) + await ns.disconnect() ns.client.disconnect.assert_awaited_with() diff --git a/tests/async/test_pubsub_manager.py b/tests/async/test_pubsub_manager.py index 46928827..71d948a6 100644 --- a/tests/async/test_pubsub_manager.py +++ b/tests/async/test_pubsub_manager.py @@ -7,7 +7,6 @@ from socketio import async_manager from socketio import async_pubsub_manager from socketio import packet -from .helpers import _run class TestAsyncPubSubManager: @@ -31,18 +30,18 @@ def generate_id(): self.pm.host_id = '123456' self.pm.initialize() - def test_default_init(self): + async def test_default_init(self): assert self.pm.channel == 'socketio' self.pm.server.start_background_task.assert_called_once_with( self.pm._thread ) - def test_custom_init(self): + async def test_custom_init(self): pubsub = async_pubsub_manager.AsyncPubSubManager(channel='foo') assert pubsub.channel == 'foo' assert len(pubsub.host_id) == 32 - def test_write_only_init(self): + async def test_write_only_init(self): mock_server = mock.MagicMock() pm = async_pubsub_manager.AsyncPubSubManager(write_only=True) pm.set_server(mock_server) @@ -51,8 +50,8 @@ def test_write_only_init(self): assert len(pm.host_id) == 32 assert pm.server.start_background_task.call_count == 0 - def test_emit(self): - _run(self.pm.emit('foo', 'bar')) + async def test_emit(self): + await self.pm.emit('foo', 'bar') self.pm._publish.assert_awaited_once_with( { 'method': 'emit', @@ -66,9 +65,9 @@ def test_emit(self): } ) - def test_emit_with_to(self): + async def test_emit_with_to(self): sid = 'room-mate' - _run(self.pm.emit('foo', 'bar', to=sid)) + await self.pm.emit('foo', 'bar', to=sid) self.pm._publish.assert_awaited_once_with( { 'method': 'emit', @@ -82,8 +81,8 @@ def test_emit_with_to(self): } ) - def test_emit_with_namespace(self): - _run(self.pm.emit('foo', 'bar', namespace='/baz')) + async def test_emit_with_namespace(self): + await self.pm.emit('foo', 'bar', namespace='/baz') self.pm._publish.assert_awaited_once_with( { 'method': 'emit', @@ -97,8 +96,8 @@ def test_emit_with_namespace(self): } ) - def test_emit_with_room(self): - _run(self.pm.emit('foo', 'bar', room='baz')) + async def test_emit_with_room(self): + await self.pm.emit('foo', 'bar', room='baz') self.pm._publish.assert_awaited_once_with( { 'method': 'emit', @@ -112,8 +111,8 @@ def test_emit_with_room(self): } ) - def test_emit_with_skip_sid(self): - _run(self.pm.emit('foo', 'bar', skip_sid='baz')) + async def test_emit_with_skip_sid(self): + await self.pm.emit('foo', 'bar', skip_sid='baz') self.pm._publish.assert_awaited_once_with( { 'method': 'emit', @@ -127,11 +126,11 @@ def test_emit_with_skip_sid(self): } ) - def test_emit_with_callback(self): + async def test_emit_with_callback(self): with mock.patch.object( self.pm, '_generate_ack_id', return_value='123' ): - _run(self.pm.emit('foo', 'bar', room='baz', callback='cb')) + await self.pm.emit('foo', 'bar', room='baz', callback='cb') self.pm._publish.assert_awaited_once_with( { 'method': 'emit', @@ -145,24 +144,22 @@ def test_emit_with_callback(self): } ) - def test_emit_with_callback_without_server(self): + async def test_emit_with_callback_without_server(self): standalone_pm = async_pubsub_manager.AsyncPubSubManager() with pytest.raises(RuntimeError): - _run(standalone_pm.emit('foo', 'bar', callback='cb')) + await standalone_pm.emit('foo', 'bar', callback='cb') - def test_emit_with_callback_missing_room(self): + async def test_emit_with_callback_missing_room(self): with mock.patch.object( self.pm, '_generate_ack_id', return_value='123' ): with pytest.raises(ValueError): - _run(self.pm.emit('foo', 'bar', callback='cb')) + await self.pm.emit('foo', 'bar', callback='cb') - def test_emit_with_ignore_queue(self): - sid = _run(self.pm.connect('123', '/')) - _run( - self.pm.emit( - 'foo', 'bar', room=sid, namespace='/', ignore_queue=True - ) + async def test_emit_with_ignore_queue(self): + sid = await self.pm.connect('123', '/') + await self.pm.emit( + 'foo', 'bar', room=sid, namespace='/', ignore_queue=True ) self.pm._publish.assert_not_awaited() assert self.pm.server._send_eio_packet.await_count == 1 @@ -171,33 +168,33 @@ def test_emit_with_ignore_queue(self): pkt = self.pm.server._send_eio_packet.await_args_list[0][0][1] assert pkt.encode() == '42["foo","bar"]' - def test_can_disconnect(self): - sid = _run(self.pm.connect('123', '/')) - assert _run(self.pm.can_disconnect(sid, '/')) is True - _run(self.pm.can_disconnect(sid, '/foo')) + async def test_can_disconnect(self): + sid = await self.pm.connect('123', '/') + assert await self.pm.can_disconnect(sid, '/') is True + await self.pm.can_disconnect(sid, '/foo') self.pm._publish.assert_awaited_once_with( {'method': 'disconnect', 'sid': sid, 'namespace': '/foo', 'host_id': '123456'} ) - def test_disconnect(self): - _run(self.pm.disconnect('foo', '/')) + async def test_disconnect(self): + await self.pm.disconnect('foo', '/') self.pm._publish.assert_awaited_once_with( {'method': 'disconnect', 'sid': 'foo', 'namespace': '/', 'host_id': '123456'} ) - def test_disconnect_ignore_queue(self): - sid = _run(self.pm.connect('123', '/')) + async def test_disconnect_ignore_queue(self): + sid = await self.pm.connect('123', '/') self.pm.pre_disconnect(sid, '/') - _run(self.pm.disconnect(sid, '/', ignore_queue=True)) + await self.pm.disconnect(sid, '/', ignore_queue=True) self.pm._publish.assert_not_awaited() assert self.pm.is_connected(sid, '/') is False - def test_enter_room(self): - sid = _run(self.pm.connect('123', '/')) - _run(self.pm.enter_room(sid, '/', 'foo')) - _run(self.pm.enter_room('456', '/', 'foo')) + async def test_enter_room(self): + sid = await self.pm.connect('123', '/') + await self.pm.enter_room(sid, '/', 'foo') + await self.pm.enter_room('456', '/', 'foo') assert sid in self.pm.rooms['/']['foo'] assert self.pm.rooms['/']['foo'][sid] == '123' self.pm._publish.assert_awaited_once_with( @@ -205,35 +202,35 @@ def test_enter_room(self): 'namespace': '/', 'host_id': '123456'} ) - def test_leave_room(self): - sid = _run(self.pm.connect('123', '/')) - _run(self.pm.leave_room(sid, '/', 'foo')) - _run(self.pm.leave_room('456', '/', 'foo')) + async def test_leave_room(self): + sid = await self.pm.connect('123', '/') + await self.pm.leave_room(sid, '/', 'foo') + await self.pm.leave_room('456', '/', 'foo') assert 'foo' not in self.pm.rooms['/'] self.pm._publish.assert_awaited_once_with( {'method': 'leave_room', 'sid': '456', 'room': 'foo', 'namespace': '/', 'host_id': '123456'} ) - def test_close_room(self): - _run(self.pm.close_room('foo')) + async def test_close_room(self): + await self.pm.close_room('foo') self.pm._publish.assert_awaited_once_with( {'method': 'close_room', 'room': 'foo', 'namespace': '/', 'host_id': '123456'} ) - def test_close_room_with_namespace(self): - _run(self.pm.close_room('foo', '/bar')) + async def test_close_room_with_namespace(self): + await self.pm.close_room('foo', '/bar') self.pm._publish.assert_awaited_once_with( {'method': 'close_room', 'room': 'foo', 'namespace': '/bar', 'host_id': '123456'} ) - def test_handle_emit(self): + async def test_handle_emit(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: - _run(self.pm._handle_emit({'event': 'foo', 'data': 'bar'})) + await self.pm._handle_emit({'event': 'foo', 'data': 'bar'}) super_emit.assert_awaited_once_with( 'foo', 'bar', @@ -243,14 +240,12 @@ def test_handle_emit(self): callback=None, ) - def test_handle_emit_with_namespace(self): + async def test_handle_emit_with_namespace(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: - _run( - self.pm._handle_emit( - {'event': 'foo', 'data': 'bar', 'namespace': '/baz'} - ) + await self.pm._handle_emit( + {'event': 'foo', 'data': 'bar', 'namespace': '/baz'} ) super_emit.assert_awaited_once_with( 'foo', @@ -261,14 +256,12 @@ def test_handle_emit_with_namespace(self): callback=None, ) - def test_handle_emit_with_room(self): + async def test_handle_emit_with_room(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: - _run( - self.pm._handle_emit( - {'event': 'foo', 'data': 'bar', 'room': 'baz'} - ) + await self.pm._handle_emit( + {'event': 'foo', 'data': 'bar', 'room': 'baz'} ) super_emit.assert_awaited_once_with( 'foo', @@ -279,14 +272,12 @@ def test_handle_emit_with_room(self): callback=None, ) - def test_handle_emit_with_skip_sid(self): + async def test_handle_emit_with_skip_sid(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: - _run( - self.pm._handle_emit( - {'event': 'foo', 'data': 'bar', 'skip_sid': '123'} - ) + await self.pm._handle_emit( + {'event': 'foo', 'data': 'bar', 'skip_sid': '123'} ) super_emit.assert_awaited_once_with( 'foo', @@ -297,20 +288,18 @@ def test_handle_emit_with_skip_sid(self): callback=None, ) - def test_handle_emit_with_remote_callback(self): + async def test_handle_emit_with_remote_callback(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: - _run( - self.pm._handle_emit( - { - 'event': 'foo', - 'data': 'bar', - 'namespace': '/baz', - 'callback': ('sid', '/baz', 123), - 'host_id': 'x', - } - ) + await self.pm._handle_emit( + { + 'event': 'foo', + 'data': 'bar', + 'namespace': '/baz', + 'callback': ('sid', '/baz', 123), + 'host_id': 'x', + } ) assert super_emit.await_count == 1 assert super_emit.await_args[0] == ('foo', 'bar') @@ -320,7 +309,7 @@ def test_handle_emit_with_remote_callback(self): assert isinstance( super_emit.await_args[1]['callback'], functools.partial ) - _run(super_emit.await_args[1]['callback']('one', 2, 'three')) + await super_emit.await_args[1]['callback']('one', 2, 'three') self.pm._publish.assert_awaited_once_with( { 'method': 'callback', @@ -332,20 +321,18 @@ def test_handle_emit_with_remote_callback(self): } ) - def test_handle_emit_with_local_callback(self): + async def test_handle_emit_with_local_callback(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: - _run( - self.pm._handle_emit( - { - 'event': 'foo', - 'data': 'bar', - 'namespace': '/baz', - 'callback': ('sid', '/baz', 123), - 'host_id': self.pm.host_id, - } - ) + await self.pm._handle_emit( + { + 'event': 'foo', + 'data': 'bar', + 'namespace': '/baz', + 'callback': ('sid', '/baz', 123), + 'host_id': self.pm.host_id, + } ) assert super_emit.await_count == 1 assert super_emit.await_args[0] == ('foo', 'bar') @@ -355,163 +342,137 @@ def test_handle_emit_with_local_callback(self): assert isinstance( super_emit.await_args[1]['callback'], functools.partial ) - _run(super_emit.await_args[1]['callback']('one', 2, 'three')) + await super_emit.await_args[1]['callback']('one', 2, 'three') self.pm._publish.assert_not_awaited() - def test_handle_callback(self): + async def test_handle_callback(self): host_id = self.pm.host_id with mock.patch.object( self.pm, 'trigger_callback' ) as trigger: - _run( - self.pm._handle_callback( - { - 'method': 'callback', - 'host_id': host_id, - 'sid': 'sid', - 'namespace': '/', - 'id': 123, - 'args': ('one', 2), - } - ) + await self.pm._handle_callback( + { + 'method': 'callback', + 'host_id': host_id, + 'sid': 'sid', + 'namespace': '/', + 'id': 123, + 'args': ('one', 2), + } ) trigger.assert_awaited_once_with('sid', 123, ('one', 2)) - def test_handle_callback_bad_host_id(self): + async def test_handle_callback_bad_host_id(self): with mock.patch.object( self.pm, 'trigger_callback' ) as trigger: - _run( - self.pm._handle_callback( - { - 'method': 'callback', - 'host_id': 'bad', - 'sid': 'sid', - 'namespace': '/', - 'id': 123, - 'args': ('one', 2), - } - ) + await self.pm._handle_callback( + { + 'method': 'callback', + 'host_id': 'bad', + 'sid': 'sid', + 'namespace': '/', + 'id': 123, + 'args': ('one', 2), + } ) assert trigger.await_count == 0 - def test_handle_callback_missing_args(self): + async def test_handle_callback_missing_args(self): host_id = self.pm.host_id with mock.patch.object( self.pm, 'trigger_callback' ) as trigger: - _run( - self.pm._handle_callback( - { - 'method': 'callback', - 'host_id': host_id, - 'sid': 'sid', - 'namespace': '/', - 'id': 123, - } - ) + await self.pm._handle_callback( + { + 'method': 'callback', + 'host_id': host_id, + 'sid': 'sid', + 'namespace': '/', + 'id': 123, + } ) - _run( - self.pm._handle_callback( - { - 'method': 'callback', - 'host_id': host_id, - 'sid': 'sid', - 'namespace': '/', - } - ) + await self.pm._handle_callback( + { + 'method': 'callback', + 'host_id': host_id, + 'sid': 'sid', + 'namespace': '/', + } ) - _run( - self.pm._handle_callback( - {'method': 'callback', 'host_id': host_id, 'sid': 'sid'} - ) + await self.pm._handle_callback( + {'method': 'callback', 'host_id': host_id, 'sid': 'sid'} ) - _run( - self.pm._handle_callback( - {'method': 'callback', 'host_id': host_id} - ) + await self.pm._handle_callback( + {'method': 'callback', 'host_id': host_id} ) assert trigger.await_count == 0 - def test_handle_disconnect(self): - _run( - self.pm._handle_disconnect( - {'method': 'disconnect', 'sid': '123', 'namespace': '/foo'} - ) + async def test_handle_disconnect(self): + await self.pm._handle_disconnect( + {'method': 'disconnect', 'sid': '123', 'namespace': '/foo'} ) self.pm.server.disconnect.assert_awaited_once_with( sid='123', namespace='/foo', ignore_queue=True ) - def test_handle_enter_room(self): - sid = _run(self.pm.connect('123', '/')) + async def test_handle_enter_room(self): + sid = await self.pm.connect('123', '/') with mock.patch.object( async_manager.AsyncManager, 'enter_room' ) as super_enter_room: - _run( - self.pm._handle_enter_room( - {'method': 'enter_room', 'sid': sid, 'namespace': '/', - 'room': 'foo'} - ) + await self.pm._handle_enter_room( + {'method': 'enter_room', 'sid': sid, 'namespace': '/', + 'room': 'foo'} ) - _run( - self.pm._handle_enter_room( - {'method': 'enter_room', 'sid': '456', 'namespace': '/', - 'room': 'foo'} - ) + await self.pm._handle_enter_room( + {'method': 'enter_room', 'sid': '456', 'namespace': '/', + 'room': 'foo'} ) super_enter_room.assert_awaited_once_with(sid, '/', 'foo') - def test_handle_leave_room(self): - sid = _run(self.pm.connect('123', '/')) + async def test_handle_leave_room(self): + sid = await self.pm.connect('123', '/') with mock.patch.object( async_manager.AsyncManager, 'leave_room' ) as super_leave_room: - _run( - self.pm._handle_leave_room( - {'method': 'leave_room', 'sid': sid, 'namespace': '/', - 'room': 'foo'} - ) + await self.pm._handle_leave_room( + {'method': 'leave_room', 'sid': sid, 'namespace': '/', + 'room': 'foo'} ) - _run( - self.pm._handle_leave_room( - {'method': 'leave_room', 'sid': '456', 'namespace': '/', - 'room': 'foo'} - ) + await self.pm._handle_leave_room( + {'method': 'leave_room', 'sid': '456', 'namespace': '/', + 'room': 'foo'} ) super_leave_room.assert_awaited_once_with(sid, '/', 'foo') - def test_handle_close_room(self): + async def test_handle_close_room(self): with mock.patch.object( async_manager.AsyncManager, 'close_room' ) as super_close_room: - _run( - self.pm._handle_close_room( - {'method': 'close_room', 'room': 'foo'} - ) + await self.pm._handle_close_room( + {'method': 'close_room', 'room': 'foo'} ) super_close_room.assert_awaited_once_with( room='foo', namespace=None ) - def test_handle_close_room_with_namespace(self): + async def test_handle_close_room_with_namespace(self): with mock.patch.object( async_manager.AsyncManager, 'close_room' ) as super_close_room: - _run( - self.pm._handle_close_room( - { - 'method': 'close_room', - 'room': 'foo', - 'namespace': '/bar', - } - ) + await self.pm._handle_close_room( + { + 'method': 'close_room', + 'room': 'foo', + 'namespace': '/bar', + } ) super_close_room.assert_awaited_once_with( room='foo', namespace='/bar' ) - def test_background_thread(self): + async def test_background_thread(self): self.pm._handle_emit = mock.AsyncMock() self.pm._handle_callback = mock.AsyncMock() self.pm._handle_disconnect = mock.AsyncMock() @@ -548,7 +509,7 @@ async def messages(): 'host_id': host_id}) self.pm._listen = messages - _run(self.pm._thread()) + await self.pm._thread() self.pm._handle_emit.assert_awaited_once_with( {'method': 'emit', 'value': 'foo', 'host_id': 'x'} @@ -575,7 +536,7 @@ async def messages(): {'method': 'close_room', 'value': 'baz', 'host_id': 'x'} ) - def test_background_thread_exception(self): + async def test_background_thread_exception(self): self.pm._handle_emit = mock.AsyncMock(side_effect=[ ValueError(), asyncio.CancelledError]) @@ -584,7 +545,7 @@ async def messages(): yield {'method': 'emit', 'value': 'bar', 'host_id': 'x'} self.pm._listen = messages - _run(self.pm._thread()) + await self.pm._thread() self.pm._handle_emit.assert_any_await( {'method': 'emit', 'value': 'foo', 'host_id': 'x'} diff --git a/tests/async/test_server.py b/tests/async/test_server.py index b2de48a9..d9129d46 100644 --- a/tests/async/test_server.py +++ b/tests/async/test_server.py @@ -11,7 +11,6 @@ from socketio import exceptions from socketio import namespace from socketio import packet -from .helpers import _run @mock.patch('socketio.server.engineio.AsyncServer', **{ @@ -32,25 +31,25 @@ def _get_mock_manager(self): mgr.trigger_callback = mock.AsyncMock() return mgr - def test_create(self, eio): + async def test_create(self, eio): eio.return_value.handle_request = mock.AsyncMock() mgr = self._get_mock_manager() s = async_server.AsyncServer( client_manager=mgr, async_handlers=True, foo='bar' ) - _run(s.handle_request({})) - _run(s.handle_request({})) + await s.handle_request({}) + await s.handle_request({}) eio.assert_called_once_with(**{'foo': 'bar', 'async_handlers': False}) assert s.manager == mgr assert s.eio.on.call_count == 3 assert s.async_handlers - def test_attach(self, eio): + async def test_attach(self, eio): s = async_server.AsyncServer() s.attach('app', 'path') eio.return_value.attach.assert_called_once_with('app', 'path') - def test_on_event(self, eio): + async def test_on_event(self, eio): s = async_server.AsyncServer() @s.on('connect') @@ -67,18 +66,16 @@ def bar(): assert s.handlers['/']['disconnect'] == bar assert s.handlers['/foo']['disconnect'] == bar - def test_emit(self, eio): + async def test_emit(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) - _run( - s.emit( - 'my event', - {'foo': 'bar'}, - to='room', - skip_sid='123', - namespace='/foo', - callback='cb', - ) + await s.emit( + 'my event', + {'foo': 'bar'}, + to='room', + skip_sid='123', + namespace='/foo', + callback='cb', ) s.manager.emit.assert_awaited_once_with( 'my event', @@ -89,16 +86,14 @@ def test_emit(self, eio): callback='cb', ignore_queue=False, ) - _run( - s.emit( - 'my event', - {'foo': 'bar'}, - room='room', - skip_sid='123', - namespace='/foo', - callback='cb', - ignore_queue=True, - ) + await s.emit( + 'my event', + {'foo': 'bar'}, + room='room', + skip_sid='123', + namespace='/foo', + callback='cb', + ignore_queue=True, ) s.manager.emit.assert_awaited_with( 'my event', @@ -110,17 +105,15 @@ def test_emit(self, eio): ignore_queue=True, ) - def test_emit_default_namespace(self, eio): + async def test_emit_default_namespace(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) - _run( - s.emit( - 'my event', - {'foo': 'bar'}, - to='room', - skip_sid='123', - callback='cb', - ) + await s.emit( + 'my event', + {'foo': 'bar'}, + to='room', + skip_sid='123', + callback='cb', ) s.manager.emit.assert_awaited_once_with( 'my event', @@ -131,15 +124,13 @@ def test_emit_default_namespace(self, eio): callback='cb', ignore_queue=False, ) - _run( - s.emit( - 'my event', - {'foo': 'bar'}, - room='room', - skip_sid='123', - callback='cb', - ignore_queue=True, - ) + await s.emit( + 'my event', + {'foo': 'bar'}, + room='room', + skip_sid='123', + callback='cb', + ignore_queue=True, ) s.manager.emit.assert_awaited_with( 'my event', @@ -151,17 +142,15 @@ def test_emit_default_namespace(self, eio): ignore_queue=True, ) - def test_send(self, eio): + async def test_send(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) - _run( - s.send( - 'foo', - to='room', - skip_sid='123', - namespace='/foo', - callback='cb', - ) + await s.send( + 'foo', + to='room', + skip_sid='123', + namespace='/foo', + callback='cb', ) s.manager.emit.assert_awaited_once_with( 'message', @@ -172,15 +161,13 @@ def test_send(self, eio): callback='cb', ignore_queue=False, ) - _run( - s.send( - 'foo', - room='room', - skip_sid='123', - namespace='/foo', - callback='cb', - ignore_queue=True, - ) + await s.send( + 'foo', + room='room', + skip_sid='123', + namespace='/foo', + callback='cb', + ignore_queue=True, ) s.manager.emit.assert_awaited_with( 'message', @@ -192,7 +179,7 @@ def test_send(self, eio): ignore_queue=True, ) - def test_call(self, eio): + async def test_call(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) @@ -201,9 +188,9 @@ async def fake_event_wait(): return True s.eio.create_event.return_value.wait = fake_event_wait - assert _run(s.call('foo', sid='123')) == ('foo', 321) + assert await s.call('foo', sid='123') == ('foo', 321) - def test_call_with_timeout(self, eio): + async def test_call_with_timeout(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) @@ -212,253 +199,253 @@ async def fake_event_wait(): s.eio.create_event.return_value.wait = fake_event_wait with pytest.raises(exceptions.TimeoutError): - _run(s.call('foo', sid='123', timeout=0.01)) + await s.call('foo', sid='123', timeout=0.01) - def test_call_with_broadcast(self, eio): + async def test_call_with_broadcast(self, eio): s = async_server.AsyncServer() with pytest.raises(ValueError): - _run(s.call('foo')) + await s.call('foo') - def test_call_without_async_handlers(self, eio): + async def test_call_without_async_handlers(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer( client_manager=mgr, async_handlers=False ) with pytest.raises(RuntimeError): - _run(s.call('foo', sid='123', timeout=12)) + await s.call('foo', sid='123', timeout=12) - def test_enter_room(self, eio): + async def test_enter_room(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) - _run(s.enter_room('123', 'room', namespace='/foo')) + await s.enter_room('123', 'room', namespace='/foo') s.manager.enter_room.assert_awaited_once_with('123', '/foo', 'room') - def test_enter_room_default_namespace(self, eio): + async def test_enter_room_default_namespace(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) - _run(s.enter_room('123', 'room')) + await s.enter_room('123', 'room') s.manager.enter_room.assert_awaited_once_with('123', '/', 'room') - def test_leave_room(self, eio): + async def test_leave_room(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) - _run(s.leave_room('123', 'room', namespace='/foo')) + await s.leave_room('123', 'room', namespace='/foo') s.manager.leave_room.assert_awaited_once_with('123', '/foo', 'room') - def test_leave_room_default_namespace(self, eio): + async def test_leave_room_default_namespace(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) - _run(s.leave_room('123', 'room')) + await s.leave_room('123', 'room') s.manager.leave_room.assert_awaited_once_with('123', '/', 'room') - def test_close_room(self, eio): + async def test_close_room(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) - _run(s.close_room('room', namespace='/foo')) + await s.close_room('room', namespace='/foo') s.manager.close_room.assert_awaited_once_with('room', '/foo') - def test_close_room_default_namespace(self, eio): + async def test_close_room_default_namespace(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) - _run(s.close_room('room')) + await s.close_room('room') s.manager.close_room.assert_awaited_once_with('room', '/') - def test_rooms(self, eio): + async def test_rooms(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) s.rooms('123', namespace='/foo') s.manager.get_rooms.assert_called_once_with('123', '/foo') - def test_rooms_default_namespace(self, eio): + async def test_rooms_default_namespace(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) s.rooms('123') s.manager.get_rooms.assert_called_once_with('123', '/') - def test_handle_request(self, eio): + async def test_handle_request(self, eio): eio.return_value.handle_request = mock.AsyncMock() s = async_server.AsyncServer() - _run(s.handle_request('environ')) + await s.handle_request('environ') s.eio.handle_request.assert_awaited_once_with('environ') - def test_send_packet(self, eio): + async def test_send_packet(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() - _run(s._send_packet('123', packet.Packet( - packet.EVENT, ['my event', 'my data'], namespace='/foo'))) + await s._send_packet('123', packet.Packet( + packet.EVENT, ['my event', 'my data'], namespace='/foo')) s.eio.send.assert_awaited_once_with( '123', '2/foo,["my event","my data"]' ) - def test_send_eio_packet(self, eio): + async def test_send_eio_packet(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() - _run(s._send_eio_packet('123', eio_packet.Packet( - eio_packet.MESSAGE, 'hello'))) + await s._send_eio_packet('123', eio_packet.Packet( + eio_packet.MESSAGE, 'hello')) assert s.eio.send_packet.await_count == 1 assert s.eio.send_packet.await_args_list[0][0][0] == '123' pkt = s.eio.send_packet.await_args_list[0][0][1] assert pkt.encode() == '4hello' - def test_transport(self, eio): + async def test_transport(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.eio.transport = mock.MagicMock(return_value='polling') - sid_foo = _run(s.manager.connect('123', '/foo')) + sid_foo = await s.manager.connect('123', '/foo') assert s.transport(sid_foo, '/foo') == 'polling' s.eio.transport.assert_called_once_with('123') - def test_handle_connect(self, eio): + async def test_handle_connect(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.initialize = mock.MagicMock() handler = mock.MagicMock() s.on('connect', handler) - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') assert s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 - _run(s._handle_eio_connect('456', 'environ')) - _run(s._handle_eio_message('456', '0')) + await s._handle_eio_connect('456', 'environ') + await s._handle_eio_message('456', '0') assert s.manager.initialize.call_count == 1 - def test_handle_connect_with_auth(self, eio): + async def test_handle_connect_with_auth(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.initialize = mock.MagicMock() handler = mock.MagicMock() s.on('connect', handler) - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0{"token":"abc"}')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0{"token":"abc"}') assert s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ', {'token': 'abc'}) s.eio.send.assert_awaited_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 - _run(s._handle_eio_connect('456', 'environ')) - _run(s._handle_eio_message('456', '0')) + await s._handle_eio_connect('456', 'environ') + await s._handle_eio_message('456', '0') assert s.manager.initialize.call_count == 1 - def test_handle_connect_with_auth_none(self, eio): + async def test_handle_connect_with_auth_none(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.initialize = mock.MagicMock() handler = mock.MagicMock(side_effect=[TypeError, None, None]) s.on('connect', handler) - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') assert s.manager.is_connected('1', '/') handler.assert_called_with('1', 'environ', None) s.eio.send.assert_awaited_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 - _run(s._handle_eio_connect('456', 'environ')) - _run(s._handle_eio_message('456', '0')) + await s._handle_eio_connect('456', 'environ') + await s._handle_eio_message('456', '0') assert s.manager.initialize.call_count == 1 - def test_handle_connect_async(self, eio): + async def test_handle_connect_async(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.initialize = mock.MagicMock() handler = mock.AsyncMock() s.on('connect', handler) - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') assert s.manager.is_connected('1', '/') handler.assert_awaited_once_with('1', 'environ') s.eio.send.assert_awaited_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 - _run(s._handle_eio_connect('456', 'environ')) - _run(s._handle_eio_message('456', '0')) + await s._handle_eio_connect('456', 'environ') + await s._handle_eio_message('456', '0') assert s.manager.initialize.call_count == 1 - def test_handle_connect_with_default_implied_namespaces(self, eio): + async def test_handle_connect_with_default_implied_namespaces(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) - _run(s._handle_eio_message('123', '0/foo,')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') + await s._handle_eio_message('123', '0/foo,') assert s.manager.is_connected('1', '/') assert not s.manager.is_connected('2', '/foo') - def test_handle_connect_with_implied_namespaces(self, eio): + async def test_handle_connect_with_implied_namespaces(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(namespaces=['/foo']) - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) - _run(s._handle_eio_message('123', '0/foo,')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') + await s._handle_eio_message('123', '0/foo,') assert not s.manager.is_connected('1', '/') assert s.manager.is_connected('1', '/foo') - def test_handle_connect_with_all_implied_namespaces(self, eio): + async def test_handle_connect_with_all_implied_namespaces(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(namespaces='*') - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) - _run(s._handle_eio_message('123', '0/foo,')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') + await s._handle_eio_message('123', '0/foo,') assert s.manager.is_connected('1', '/') assert s.manager.is_connected('2', '/foo') - def test_handle_connect_namespace(self, eio): + async def test_handle_connect_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock() s.on('connect', handler, namespace='/foo') - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0/foo,')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0/foo,') assert s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with('123', '0/foo,{"sid":"1"}') - def test_handle_connect_always_connect(self, eio): + async def test_handle_connect_always_connect(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(always_connect=True) s.manager.initialize = mock.MagicMock() handler = mock.MagicMock() s.on('connect', handler) - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') assert s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with('123', '0{"sid":"1"}') assert s.manager.initialize.call_count == 1 - _run(s._handle_eio_connect('456', 'environ')) - _run(s._handle_eio_message('456', '0')) + await s._handle_eio_connect('456', 'environ') + await s._handle_eio_message('456', '0') assert s.manager.initialize.call_count == 1 - def test_handle_connect_rejected(self, eio): + async def test_handle_connect_rejected(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock(return_value=False) s.on('connect', handler) - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with( '123', '4{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} - def test_handle_connect_namespace_rejected(self, eio): + async def test_handle_connect_namespace_rejected(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock(return_value=False) s.on('connect', handler, namespace='/foo') - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0/foo,')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0/foo,') assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_any_await( '123', '4/foo,{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} - def test_handle_connect_rejected_always_connect(self, eio): + async def test_handle_connect_rejected_always_connect(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(always_connect=True) handler = mock.MagicMock(return_value=False) s.on('connect', handler) - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') assert not s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_any_await('123', '0{"sid":"1"}') @@ -466,13 +453,13 @@ def test_handle_connect_rejected_always_connect(self, eio): '123', '1{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} - def test_handle_connect_namespace_rejected_always_connect(self, eio): + async def test_handle_connect_namespace_rejected_always_connect(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(always_connect=True) handler = mock.MagicMock(return_value=False) s.on('connect', handler, namespace='/foo') - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0/foo,')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0/foo,') assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_any_await('123', '0/foo,{"sid":"1"}') @@ -480,37 +467,37 @@ def test_handle_connect_namespace_rejected_always_connect(self, eio): '123', '1/foo,{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} - def test_handle_connect_rejected_with_exception(self, eio): + async def test_handle_connect_rejected_with_exception(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError('fail_reason') ) s.on('connect', handler) - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') assert not s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with( '123', '4{"message":"fail_reason"}') assert s.environ == {'123': 'environ'} - def test_handle_connect_rejected_with_empty_exception(self, eio): + async def test_handle_connect_rejected_with_empty_exception(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError() ) s.on('connect', handler) - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') assert not s.manager.is_connected('1', '/') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with( '123', '4{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} - def test_handle_connect_namespace_rejected_with_exception(self, eio): + async def test_handle_connect_namespace_rejected_with_exception(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock( @@ -518,109 +505,110 @@ def test_handle_connect_namespace_rejected_with_exception(self, eio): 'fail_reason', 1, '2') ) s.on('connect', handler, namespace='/foo') - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0/foo,')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0/foo,') assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with( '123', '4/foo,{"message":"fail_reason","data":[1,"2"]}') assert s.environ == {'123': 'environ'} - def test_handle_connect_namespace_rejected_with_empty_exception(self, eio): + async def test_handle_connect_namespace_rejected_with_empty_exception( + self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock( side_effect=exceptions.ConnectionRefusedError() ) s.on('connect', handler, namespace='/foo') - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0/foo,')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0/foo,') assert not s.manager.is_connected('1', '/foo') handler.assert_called_once_with('1', 'environ') s.eio.send.assert_awaited_once_with( '123', '4/foo,{"message":"Connection rejected by server"}') assert s.environ == {'123': 'environ'} - def test_handle_disconnect(self, eio): + async def test_handle_disconnect(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.manager.disconnect = mock.AsyncMock() handler = mock.MagicMock() s.on('disconnect', handler) - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) - _run(s._handle_eio_disconnect('123')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') + await s._handle_eio_disconnect('123') handler.assert_called_once_with('1') s.manager.disconnect.assert_awaited_once_with( '1', '/', ignore_queue=True) assert s.environ == {} - def test_handle_disconnect_namespace(self, eio): + async def test_handle_disconnect_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock() s.on('disconnect', handler) handler_namespace = mock.MagicMock() s.on('disconnect', handler_namespace, namespace='/foo') - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0/foo,')) - _run(s._handle_eio_disconnect('123')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0/foo,') + await s._handle_eio_disconnect('123') handler.assert_not_called() handler_namespace.assert_called_once_with('1') assert s.environ == {} - def test_handle_disconnect_only_namespace(self, eio): + async def test_handle_disconnect_only_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() handler = mock.MagicMock() s.on('disconnect', handler) handler_namespace = mock.MagicMock() s.on('disconnect', handler_namespace, namespace='/foo') - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0/foo,')) - _run(s._handle_eio_message('123', '1/foo,')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0/foo,') + await s._handle_eio_message('123', '1/foo,') assert handler.call_count == 0 handler_namespace.assert_called_once_with('1') assert s.environ == {'123': 'environ'} - def test_handle_disconnect_unknown_client(self, eio): + async def test_handle_disconnect_unknown_client(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) - _run(s._handle_eio_disconnect('123')) + await s._handle_eio_disconnect('123') - def test_handle_event(self, eio): + async def test_handle_event(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) - sid = _run(s.manager.connect('123', '/')) + sid = await s.manager.connect('123', '/') handler = mock.AsyncMock() catchall_handler = mock.AsyncMock() s.on('msg', handler) s.on('*', catchall_handler) - _run(s._handle_eio_message('123', '2["msg","a","b"]')) - _run(s._handle_eio_message('123', '2["my message","a","b","c"]')) + await s._handle_eio_message('123', '2["msg","a","b"]') + await s._handle_eio_message('123', '2["my message","a","b","c"]') handler.assert_awaited_once_with(sid, 'a', 'b') catchall_handler.assert_awaited_once_with( 'my message', sid, 'a', 'b', 'c') - def test_handle_event_with_namespace(self, eio): + async def test_handle_event_with_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) - sid = _run(s.manager.connect('123', '/foo')) + sid = await s.manager.connect('123', '/foo') handler = mock.MagicMock() catchall_handler = mock.MagicMock() s.on('msg', handler, namespace='/foo') s.on('*', catchall_handler, namespace='/foo') - _run(s._handle_eio_message('123', '2/foo,["msg","a","b"]')) - _run(s._handle_eio_message('123', '2/foo,["my message","a","b","c"]')) + await s._handle_eio_message('123', '2/foo,["msg","a","b"]') + await s._handle_eio_message('123', '2/foo,["my message","a","b","c"]') handler.assert_called_once_with(sid, 'a', 'b') catchall_handler.assert_called_once_with( 'my message', sid, 'a', 'b', 'c') - def test_handle_event_with_catchall_namespace(self, eio): + async def test_handle_event_with_catchall_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) - sid_foo = _run(s.manager.connect('123', '/foo')) - sid_bar = _run(s.manager.connect('123', '/bar')) + sid_foo = await s.manager.connect('123', '/foo') + sid_bar = await s.manager.connect('123', '/bar') connect_star_handler = mock.MagicMock() msg_foo_handler = mock.MagicMock() msg_star_handler = mock.MagicMock() @@ -631,12 +619,12 @@ def test_handle_event_with_catchall_namespace(self, eio): s.on('msg', msg_star_handler, namespace='*') s.on('*', star_foo_handler, namespace='/foo') s.on('*', star_star_handler, namespace='*') - _run(s._trigger_event('connect', '/bar', sid_bar)) - _run(s._handle_eio_message('123', '2/foo,["msg","a","b"]')) - _run(s._handle_eio_message('123', '2/bar,["msg","a","b"]')) - _run(s._handle_eio_message('123', '2/foo,["my message","a","b","c"]')) - _run(s._handle_eio_message('123', '2/bar,["my message","a","b","c"]')) - _run(s._trigger_event('disconnect', '/bar', sid_bar)) + await s._trigger_event('connect', '/bar', sid_bar) + await s._handle_eio_message('123', '2/foo,["msg","a","b"]') + await s._handle_eio_message('123', '2/bar,["msg","a","b"]') + await s._handle_eio_message('123', '2/foo,["my message","a","b","c"]') + await s._handle_eio_message('123', '2/bar,["my message","a","b","c"]') + await s._trigger_event('disconnect', '/bar', sid_bar) connect_star_handler.assert_called_once_with('/bar', sid_bar) msg_foo_handler.assert_called_once_with(sid_foo, 'a', 'b') msg_star_handler.assert_called_once_with('/bar', sid_bar, 'a', 'b') @@ -645,157 +633,151 @@ def test_handle_event_with_catchall_namespace(self, eio): star_star_handler.assert_called_once_with( 'my message', '/bar', sid_bar, 'a', 'b', 'c') - def test_handle_event_with_disconnected_namespace(self, eio): + async def test_handle_event_with_disconnected_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) - _run(s.manager.connect('123', '/foo')) + await s.manager.connect('123', '/foo') handler = mock.MagicMock() s.on('my message', handler, namespace='/bar') - _run(s._handle_eio_message('123', '2/bar,["my message","a","b","c"]')) + await s._handle_eio_message('123', '2/bar,["my message","a","b","c"]') handler.assert_not_called() - def test_handle_event_binary(self, eio): + async def test_handle_event_binary(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) - sid = _run(s.manager.connect('123', '/')) + sid = await s.manager.connect('123', '/') handler = mock.MagicMock() s.on('my message', handler) - _run( - s._handle_eio_message( - '123', - '52-["my message","a",' - '{"_placeholder":true,"num":1},' - '{"_placeholder":true,"num":0}]', - ) + await s._handle_eio_message( + '123', + '52-["my message","a",' + '{"_placeholder":true,"num":1},' + '{"_placeholder":true,"num":0}]', ) - _run(s._handle_eio_message('123', b'foo')) - _run(s._handle_eio_message('123', b'bar')) + await s._handle_eio_message('123', b'foo') + await s._handle_eio_message('123', b'bar') handler.assert_called_once_with(sid, 'a', b'bar', b'foo') - def test_handle_event_binary_ack(self, eio): + async def test_handle_event_binary_ack(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) s.manager.trigger_callback = mock.AsyncMock() - sid = _run(s.manager.connect('123', '/')) - _run( - s._handle_eio_message( - '123', - '61-321["my message","a",' '{"_placeholder":true,"num":0}]', - ) + sid = await s.manager.connect('123', '/') + await s._handle_eio_message( + '123', + '61-321["my message","a",' '{"_placeholder":true,"num":0}]', ) - _run(s._handle_eio_message('123', b'foo')) + await s._handle_eio_message('123', b'foo') s.manager.trigger_callback.assert_awaited_once_with( sid, 321, ['my message', 'a', b'foo'] ) - def test_handle_event_with_ack(self, eio): + async def test_handle_event_with_ack(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) - sid = _run(s.manager.connect('123', '/')) + sid = await s.manager.connect('123', '/') handler = mock.MagicMock(return_value='foo') s.on('my message', handler) - _run(s._handle_eio_message('123', '21000["my message","foo"]')) + await s._handle_eio_message('123', '21000["my message","foo"]') handler.assert_called_once_with(sid, 'foo') s.eio.send.assert_awaited_once_with( '123', '31000["foo"]' ) - def test_handle_unknown_event_with_ack(self, eio): + async def test_handle_unknown_event_with_ack(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) - _run(s.manager.connect('123', '/')) + await s.manager.connect('123', '/') handler = mock.MagicMock(return_value='foo') s.on('my message', handler) - _run(s._handle_eio_message('123', '21000["another message","foo"]')) + await s._handle_eio_message('123', '21000["another message","foo"]') s.eio.send.assert_not_awaited() - def test_handle_event_with_ack_none(self, eio): + async def test_handle_event_with_ack_none(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) - sid = _run(s.manager.connect('123', '/')) + sid = await s.manager.connect('123', '/') handler = mock.MagicMock(return_value=None) s.on('my message', handler) - _run(s._handle_eio_message('123', '21000["my message","foo"]')) + await s._handle_eio_message('123', '21000["my message","foo"]') handler.assert_called_once_with(sid, 'foo') s.eio.send.assert_awaited_once_with('123', '31000[]') - def test_handle_event_with_ack_tuple(self, eio): + async def test_handle_event_with_ack_tuple(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) - sid = _run(s.manager.connect('123', '/')) + sid = await s.manager.connect('123', '/') handler = mock.MagicMock(return_value=(1, '2', True)) s.on('my message', handler) - _run(s._handle_eio_message('123', '21000["my message","a","b","c"]')) + await s._handle_eio_message('123', '21000["my message","a","b","c"]') handler.assert_called_once_with(sid, 'a', 'b', 'c') s.eio.send.assert_awaited_once_with( '123', '31000[1,"2",true]' ) - def test_handle_event_with_ack_list(self, eio): + async def test_handle_event_with_ack_list(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) - sid = _run(s.manager.connect('123', '/')) + sid = await s.manager.connect('123', '/') handler = mock.MagicMock(return_value=[1, '2', True]) s.on('my message', handler) - _run(s._handle_eio_message('123', '21000["my message","a","b","c"]')) + await s._handle_eio_message('123', '21000["my message","a","b","c"]') handler.assert_called_once_with(sid, 'a', 'b', 'c') s.eio.send.assert_awaited_once_with( '123', '31000[[1,"2",true]]' ) - def test_handle_event_with_ack_binary(self, eio): + async def test_handle_event_with_ack_binary(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer(async_handlers=False) - sid = _run(s.manager.connect('123', '/')) + sid = await s.manager.connect('123', '/') handler = mock.MagicMock(return_value=b'foo') s.on('my message', handler) - _run(s._handle_eio_message('123', '21000["my message","foo"]')) + await s._handle_eio_message('123', '21000["my message","foo"]') handler.assert_any_call(sid, 'foo') - def test_handle_error_packet(self, eio): + async def test_handle_error_packet(self, eio): s = async_server.AsyncServer() with pytest.raises(ValueError): - _run(s._handle_eio_message('123', '4')) + await s._handle_eio_message('123', '4') - def test_handle_invalid_packet(self, eio): + async def test_handle_invalid_packet(self, eio): s = async_server.AsyncServer() with pytest.raises(ValueError): - _run(s._handle_eio_message('123', '9')) + await s._handle_eio_message('123', '9') - def test_send_with_ack(self, eio): + async def test_send_with_ack(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/'] = {} - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') cb = mock.MagicMock() id1 = s.manager._generate_ack_id('1', cb) id2 = s.manager._generate_ack_id('1', cb) - _run(s._send_packet('123', packet.Packet( - packet.EVENT, ['my event', 'foo'], id=id1))) - _run(s._send_packet('123', packet.Packet( - packet.EVENT, ['my event', 'bar'], id=id2))) - _run(s._handle_eio_message('123', '31["foo",2]')) + await s._send_packet('123', packet.Packet( + packet.EVENT, ['my event', 'foo'], id=id1)) + await s._send_packet('123', packet.Packet( + packet.EVENT, ['my event', 'bar'], id=id2)) + await s._handle_eio_message('123', '31["foo",2]') cb.assert_called_once_with('foo', 2) - def test_send_with_ack_namespace(self, eio): + async def test_send_with_ack_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/foo'] = {} - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0/foo,')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0/foo,') cb = mock.MagicMock() id = s.manager._generate_ack_id('1', cb) - _run( - s._send_packet( - '123', packet.Packet(packet.EVENT, ['my event', 'foo'], - namespace='/foo', id=id) - ) + await s._send_packet( + '123', packet.Packet(packet.EVENT, ['my event', 'foo'], + namespace='/foo', id=id) ) - _run(s._handle_eio_message('123', '3/foo,1["foo",2]')) + await s._handle_eio_message('123', '3/foo,1["foo",2]') cb.assert_called_once_with('foo', 2) - def test_session(self, eio): + async def test_session(self, eio): fake_session = {} async def fake_get_session(eio_sid): @@ -836,65 +818,65 @@ async def _test(): '/ns': {'a': 'b'}, } - _run(_test()) + await _test() - def test_disconnect(self, eio): + async def test_disconnect(self, eio): eio.return_value.send = mock.AsyncMock() eio.return_value.disconnect = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/'] = {} - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) - _run(s.disconnect('1')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') + await s.disconnect('1') s.eio.send.assert_any_await('123', '1') assert not s.manager.is_connected('1', '/') - def test_disconnect_ignore_queue(self, eio): + async def test_disconnect_ignore_queue(self, eio): eio.return_value.send = mock.AsyncMock() eio.return_value.disconnect = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/'] = {} - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) - _run(s.disconnect('1', ignore_queue=True)) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') + await s.disconnect('1', ignore_queue=True) s.eio.send.assert_any_await('123', '1') assert not s.manager.is_connected('1', '/') - def test_disconnect_namespace(self, eio): + async def test_disconnect_namespace(self, eio): eio.return_value.send = mock.AsyncMock() eio.return_value.disconnect = mock.AsyncMock() s = async_server.AsyncServer() s.handlers['/foo'] = {} - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0/foo,')) - _run(s.disconnect('1', namespace='/foo')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0/foo,') + await s.disconnect('1', namespace='/foo') s.eio.send.assert_any_await('123', '1/foo,') assert not s.manager.is_connected('1', '/foo') - def test_disconnect_twice(self, eio): + async def test_disconnect_twice(self, eio): eio.return_value.send = mock.AsyncMock() eio.return_value.disconnect = mock.AsyncMock() s = async_server.AsyncServer() - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0')) - _run(s.disconnect('1')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') + await s.disconnect('1') calls = s.eio.send.await_count assert not s.manager.is_connected('1', '/') - _run(s.disconnect('1')) + await s.disconnect('1') assert calls == s.eio.send.await_count - def test_disconnect_twice_namespace(self, eio): + async def test_disconnect_twice_namespace(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0/foo,')) - _run(s.disconnect('1', namespace='/foo')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0/foo,') + await s.disconnect('1', namespace='/foo') calls = s.eio.send.await_count assert not s.manager.is_connected('1', '/foo') - _run(s.disconnect('1', namespace='/foo')) + await s.disconnect('1', namespace='/foo') assert calls == s.eio.send.await_count - def test_namespace_handler(self, eio): + async def test_namespace_handler(self, eio): eio.return_value.send = mock.AsyncMock() result = {} @@ -916,19 +898,19 @@ async def on_baz(self, sid, data1, data2): s = async_server.AsyncServer(async_handlers=False) s.register_namespace(MyNamespace('/foo')) - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0/foo,')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0/foo,') assert result['result'] == ('1', 'environ') - _run(s._handle_eio_message('123', '2/foo,["foo","a"]')) + await s._handle_eio_message('123', '2/foo,["foo","a"]') assert result['result'] == ('1', 'a') - _run(s._handle_eio_message('123', '2/foo,["bar"]')) + await s._handle_eio_message('123', '2/foo,["bar"]') assert result['result'] == 'bar' - _run(s._handle_eio_message('123', '2/foo,["baz","a","b"]')) + await s._handle_eio_message('123', '2/foo,["baz","a","b"]') assert result['result'] == ('a', 'b') - _run(s.disconnect('1', '/foo')) + await s.disconnect('1', '/foo') assert result['result'] == ('disconnect', '1') - def test_catchall_namespace_handler(self, eio): + async def test_catchall_namespace_handler(self, eio): eio.return_value.send = mock.AsyncMock() result = {} @@ -950,19 +932,19 @@ async def on_baz(self, ns, sid, data1, data2): s = async_server.AsyncServer(async_handlers=False, namespaces='*') s.register_namespace(MyNamespace('*')) - _run(s._handle_eio_connect('123', 'environ')) - _run(s._handle_eio_message('123', '0/foo,')) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0/foo,') assert result['result'] == ('1', '/foo', 'environ') - _run(s._handle_eio_message('123', '2/foo,["foo","a"]')) + await s._handle_eio_message('123', '2/foo,["foo","a"]') assert result['result'] == ('1', '/foo', 'a') - _run(s._handle_eio_message('123', '2/foo,["bar"]')) + await s._handle_eio_message('123', '2/foo,["bar"]') assert result['result'] == 'bar/foo' - _run(s._handle_eio_message('123', '2/foo,["baz","a","b"]')) + await s._handle_eio_message('123', '2/foo,["baz","a","b"]') assert result['result'] == ('/foo', 'a', 'b') - _run(s.disconnect('1', '/foo')) + await s.disconnect('1', '/foo') assert result['result'] == ('disconnect', '1', '/foo') - def test_bad_namespace_handler(self, eio): + async def test_bad_namespace_handler(self, eio): class Dummy: pass @@ -981,7 +963,7 @@ class SyncNS(namespace.Namespace): with pytest.raises(ValueError): s.register_namespace(SyncNS()) - def test_logger(self, eio): + async def test_logger(self, eio): s = async_server.AsyncServer(logger=False) assert s.logger.getEffectiveLevel() == logging.ERROR s.logger.setLevel(logging.NOTSET) @@ -994,13 +976,13 @@ def test_logger(self, eio): s = async_server.AsyncServer(logger='foo') assert s.logger == 'foo' - def test_engineio_logger(self, eio): + async def test_engineio_logger(self, eio): async_server.AsyncServer(engineio_logger='foo') eio.assert_called_once_with( **{'logger': 'foo', 'async_handlers': False} ) - def test_custom_json(self, eio): + async def test_custom_json(self, eio): # Warning: this test cannot run in parallel with other tests, as it # changes the JSON encoding/decoding functions @@ -1029,10 +1011,10 @@ def loads(*args, **kwargs): # restore the default JSON module packet.Packet.json = json - def test_async_handlers(self, eio): + async def test_async_handlers(self, eio): s = async_server.AsyncServer(async_handlers=True) - _run(s.manager.connect('123', '/')) - _run(s._handle_eio_message('123', '2["my message","a","b","c"]')) + await s.manager.connect('123', '/') + await s._handle_eio_message('123', '2["my message","a","b","c"]') s.eio.start_background_task.assert_called_once_with( s._handle_event_internal, s, @@ -1043,21 +1025,21 @@ def test_async_handlers(self, eio): None, ) - def test_shutdown(self, eio): + async def test_shutdown(self, eio): s = async_server.AsyncServer() s.eio.shutdown = mock.AsyncMock() - _run(s.shutdown()) + await s.shutdown() s.eio.shutdown.assert_awaited_once_with() - def test_start_background_task(self, eio): + async def test_start_background_task(self, eio): s = async_server.AsyncServer() s.start_background_task('foo', 'bar', baz='baz') s.eio.start_background_task.assert_called_once_with( 'foo', 'bar', baz='baz' ) - def test_sleep(self, eio): + async def test_sleep(self, eio): eio.return_value.sleep = mock.AsyncMock() s = async_server.AsyncServer() - _run(s.sleep(1.23)) + await s.sleep(1.23) s.eio.sleep.assert_awaited_once_with(1.23) diff --git a/tests/async/test_simple_client.py b/tests/async/test_simple_client.py index a8d08f41..08926922 100644 --- a/tests/async/test_simple_client.py +++ b/tests/async/test_simple_client.py @@ -4,11 +4,10 @@ from socketio import AsyncSimpleClient from socketio.exceptions import SocketIOError, TimeoutError, DisconnectedError -from .helpers import _run class TestAsyncAsyncSimpleClient: - def test_constructor(self): + async def test_constructor(self): client = AsyncSimpleClient(1, '2', a='3', b=4) assert client.client_args == (1, '2') assert client.client_kwargs == {'a': '3', 'b': 4} @@ -16,15 +15,15 @@ def test_constructor(self): assert client.input_buffer == [] assert not client.connected - def test_connect(self): + async def test_connect(self): client = AsyncSimpleClient(123, a='b') with mock.patch('socketio.async_simple_client.AsyncClient') \ as mock_client: mock_client.return_value.connect = mock.AsyncMock() - _run(client.connect('url', headers='h', auth='a', transports='t', - namespace='n', socketio_path='s', - wait_timeout='w')) + await client.connect('url', headers='h', auth='a', transports='t', + namespace='n', socketio_path='s', + wait_timeout='w') mock_client.assert_called_once_with(123, a='b') assert client.client == mock_client() mock_client().connect.assert_awaited_once_with( @@ -35,7 +34,7 @@ def test_connect(self): assert client.namespace == 'n' assert not client.input_event.is_set() - def test_connect_context_manager(self): + async def test_connect_context_manager(self): async def _t(): async with AsyncSimpleClient(123, a='b') as client: with mock.patch('socketio.async_simple_client.AsyncClient') \ @@ -56,17 +55,17 @@ async def _t(): assert client.namespace == 'n' assert not client.input_event.is_set() - _run(_t()) + await _t() - def test_connect_twice(self): + async def test_connect_twice(self): client = AsyncSimpleClient(123, a='b') client.client = mock.MagicMock() client.connected = True with pytest.raises(RuntimeError): - _run(client.connect('url')) + await client.connect('url') - def test_properties(self): + async def test_properties(self): client = AsyncSimpleClient() client.client = mock.MagicMock(transport='websocket') client.client.get_sid.return_value = 'sid' @@ -76,7 +75,7 @@ def test_properties(self): assert client.sid == 'sid' assert client.transport == 'websocket' - def test_emit(self): + async def test_emit(self): client = AsyncSimpleClient() client.client = mock.MagicMock() client.client.emit = mock.AsyncMock() @@ -84,18 +83,18 @@ def test_emit(self): client.connected_event.set() client.connected = True - _run(client.emit('foo', 'bar')) + await client.emit('foo', 'bar') client.client.emit.assert_awaited_once_with('foo', 'bar', namespace='/ns') - def test_emit_disconnected(self): + async def test_emit_disconnected(self): client = AsyncSimpleClient() client.connected_event.set() client.connected = False with pytest.raises(DisconnectedError): - _run(client.emit('foo', 'bar')) + await client.emit('foo', 'bar') - def test_emit_retries(self): + async def test_emit_retries(self): client = AsyncSimpleClient() client.connected_event.set() client.connected = True @@ -103,10 +102,10 @@ def test_emit_retries(self): client.client.emit = mock.AsyncMock() client.client.emit.side_effect = [SocketIOError(), None] - _run(client.emit('foo', 'bar')) + await client.emit('foo', 'bar') client.client.emit.assert_awaited_with('foo', 'bar', namespace='/') - def test_call(self): + async def test_call(self): client = AsyncSimpleClient() client.client = mock.MagicMock() client.client.call = mock.AsyncMock() @@ -115,18 +114,18 @@ def test_call(self): client.connected_event.set() client.connected = True - assert _run(client.call('foo', 'bar')) == 'result' + assert await client.call('foo', 'bar') == 'result' client.client.call.assert_awaited_once_with( 'foo', 'bar', namespace='/ns', timeout=60) - def test_call_disconnected(self): + async def test_call_disconnected(self): client = AsyncSimpleClient() client.connected_event.set() client.connected = False with pytest.raises(DisconnectedError): - _run(client.call('foo', 'bar')) + await client.call('foo', 'bar') - def test_call_retries(self): + async def test_call_retries(self): client = AsyncSimpleClient() client.connected_event.set() client.connected = True @@ -134,17 +133,17 @@ def test_call_retries(self): client.client.call = mock.AsyncMock() client.client.call.side_effect = [SocketIOError(), 'result'] - assert _run(client.call('foo', 'bar')) == 'result' + assert await client.call('foo', 'bar') == 'result' client.client.call.assert_awaited_with('foo', 'bar', namespace='/', timeout=60) - def test_receive_with_input_buffer(self): + async def test_receive_with_input_buffer(self): client = AsyncSimpleClient() client.input_buffer = ['foo', 'bar'] - assert _run(client.receive()) == 'foo' - assert _run(client.receive()) == 'bar' + assert await client.receive() == 'foo' + assert await client.receive() == 'bar' - def test_receive_without_input_buffer(self): + async def test_receive_without_input_buffer(self): client = AsyncSimpleClient() client.connected_event.set() client.connected = True @@ -155,9 +154,9 @@ async def fake_wait(timeout=None): return True client.input_event.wait = fake_wait - assert _run(client.receive()) == 'foo' + assert await client.receive() == 'foo' - def test_receive_with_timeout(self): + async def test_receive_with_timeout(self): client = AsyncSimpleClient() client.connected_event.set() client.connected = True @@ -168,22 +167,22 @@ async def fake_wait(timeout=None): client.input_event.wait = fake_wait with pytest.raises(TimeoutError): - _run(client.receive(timeout=0.01)) + await client.receive(timeout=0.01) - def test_receive_disconnected(self): + async def test_receive_disconnected(self): client = AsyncSimpleClient() client.connected_event.set() client.connected = False with pytest.raises(DisconnectedError): - _run(client.receive()) + await client.receive() - def test_disconnect(self): + async def test_disconnect(self): client = AsyncSimpleClient() mc = mock.MagicMock() mc.disconnect = mock.AsyncMock() client.client = mc client.connected = True - _run(client.disconnect()) - _run(client.disconnect()) + await client.disconnect() + await client.disconnect() mc.disconnect.assert_awaited_once_with() assert client.client is None diff --git a/tox.ini b/tox.ini index 6d809d40..fc0116cb 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,7 @@ deps= aiohttp msgpack pytest + pytest-asyncio pytest-timeout pytest-cov From bd8555da8523d1a73432685a00eb5acb4d2261f5 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 18 Dec 2024 17:39:03 +0000 Subject: [PATCH 486/571] Pass a `reason` argument to the disconnect handler (#1422) --- docs/client.rst | 23 +++-- docs/server.rst | 23 ++++- examples/client/async/fiddle_client.py | 4 +- examples/client/sync/fiddle_client.py | 4 +- examples/server/aiohttp/app.html | 4 +- examples/server/aiohttp/app.py | 6 +- examples/server/aiohttp/fiddle.py | 6 +- examples/server/asgi/app.html | 4 +- examples/server/asgi/app.py | 4 +- examples/server/asgi/fiddle.py | 4 +- examples/server/javascript/fiddle.js | 4 +- examples/server/sanic/app.html | 4 +- examples/server/sanic/app.py | 4 +- examples/server/sanic/fiddle.py | 4 +- examples/server/tornado/app.py | 4 +- examples/server/tornado/fiddle.py | 4 +- examples/server/tornado/templates/app.html | 4 +- examples/server/wsgi/app.py | 4 +- .../socketio_app/static/index.html | 4 +- .../django_socketio/socketio_app/views.py | 4 +- examples/server/wsgi/fiddle.py | 4 +- examples/server/wsgi/templates/index.html | 4 +- pyproject.toml | 2 +- src/socketio/async_client.py | 33 +++++-- src/socketio/async_namespace.py | 40 ++++++++- src/socketio/async_server.py | 33 +++++-- src/socketio/base_client.py | 5 +- src/socketio/base_server.py | 3 + src/socketio/client.py | 20 +++-- src/socketio/namespace.py | 18 +++- src/socketio/server.py | 24 +++-- tests/async/test_client.py | 89 ++++++++++--------- tests/async/test_manager.py | 2 - tests/async/test_namespace.py | 62 ++++++++++++- tests/async/test_server.py | 53 ++++++++--- tests/common/test_client.py | 72 +++++++-------- tests/common/test_namespace.py | 38 +++++++- tests/common/test_server.py | 38 +++++--- 38 files changed, 469 insertions(+), 193 deletions(-) diff --git a/docs/client.rst b/docs/client.rst index 1a55b71e..e3e1fb2c 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -312,8 +312,8 @@ server:: print("The connection failed!") @sio.event - def disconnect(): - print("I'm disconnected!") + def disconnect(reason): + print("I'm disconnected! reason:", reason) The ``connect_error`` handler is invoked when a connection attempt fails. If the server provides arguments, these are passed on to the handler. The server @@ -325,7 +325,20 @@ server initiated disconnects, or accidental disconnects, for example due to networking failures. In the case of an accidental disconnection, the client is going to attempt to reconnect immediately after invoking the disconnect handler. As soon as the connection is re-established the connect handler will -be invoked once again. +be invoked once again. The handler receives a ``reason`` argument which +provides the cause of the disconnection:: + + @sio.event + def disconnect(reason): + if reason == sio.reason.CLIENT_DISCONNECT: + print('the client disconnected') + elif reason == sio.reason.SERVER_DISCONNECT: + print('the server disconnected the client') + else: + print('disconnect reason:', reason) + +See the The :attr:`socketio.Client.reason` attribute for a list of possible +disconnection reasons. The ``connect``, ``connect_error`` and ``disconnect`` events have to be defined explicitly and are not invoked on a catch-all event handler. @@ -509,7 +522,7 @@ that belong to a namespace can be created as methods of a subclass of def on_connect(self): pass - def on_disconnect(self): + def on_disconnect(self, reason): pass def on_my_event(self, data): @@ -525,7 +538,7 @@ coroutines if desired:: def on_connect(self): pass - def on_disconnect(self): + def on_disconnect(self, reason): pass async def on_my_event(self, data): diff --git a/docs/server.rst b/docs/server.rst index c20adf9f..ed15ed32 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -232,8 +232,8 @@ automatically when a client connects or disconnects from the server:: print('connect ', sid) @sio.event - def disconnect(sid): - print('disconnect ', sid) + def disconnect(sid, reason): + print('disconnect ', sid, reason) The ``connect`` event is an ideal place to perform user authentication, and any necessary mapping between user entities in the application and the ``sid`` @@ -256,6 +256,21 @@ message:: def connect(sid, environ, auth): raise ConnectionRefusedError('authentication failed') +The disconnect handler receives the ``sid`` assigned to the client and a +``reason``, which provides the cause of the disconnection:: + + @sio.event + def disconnect(sid, reason): + if reason == sio.reason.CLIENT_DISCONNECT: + print('the client disconnected') + elif reason == sio.reason.SERVER_DISCONNECT: + print('the server disconnected the client') + else: + print('disconnect reason:', reason) + +See the The :attr:`socketio.Server.reason` attribute for a list of possible +disconnection reasons. + Catch-All Event Handlers ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -433,7 +448,7 @@ belong to a namespace can be created as methods in a subclass of def on_connect(self, sid, environ): pass - def on_disconnect(self, sid): + def on_disconnect(self, sid, reason): pass def on_my_event(self, sid, data): @@ -449,7 +464,7 @@ if desired:: def on_connect(self, sid, environ): pass - def on_disconnect(self, sid): + def on_disconnect(self, sid, reason): pass async def on_my_event(self, sid, data): diff --git a/examples/client/async/fiddle_client.py b/examples/client/async/fiddle_client.py index 5b43dccd..e5aeb6cc 100644 --- a/examples/client/async/fiddle_client.py +++ b/examples/client/async/fiddle_client.py @@ -10,8 +10,8 @@ async def connect(): @sio.event -async def disconnect(): - print('disconnected from server') +async def disconnect(reason): + print('disconnected from server, reason:', reason) @sio.event diff --git a/examples/client/sync/fiddle_client.py b/examples/client/sync/fiddle_client.py index 50f5e2aa..71a7a540 100644 --- a/examples/client/sync/fiddle_client.py +++ b/examples/client/sync/fiddle_client.py @@ -9,8 +9,8 @@ def connect(): @sio.event -def disconnect(): - print('disconnected from server') +def disconnect(reason): + print('disconnected from server, reason:', reason) @sio.event diff --git a/examples/server/aiohttp/app.html b/examples/server/aiohttp/app.html index 74d404d7..627b9186 100644 --- a/examples/server/aiohttp/app.html +++ b/examples/server/aiohttp/app.html @@ -11,8 +11,8 @@ socket.on('connect', function() { socket.emit('my_event', {data: 'I\'m connected!'}); }); - socket.on('disconnect', function() { - $('#log').append('
Disconnected'); + socket.on('disconnect', function(reason) { + $('#log').append('
Disconnected: ' + reason); }); socket.on('my_response', function(msg) { $('#log').append('
Received: ' + msg.data); diff --git a/examples/server/aiohttp/app.py b/examples/server/aiohttp/app.py index cba51937..1568ca1f 100644 --- a/examples/server/aiohttp/app.py +++ b/examples/server/aiohttp/app.py @@ -70,8 +70,8 @@ async def connect(sid, environ): @sio.event -def disconnect(sid): - print('Client disconnected') +def disconnect(sid, reason): + print('Client disconnected, reason:', reason) app.router.add_static('/static', 'static') @@ -84,4 +84,4 @@ async def init_app(): if __name__ == '__main__': - web.run_app(init_app()) + web.run_app(init_app(), port=5000) diff --git a/examples/server/aiohttp/fiddle.py b/examples/server/aiohttp/fiddle.py index dfde8e10..64ce330d 100644 --- a/examples/server/aiohttp/fiddle.py +++ b/examples/server/aiohttp/fiddle.py @@ -19,8 +19,8 @@ async def connect(sid, environ, auth): @sio.event -def disconnect(sid): - print('disconnected', sid) +def disconnect(sid, reason): + print('disconnected', sid, reason) app.router.add_static('/static', 'static') @@ -28,4 +28,4 @@ def disconnect(sid): if __name__ == '__main__': - web.run_app(app) + web.run_app(app, port=5000) diff --git a/examples/server/asgi/app.html b/examples/server/asgi/app.html index d2f0e9ac..ad826565 100644 --- a/examples/server/asgi/app.html +++ b/examples/server/asgi/app.html @@ -11,8 +11,8 @@ socket.on('connect', function() { socket.emit('my_event', {data: 'I\'m connected!'}); }); - socket.on('disconnect', function() { - $('#log').append('
Disconnected'); + socket.on('disconnect', function(reason) { + $('#log').append('
Disconnected: ' + reason); }); socket.on('my_response', function(msg) { $('#log').append('
Received: ' + msg.data); diff --git a/examples/server/asgi/app.py b/examples/server/asgi/app.py index 36af85f2..d549ab09 100644 --- a/examples/server/asgi/app.py +++ b/examples/server/asgi/app.py @@ -88,8 +88,8 @@ async def test_connect(sid, environ): @sio.on('disconnect') -def test_disconnect(sid): - print('Client disconnected') +def test_disconnect(sid, reason): + print('Client disconnected, reason:', reason) if __name__ == '__main__': diff --git a/examples/server/asgi/fiddle.py b/examples/server/asgi/fiddle.py index 6899ed1a..402a3799 100644 --- a/examples/server/asgi/fiddle.py +++ b/examples/server/asgi/fiddle.py @@ -17,8 +17,8 @@ async def connect(sid, environ, auth): @sio.event -def disconnect(sid): - print('disconnected', sid) +def disconnect(sid, reason): + print('disconnected', sid, reason) if __name__ == '__main__': diff --git a/examples/server/javascript/fiddle.js b/examples/server/javascript/fiddle.js index 940e4da3..c6a039a0 100644 --- a/examples/server/javascript/fiddle.js +++ b/examples/server/javascript/fiddle.js @@ -19,8 +19,8 @@ io.on('connection', socket => { hello: 'you' }); - socket.on('disconnect', () => { - console.log(`disconnect ${socket.id}`); + socket.on('disconnect', (reason) => { + console.log(`disconnect ${socket.id}, reason: ${reason}`); }); }); diff --git a/examples/server/sanic/app.html b/examples/server/sanic/app.html index 30c59643..b87b2df1 100644 --- a/examples/server/sanic/app.html +++ b/examples/server/sanic/app.html @@ -11,8 +11,8 @@ socket.on('connect', function() { socket.emit('my_event', {data: 'I\'m connected!'}); }); - socket.on('disconnect', function() { - $('#log').append('
Disconnected'); + socket.on('disconnect', function(reason) { + $('#log').append('
Disconnected: ' + reason); }); socket.on('my_response', function(msg) { $('#log').append('
Received: ' + msg.data); diff --git a/examples/server/sanic/app.py b/examples/server/sanic/app.py index 7f02d231..447ddff6 100644 --- a/examples/server/sanic/app.py +++ b/examples/server/sanic/app.py @@ -77,8 +77,8 @@ async def connect(sid, environ): @sio.event -def disconnect(sid): - print('Client disconnected') +def disconnect(sid, reason): + print('Client disconnected, reason:', reason) app.static('/static', './static') diff --git a/examples/server/sanic/fiddle.py b/examples/server/sanic/fiddle.py index 5ecb509e..405e6e56 100644 --- a/examples/server/sanic/fiddle.py +++ b/examples/server/sanic/fiddle.py @@ -21,8 +21,8 @@ async def connect(sid, environ, auth): @sio.event -def disconnect(sid): - print('disconnected', sid) +def disconnect(sid, reason): + print('disconnected', sid, reason) app.static('/static', './static') diff --git a/examples/server/tornado/app.py b/examples/server/tornado/app.py index 16f7a191..58317d9b 100644 --- a/examples/server/tornado/app.py +++ b/examples/server/tornado/app.py @@ -75,8 +75,8 @@ async def connect(sid, environ): @sio.event -def disconnect(sid): - print('Client disconnected') +def disconnect(sid, reason): + print('Client disconnected, reason:', reason) def main(): diff --git a/examples/server/tornado/fiddle.py b/examples/server/tornado/fiddle.py index 1e7e9278..b3878a2a 100644 --- a/examples/server/tornado/fiddle.py +++ b/examples/server/tornado/fiddle.py @@ -24,8 +24,8 @@ async def connect(sid, environ, auth): @sio.event -def disconnect(sid): - print('disconnected', sid) +def disconnect(sid, reason): + print('disconnected', sid, reason) def main(): diff --git a/examples/server/tornado/templates/app.html b/examples/server/tornado/templates/app.html index 74d404d7..627b9186 100644 --- a/examples/server/tornado/templates/app.html +++ b/examples/server/tornado/templates/app.html @@ -11,8 +11,8 @@ socket.on('connect', function() { socket.emit('my_event', {data: 'I\'m connected!'}); }); - socket.on('disconnect', function() { - $('#log').append('
Disconnected'); + socket.on('disconnect', function(reason) { + $('#log').append('
Disconnected: ' + reason); }); socket.on('my_response', function(msg) { $('#log').append('
Received: ' + msg.data); diff --git a/examples/server/wsgi/app.py b/examples/server/wsgi/app.py index 7b019fd0..62bd59b1 100644 --- a/examples/server/wsgi/app.py +++ b/examples/server/wsgi/app.py @@ -94,8 +94,8 @@ def connect(sid, environ): @sio.event -def disconnect(sid): - print('Client disconnected') +def disconnect(sid, reason): + print('Client disconnected, reason:', reason) if __name__ == '__main__': diff --git a/examples/server/wsgi/django_socketio/socketio_app/static/index.html b/examples/server/wsgi/django_socketio/socketio_app/static/index.html index 6dbef78b..b10818f4 100644 --- a/examples/server/wsgi/django_socketio/socketio_app/static/index.html +++ b/examples/server/wsgi/django_socketio/socketio_app/static/index.html @@ -11,8 +11,8 @@ socket.on('connect', function() { socket.emit('my_event', {data: 'I\'m connected!'}); }); - socket.on('disconnect', function() { - $('#log').append('
Disconnected'); + socket.on('disconnect', function(reason) { + $('#log').append('
Disconnected: ' + reason); }); socket.on('my_response', function(msg) { $('#log').append('
Received: ' + msg.data); diff --git a/examples/server/wsgi/django_socketio/socketio_app/views.py b/examples/server/wsgi/django_socketio/socketio_app/views.py index 854c0fb0..f54e1d67 100644 --- a/examples/server/wsgi/django_socketio/socketio_app/views.py +++ b/examples/server/wsgi/django_socketio/socketio_app/views.py @@ -78,5 +78,5 @@ def connect(sid, environ): @sio.event -def disconnect(sid): - print('Client disconnected') +def disconnect(sid, reason): + print('Client disconnected, reason:', reason) diff --git a/examples/server/wsgi/fiddle.py b/examples/server/wsgi/fiddle.py index 247751be..e9cd703d 100644 --- a/examples/server/wsgi/fiddle.py +++ b/examples/server/wsgi/fiddle.py @@ -23,8 +23,8 @@ def connect(sid, environ, auth): @sio.event -def disconnect(sid): - print('disconnected', sid) +def disconnect(sid, reason): + print('disconnected', sid, reason) if __name__ == '__main__': diff --git a/examples/server/wsgi/templates/index.html b/examples/server/wsgi/templates/index.html index 8a7308af..e37a6cbd 100644 --- a/examples/server/wsgi/templates/index.html +++ b/examples/server/wsgi/templates/index.html @@ -11,8 +11,8 @@ socket.on('connect', function() { socket.emit('my_event', {data: 'I\'m connected!'}); }); - socket.on('disconnect', function() { - $('#log').append('
Disconnected'); + socket.on('disconnect', function(reason) { + $('#log').append('
Disconnected: ' + reason); }); socket.on('my_response', function(msg) { $('#log').append('
Received: ' + msg.data); diff --git a/pyproject.toml b/pyproject.toml index 8a5453a1..a3ebb6f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ requires-python = ">=3.8" dependencies = [ "bidict >= 0.21.0", - "python-engineio >= 4.8.0", + "python-engineio >= 4.11.0", ] [project.readme] diff --git a/src/socketio/async_client.py b/src/socketio/async_client.py index fb1abc14..463073e7 100644 --- a/src/socketio/async_client.py +++ b/src/socketio/async_client.py @@ -338,7 +338,6 @@ async def shutdown(self): await self.disconnect() elif self._reconnect_task: # pragma: no branch self._reconnect_abort.set() - print(self._reconnect_task) await self._reconnect_task def start_background_task(self, target, *args, **kwargs): @@ -398,8 +397,9 @@ async def _handle_disconnect(self, namespace): if not self.connected: return namespace = namespace or '/' - await self._trigger_event('disconnect', namespace=namespace) - await self._trigger_event('__disconnect_final', namespace=namespace) + await self._trigger_event('disconnect', namespace, + self.reason.SERVER_DISCONNECT) + await self._trigger_event('__disconnect_final', namespace) if namespace in self.namespaces: del self.namespaces[namespace] if not self.namespaces: @@ -462,11 +462,27 @@ async def _trigger_event(self, event, namespace, *args): if handler: if asyncio.iscoroutinefunction(handler): try: - ret = await handler(*args) + try: + ret = await handler(*args) + except TypeError: + # the legacy disconnect event does not take a reason + # argument + if event == 'disconnect': + ret = await handler(*args[:-1]) + else: # pragma: no cover + raise except asyncio.CancelledError: # pragma: no cover ret = None else: - ret = handler(*args) + try: + ret = handler(*args) + except TypeError: + # the legacy disconnect event does not take a reason + # argument + if event == 'disconnect': + ret = handler(*args[:-1]) + else: # pragma: no cover + raise return ret # or else, forward the event to a namepsace handler if one exists @@ -566,16 +582,15 @@ async def _handle_eio_message(self, data): else: raise ValueError('Unknown packet type.') - async def _handle_eio_disconnect(self): + async def _handle_eio_disconnect(self, reason): """Handle the Engine.IO disconnection event.""" self.logger.info('Engine.IO connection dropped') will_reconnect = self.reconnection and self.eio.state == 'connected' if self.connected: for n in self.namespaces: - await self._trigger_event('disconnect', namespace=n) + await self._trigger_event('disconnect', n, reason) if not will_reconnect: - await self._trigger_event('__disconnect_final', - namespace=n) + await self._trigger_event('__disconnect_final', n) self.namespaces = {} self.connected = False self.callbacks = {} diff --git a/src/socketio/async_namespace.py b/src/socketio/async_namespace.py index 89442aeb..42d65089 100644 --- a/src/socketio/async_namespace.py +++ b/src/socketio/async_namespace.py @@ -34,11 +34,27 @@ async def trigger_event(self, event, *args): handler = getattr(self, handler_name) if asyncio.iscoroutinefunction(handler) is True: try: - ret = await handler(*args) + try: + ret = await handler(*args) + except TypeError: + # legacy disconnect events do not have a reason + # argument + if event == 'disconnect': + ret = await handler(*args[:-1]) + else: # pragma: no cover + raise except asyncio.CancelledError: # pragma: no cover ret = None else: - ret = handler(*args) + try: + ret = handler(*args) + except TypeError: + # legacy disconnect events do not have a reason + # argument + if event == 'disconnect': + ret = handler(*args[:-1]) + else: # pragma: no cover + raise return ret async def emit(self, event, data=None, to=None, room=None, skip_sid=None, @@ -199,11 +215,27 @@ async def trigger_event(self, event, *args): handler = getattr(self, handler_name) if asyncio.iscoroutinefunction(handler) is True: try: - ret = await handler(*args) + try: + ret = await handler(*args) + except TypeError: + # legacy disconnect events do not have a reason + # argument + if event == 'disconnect': + ret = await handler(*args[:-1]) + else: # pragma: no cover + raise except asyncio.CancelledError: # pragma: no cover ret = None else: - ret = handler(*args) + try: + ret = handler(*args) + except TypeError: + # legacy disconnect events do not have a reason + # argument + if event == 'disconnect': + ret = handler(*args[:-1]) + else: # pragma: no cover + raise return ret async def emit(self, event, data=None, namespace=None, callback=None): diff --git a/src/socketio/async_server.py b/src/socketio/async_server.py index 9b0e9774..f10fb8ae 100644 --- a/src/socketio/async_server.py +++ b/src/socketio/async_server.py @@ -427,7 +427,8 @@ async def disconnect(self, sid, namespace=None, ignore_queue=False): eio_sid = self.manager.pre_disconnect(sid, namespace=namespace) await self._send_packet(eio_sid, self.packet_class( packet.DISCONNECT, namespace=namespace)) - await self._trigger_event('disconnect', namespace, sid) + await self._trigger_event('disconnect', namespace, sid, + self.reason.SERVER_DISCONNECT) await self.manager.disconnect(sid, namespace=namespace, ignore_queue=True) @@ -575,14 +576,15 @@ async def _handle_connect(self, eio_sid, namespace, data): await self._send_packet(eio_sid, self.packet_class( packet.CONNECT, {'sid': sid}, namespace=namespace)) - async def _handle_disconnect(self, eio_sid, namespace): + async def _handle_disconnect(self, eio_sid, namespace, reason=None): """Handle a client disconnect.""" namespace = namespace or '/' sid = self.manager.sid_from_eio_sid(eio_sid, namespace) if not self.manager.is_connected(sid, namespace): # pragma: no cover return self.manager.pre_disconnect(sid, namespace=namespace) - await self._trigger_event('disconnect', namespace, sid) + await self._trigger_event('disconnect', namespace, sid, + reason or self.reason.CLIENT_DISCONNECT) await self.manager.disconnect(sid, namespace, ignore_queue=True) async def _handle_event(self, eio_sid, namespace, id, data): @@ -634,11 +636,25 @@ async def _trigger_event(self, event, namespace, *args): if handler: if asyncio.iscoroutinefunction(handler): try: - ret = await handler(*args) + try: + ret = await handler(*args) + except TypeError: + # legacy disconnect events use only one argument + if event == 'disconnect': + ret = await handler(*args[:-1]) + else: # pragma: no cover + raise except asyncio.CancelledError: # pragma: no cover ret = None else: - ret = handler(*args) + try: + ret = handler(*args) + except TypeError: + # legacy disconnect events use only one argument + if event == 'disconnect': + ret = handler(*args[:-1]) + else: # pragma: no cover + raise return ret # or else, forward the event to a namespace handler if one exists handler, args = self._get_namespace_handler(namespace, args) @@ -671,7 +687,8 @@ async def _handle_eio_message(self, eio_sid, data): if pkt.packet_type == packet.CONNECT: await self._handle_connect(eio_sid, pkt.namespace, pkt.data) elif pkt.packet_type == packet.DISCONNECT: - await self._handle_disconnect(eio_sid, pkt.namespace) + await self._handle_disconnect(eio_sid, pkt.namespace, + self.reason.CLIENT_DISCONNECT) elif pkt.packet_type == packet.EVENT: await self._handle_event(eio_sid, pkt.namespace, pkt.id, pkt.data) @@ -686,10 +703,10 @@ async def _handle_eio_message(self, eio_sid, data): else: raise ValueError('Unknown packet type.') - async def _handle_eio_disconnect(self, eio_sid): + async def _handle_eio_disconnect(self, eio_sid, reason): """Handle Engine.IO disconnect event.""" for n in list(self.manager.get_namespaces()).copy(): - await self._handle_disconnect(eio_sid, n) + await self._handle_disconnect(eio_sid, n, reason) if eio_sid in self.environ: del self.environ[eio_sid] diff --git a/src/socketio/base_client.py b/src/socketio/base_client.py index 1becf914..7bf44207 100644 --- a/src/socketio/base_client.py +++ b/src/socketio/base_client.py @@ -3,6 +3,8 @@ import signal import threading +import engineio + from . import base_namespace from . import packet @@ -31,6 +33,7 @@ def signal_handler(sig, frame): # pragma: no cover class BaseClient: reserved_events = ['connect', 'connect_error', 'disconnect', '__disconnect_final'] + reason = engineio.Client.reason def __init__(self, reconnection=True, reconnection_attempts=0, reconnection_delay=1, reconnection_delay_max=5, @@ -285,7 +288,7 @@ def _handle_eio_connect(self): # pragma: no cover def _handle_eio_message(self, data): # pragma: no cover raise NotImplementedError() - def _handle_eio_disconnect(self): # pragma: no cover + def _handle_eio_disconnect(self, reason): # pragma: no cover raise NotImplementedError() def _engineio_client_class(self): # pragma: no cover diff --git a/src/socketio/base_server.py b/src/socketio/base_server.py index d5a353bc..d134eba1 100644 --- a/src/socketio/base_server.py +++ b/src/socketio/base_server.py @@ -1,5 +1,7 @@ import logging +import engineio + from . import manager from . import base_namespace from . import packet @@ -9,6 +11,7 @@ class BaseServer: reserved_events = ['connect', 'disconnect'] + reason = engineio.Server.reason def __init__(self, client_manager=None, logger=False, serializer='default', json=None, async_handlers=True, always_connect=False, diff --git a/src/socketio/client.py b/src/socketio/client.py index c4f9eaa7..ade2dd6f 100644 --- a/src/socketio/client.py +++ b/src/socketio/client.py @@ -377,8 +377,9 @@ def _handle_disconnect(self, namespace): if not self.connected: return namespace = namespace or '/' - self._trigger_event('disconnect', namespace=namespace) - self._trigger_event('__disconnect_final', namespace=namespace) + self._trigger_event('disconnect', namespace, + self.reason.SERVER_DISCONNECT) + self._trigger_event('__disconnect_final', namespace) if namespace in self.namespaces: del self.namespaces[namespace] if not self.namespaces: @@ -436,7 +437,14 @@ def _trigger_event(self, event, namespace, *args): # first see if we have an explicit handler for the event handler, args = self._get_event_handler(event, namespace, args) if handler: - return handler(*args) + try: + return handler(*args) + except TypeError: + # the legacy disconnect event does not take a reason argument + if event == 'disconnect': + return handler(*args[:-1]) + else: # pragma: no cover + raise # or else, forward the event to a namespace handler if one exists handler, args = self._get_namespace_handler(namespace, args) @@ -525,15 +533,15 @@ def _handle_eio_message(self, data): else: raise ValueError('Unknown packet type.') - def _handle_eio_disconnect(self): + def _handle_eio_disconnect(self, reason): """Handle the Engine.IO disconnection event.""" self.logger.info('Engine.IO connection dropped') will_reconnect = self.reconnection and self.eio.state == 'connected' if self.connected: for n in self.namespaces: - self._trigger_event('disconnect', namespace=n) + self._trigger_event('disconnect', n, reason) if not will_reconnect: - self._trigger_event('__disconnect_final', namespace=n) + self._trigger_event('__disconnect_final', n) self.namespaces = {} self.connected = False self.callbacks = {} diff --git a/src/socketio/namespace.py b/src/socketio/namespace.py index 3bf4f95b..60cab783 100644 --- a/src/socketio/namespace.py +++ b/src/socketio/namespace.py @@ -23,7 +23,14 @@ def trigger_event(self, event, *args): """ handler_name = 'on_' + (event or '') if hasattr(self, handler_name): - return getattr(self, handler_name)(*args) + try: + return getattr(self, handler_name)(*args) + except TypeError: + # legacy disconnect events do not have a reason argument + if event == 'disconnect': + return getattr(self, handler_name)(*args[:-1]) + else: # pragma: no cover + raise def emit(self, event, data=None, to=None, room=None, skip_sid=None, namespace=None, callback=None, ignore_queue=False): @@ -154,7 +161,14 @@ def trigger_event(self, event, *args): """ handler_name = 'on_' + (event or '') if hasattr(self, handler_name): - return getattr(self, handler_name)(*args) + try: + return getattr(self, handler_name)(*args) + except TypeError: + # legacy disconnect events do not have a reason argument + if event == 'disconnect': + return getattr(self, handler_name)(*args[:-1]) + else: # pragma: no cover + raise def emit(self, event, data=None, namespace=None, callback=None): """Emit a custom event to the server. diff --git a/src/socketio/server.py b/src/socketio/server.py index ae73df6a..71c702de 100644 --- a/src/socketio/server.py +++ b/src/socketio/server.py @@ -403,7 +403,8 @@ def disconnect(self, sid, namespace=None, ignore_queue=False): eio_sid = self.manager.pre_disconnect(sid, namespace=namespace) self._send_packet(eio_sid, self.packet_class( packet.DISCONNECT, namespace=namespace)) - self._trigger_event('disconnect', namespace, sid) + self._trigger_event('disconnect', namespace, sid, + self.reason.SERVER_DISCONNECT) self.manager.disconnect(sid, namespace=namespace, ignore_queue=True) @@ -557,14 +558,15 @@ def _handle_connect(self, eio_sid, namespace, data): self._send_packet(eio_sid, self.packet_class( packet.CONNECT, {'sid': sid}, namespace=namespace)) - def _handle_disconnect(self, eio_sid, namespace): + def _handle_disconnect(self, eio_sid, namespace, reason=None): """Handle a client disconnect.""" namespace = namespace or '/' sid = self.manager.sid_from_eio_sid(eio_sid, namespace) if not self.manager.is_connected(sid, namespace): # pragma: no cover return self.manager.pre_disconnect(sid, namespace=namespace) - self._trigger_event('disconnect', namespace, sid) + self._trigger_event('disconnect', namespace, sid, + reason or self.reason.CLIENT_DISCONNECT) self.manager.disconnect(sid, namespace, ignore_queue=True) def _handle_event(self, eio_sid, namespace, id, data): @@ -611,7 +613,14 @@ def _trigger_event(self, event, namespace, *args): # first see if we have an explicit handler for the event handler, args = self._get_event_handler(event, namespace, args) if handler: - return handler(*args) + try: + return handler(*args) + except TypeError: + # legacy disconnect events use only one argument + if event == 'disconnect': + return handler(*args[:-1]) + else: # pragma: no cover + raise # or else, forward the event to a namespace handler if one exists handler, args = self._get_namespace_handler(namespace, args) if handler: @@ -642,7 +651,8 @@ def _handle_eio_message(self, eio_sid, data): if pkt.packet_type == packet.CONNECT: self._handle_connect(eio_sid, pkt.namespace, pkt.data) elif pkt.packet_type == packet.DISCONNECT: - self._handle_disconnect(eio_sid, pkt.namespace) + self._handle_disconnect(eio_sid, pkt.namespace, + self.reason.CLIENT_DISCONNECT) elif pkt.packet_type == packet.EVENT: self._handle_event(eio_sid, pkt.namespace, pkt.id, pkt.data) elif pkt.packet_type == packet.ACK: @@ -655,10 +665,10 @@ def _handle_eio_message(self, eio_sid, data): else: raise ValueError('Unknown packet type.') - def _handle_eio_disconnect(self, eio_sid): + def _handle_eio_disconnect(self, eio_sid, reason): """Handle Engine.IO disconnect event.""" for n in list(self.manager.get_namespaces()).copy(): - self._handle_disconnect(eio_sid, n) + self._handle_disconnect(eio_sid, n, reason) if eio_sid in self.environ: del self.environ[eio_sid] diff --git a/tests/async/test_client.py b/tests/async/test_client.py index 26681b76..3eec1a88 100644 --- a/tests/async/test_client.py +++ b/tests/async/test_client.py @@ -578,11 +578,9 @@ async def test_handle_disconnect(self): c._trigger_event = mock.AsyncMock() await c._handle_disconnect('/') c._trigger_event.assert_any_await( - 'disconnect', namespace='/' - ) - c._trigger_event.assert_any_await( - '__disconnect_final', namespace='/' + 'disconnect', '/', c.reason.SERVER_DISCONNECT ) + c._trigger_event.assert_any_await('__disconnect_final', '/') assert not c.connected await c._handle_disconnect('/') assert c._trigger_event.await_count == 2 @@ -593,21 +591,15 @@ async def test_handle_disconnect_namespace(self): c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.AsyncMock() await c._handle_disconnect('/foo') - c._trigger_event.assert_any_await( - 'disconnect', namespace='/foo' - ) - c._trigger_event.assert_any_await( - '__disconnect_final', namespace='/foo' - ) + c._trigger_event.assert_any_await('disconnect', '/foo', + c.reason.SERVER_DISCONNECT) + c._trigger_event.assert_any_await('__disconnect_final', '/foo') assert c.namespaces == {'/bar': '2'} assert c.connected await c._handle_disconnect('/bar') - c._trigger_event.assert_any_await( - 'disconnect', namespace='/bar' - ) - c._trigger_event.assert_any_await( - '__disconnect_final', namespace='/bar' - ) + c._trigger_event.assert_any_await('disconnect', '/bar', + c.reason.SERVER_DISCONNECT) + c._trigger_event.assert_any_await('__disconnect_final', '/bar') assert c.namespaces == {} assert not c.connected @@ -617,12 +609,9 @@ async def test_handle_disconnect_unknown_namespace(self): c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.AsyncMock() await c._handle_disconnect('/baz') - c._trigger_event.assert_any_await( - 'disconnect', namespace='/baz' - ) - c._trigger_event.assert_any_await( - '__disconnect_final', namespace='/baz' - ) + c._trigger_event.assert_any_await('disconnect', '/baz', + c.reason.SERVER_DISCONNECT) + c._trigger_event.assert_any_await('__disconnect_final', '/baz') assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected @@ -632,8 +621,9 @@ async def test_handle_disconnect_default_namespaces(self): c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.AsyncMock() await c._handle_disconnect('/') - c._trigger_event.assert_any_await('disconnect', namespace='/') - c._trigger_event.assert_any_await('__disconnect_final', namespace='/') + c._trigger_event.assert_any_await('disconnect', '/', + c.reason.SERVER_DISCONNECT) + c._trigger_event.assert_any_await('__disconnect_final', '/') assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected @@ -818,6 +808,26 @@ async def test_trigger_event_namespace(self): handler.assert_awaited_once_with(1, '2') catchall_handler.assert_awaited_once_with('bar', 1, '2', 3) + async def test_trigger_legacy_disconnect_event(self): + c = async_client.AsyncClient() + + @c.on('disconnect') + def baz(): + return 'baz' + + r = await c._trigger_event('disconnect', '/', 'foo') + assert r == 'baz' + + async def test_trigger_legacy_disconnect_event_async(self): + c = async_client.AsyncClient() + + @c.on('disconnect') + async def baz(): + return 'baz' + + r = await c._trigger_event('disconnect', '/', 'foo') + assert r == 'baz' + async def test_trigger_event_class_namespace(self): c = async_client.AsyncClient() result = [] @@ -1127,10 +1137,8 @@ async def test_eio_disconnect(self): c.start_background_task = mock.MagicMock() c.sid = 'foo' c.eio.state = 'connected' - await c._handle_eio_disconnect() - c._trigger_event.assert_awaited_once_with( - 'disconnect', namespace='/' - ) + await c._handle_eio_disconnect('foo') + c._trigger_event.assert_awaited_once_with('disconnect', '/', 'foo') assert c.sid is None assert not c.connected @@ -1141,9 +1149,13 @@ async def test_eio_disconnect_namespaces(self): c._trigger_event = mock.AsyncMock() c.sid = 'foo' c.eio.state = 'connected' - await c._handle_eio_disconnect() - c._trigger_event.assert_any_await('disconnect', namespace='/foo') - c._trigger_event.assert_any_await('disconnect', namespace='/bar') + await c._handle_eio_disconnect(c.reason.CLIENT_DISCONNECT) + c._trigger_event.assert_any_await('disconnect', '/foo', + c.reason.CLIENT_DISCONNECT) + c._trigger_event.assert_any_await('disconnect', '/bar', + c.reason.CLIENT_DISCONNECT) + c._trigger_event.asserT_any_await('disconnect', '/', + c.reason.CLIENT_DISCONNECT) assert c.sid is None assert not c.connected @@ -1151,14 +1163,14 @@ async def test_eio_disconnect_reconnect(self): c = async_client.AsyncClient(reconnection=True) c.start_background_task = mock.MagicMock() c.eio.state = 'connected' - await c._handle_eio_disconnect() + await c._handle_eio_disconnect(c.reason.CLIENT_DISCONNECT) c.start_background_task.assert_called_once_with(c._handle_reconnect) async def test_eio_disconnect_self_disconnect(self): c = async_client.AsyncClient(reconnection=True) c.start_background_task = mock.MagicMock() c.eio.state = 'disconnected' - await c._handle_eio_disconnect() + await c._handle_eio_disconnect(c.reason.CLIENT_DISCONNECT) c.start_background_task.assert_not_called() async def test_eio_disconnect_no_reconnect(self): @@ -1169,13 +1181,10 @@ async def test_eio_disconnect_no_reconnect(self): c.start_background_task = mock.MagicMock() c.sid = 'foo' c.eio.state = 'connected' - await c._handle_eio_disconnect() - c._trigger_event.assert_any_await( - 'disconnect', namespace='/' - ) - c._trigger_event.assert_any_await( - '__disconnect_final', namespace='/' - ) + await c._handle_eio_disconnect(c.reason.TRANSPORT_ERROR) + c._trigger_event.assert_any_await('disconnect', '/', + c.reason.TRANSPORT_ERROR) + c._trigger_event.assert_any_await('__disconnect_final', '/') assert c.sid is None assert not c.connected c.start_background_task.assert_not_called() diff --git a/tests/async/test_manager.py b/tests/async/test_manager.py index fd5fe816..aa890649 100644 --- a/tests/async/test_manager.py +++ b/tests/async/test_manager.py @@ -183,8 +183,6 @@ async def test_close_room(self): await self.bm.enter_room(sid, '/foo', 'bar') await self.bm.enter_room(sid, '/foo', 'bar') await self.bm.close_room('bar', '/foo') - from pprint import pprint - pprint(self.bm.rooms) assert 'bar' not in self.bm.rooms['/foo'] async def test_close_invalid_room(self): diff --git a/tests/async/test_namespace.py b/tests/async/test_namespace.py index ad9b1a03..526d6769 100644 --- a/tests/async/test_namespace.py +++ b/tests/async/test_namespace.py @@ -19,13 +19,37 @@ async def on_connect(self, sid, environ): async def test_disconnect_event(self): result = {} + class MyNamespace(async_namespace.AsyncNamespace): + async def on_disconnect(self, sid, reason): + result['result'] = (sid, reason) + + ns = MyNamespace('/foo') + ns._set_server(mock.MagicMock()) + await ns.trigger_event('disconnect', 'sid', 'foo') + assert result['result'] == ('sid', 'foo') + + async def test_legacy_disconnect_event(self): + result = {} + + class MyNamespace(async_namespace.AsyncNamespace): + def on_disconnect(self, sid): + result['result'] = sid + + ns = MyNamespace('/foo') + ns._set_server(mock.MagicMock()) + await ns.trigger_event('disconnect', 'sid', 'foo') + assert result['result'] == 'sid' + + async def test_legacy_disconnect_event_async(self): + result = {} + class MyNamespace(async_namespace.AsyncNamespace): async def on_disconnect(self, sid): result['result'] = sid ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) - await ns.trigger_event('disconnect', 'sid') + await ns.trigger_event('disconnect', 'sid', 'foo') assert result['result'] == 'sid' async def test_sync_event(self): @@ -242,6 +266,42 @@ async def test_disconnect(self): await ns.disconnect('sid', namespace='/bar') ns.server.disconnect.assert_awaited_with('sid', namespace='/bar') + async def test_disconnect_event_client(self): + result = {} + + class MyNamespace(async_namespace.AsyncClientNamespace): + async def on_disconnect(self, reason): + result['result'] = reason + + ns = MyNamespace('/foo') + ns._set_client(mock.MagicMock()) + await ns.trigger_event('disconnect', 'foo') + assert result['result'] == 'foo' + + async def test_legacy_disconnect_event_client(self): + result = {} + + class MyNamespace(async_namespace.AsyncClientNamespace): + def on_disconnect(self): + result['result'] = 'ok' + + ns = MyNamespace('/foo') + ns._set_client(mock.MagicMock()) + await ns.trigger_event('disconnect', 'foo') + assert result['result'] == 'ok' + + async def test_legacy_disconnect_event_client_async(self): + result = {} + + class MyNamespace(async_namespace.AsyncClientNamespace): + async def on_disconnect(self): + result['result'] = 'ok' + + ns = MyNamespace('/foo') + ns._set_client(mock.MagicMock()) + await ns.trigger_event('disconnect', 'foo') + assert result['result'] == 'ok' + async def test_sync_event_client(self): result = {} diff --git a/tests/async/test_server.py b/tests/async/test_server.py index d9129d46..f60de27a 100644 --- a/tests/async/test_server.py +++ b/tests/async/test_server.py @@ -56,7 +56,7 @@ async def test_on_event(self, eio): def foo(): pass - def bar(): + def bar(reason): pass s.on('disconnect', bar) @@ -537,8 +537,36 @@ async def test_handle_disconnect(self, eio): s.on('disconnect', handler) await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0') - await s._handle_eio_disconnect('123') - handler.assert_called_once_with('1') + await s._handle_eio_disconnect('123', 'foo') + handler.assert_called_once_with('1', 'foo') + s.manager.disconnect.assert_awaited_once_with( + '1', '/', ignore_queue=True) + assert s.environ == {} + + async def test_handle_legacy_disconnect(self, eio): + eio.return_value.send = mock.AsyncMock() + s = async_server.AsyncServer() + s.manager.disconnect = mock.AsyncMock() + handler = mock.MagicMock(side_effect=[TypeError, None]) + s.on('disconnect', handler) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') + await s._handle_eio_disconnect('123', 'foo') + handler.assert_called_with('1') + s.manager.disconnect.assert_awaited_once_with( + '1', '/', ignore_queue=True) + assert s.environ == {} + + async def test_handle_legacy_disconnect_async(self, eio): + eio.return_value.send = mock.AsyncMock() + s = async_server.AsyncServer() + s.manager.disconnect = mock.AsyncMock() + handler = mock.AsyncMock(side_effect=[TypeError, None]) + s.on('disconnect', handler) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') + await s._handle_eio_disconnect('123', 'foo') + handler.assert_awaited_with('1') s.manager.disconnect.assert_awaited_once_with( '1', '/', ignore_queue=True) assert s.environ == {} @@ -552,9 +580,9 @@ async def test_handle_disconnect_namespace(self, eio): s.on('disconnect', handler_namespace, namespace='/foo') await s._handle_eio_connect('123', 'environ') await s._handle_eio_message('123', '0/foo,') - await s._handle_eio_disconnect('123') + await s._handle_eio_disconnect('123', 'foo') handler.assert_not_called() - handler_namespace.assert_called_once_with('1') + handler_namespace.assert_called_once_with('1', 'foo') assert s.environ == {} async def test_handle_disconnect_only_namespace(self, eio): @@ -568,13 +596,14 @@ async def test_handle_disconnect_only_namespace(self, eio): await s._handle_eio_message('123', '0/foo,') await s._handle_eio_message('123', '1/foo,') assert handler.call_count == 0 - handler_namespace.assert_called_once_with('1') + handler_namespace.assert_called_once_with( + '1', s.reason.CLIENT_DISCONNECT) assert s.environ == {'123': 'environ'} async def test_handle_disconnect_unknown_client(self, eio): mgr = self._get_mock_manager() s = async_server.AsyncServer(client_manager=mgr) - await s._handle_eio_disconnect('123') + await s._handle_eio_disconnect('123', 'foo') async def test_handle_event(self, eio): eio.return_value.send = mock.AsyncMock() @@ -624,7 +653,8 @@ async def test_handle_event_with_catchall_namespace(self, eio): await s._handle_eio_message('123', '2/bar,["msg","a","b"]') await s._handle_eio_message('123', '2/foo,["my message","a","b","c"]') await s._handle_eio_message('123', '2/bar,["my message","a","b","c"]') - await s._trigger_event('disconnect', '/bar', sid_bar) + await s._trigger_event('disconnect', '/bar', sid_bar, + s.reason.CLIENT_DISCONNECT) connect_star_handler.assert_called_once_with('/bar', sid_bar) msg_foo_handler.assert_called_once_with(sid_foo, 'a', 'b') msg_star_handler.assert_called_once_with('/bar', sid_bar, 'a', 'b') @@ -884,8 +914,8 @@ class MyNamespace(async_namespace.AsyncNamespace): def on_connect(self, sid, environ): result['result'] = (sid, environ) - async def on_disconnect(self, sid): - result['result'] = ('disconnect', sid) + async def on_disconnect(self, sid, reason): + result['result'] = ('disconnect', sid, reason) async def on_foo(self, sid, data): result['result'] = (sid, data) @@ -908,7 +938,8 @@ async def on_baz(self, sid, data1, data2): await s._handle_eio_message('123', '2/foo,["baz","a","b"]') assert result['result'] == ('a', 'b') await s.disconnect('1', '/foo') - assert result['result'] == ('disconnect', '1') + assert result['result'] == ('disconnect', '1', + s.reason.SERVER_DISCONNECT) async def test_catchall_namespace_handler(self, eio): eio.return_value.send = mock.AsyncMock() diff --git a/tests/common/test_client.py b/tests/common/test_client.py index c7399dc1..ac930c79 100644 --- a/tests/common/test_client.py +++ b/tests/common/test_client.py @@ -752,8 +752,9 @@ def test_handle_disconnect(self): c.connected = True c._trigger_event = mock.MagicMock() c._handle_disconnect('/') - c._trigger_event.assert_any_call('disconnect', namespace='/') - c._trigger_event.assert_any_call('__disconnect_final', namespace='/') + c._trigger_event.assert_any_call('disconnect', '/', + c.reason.SERVER_DISCONNECT) + c._trigger_event.assert_any_call('__disconnect_final', '/') assert not c.connected c._handle_disconnect('/') assert c._trigger_event.call_count == 2 @@ -764,21 +765,15 @@ def test_handle_disconnect_namespace(self): c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_disconnect('/foo') - c._trigger_event.assert_any_call( - 'disconnect', namespace='/foo' - ) - c._trigger_event.assert_any_call( - '__disconnect_final', namespace='/foo' - ) + c._trigger_event.assert_any_call('disconnect', '/foo', + c.reason.SERVER_DISCONNECT) + c._trigger_event.assert_any_call('__disconnect_final', '/foo') assert c.namespaces == {'/bar': '2'} assert c.connected c._handle_disconnect('/bar') - c._trigger_event.assert_any_call( - 'disconnect', namespace='/bar' - ) - c._trigger_event.assert_any_call( - '__disconnect_final', namespace='/bar' - ) + c._trigger_event.assert_any_call('disconnect', '/bar', + c.reason.SERVER_DISCONNECT) + c._trigger_event.assert_any_call('__disconnect_final', '/bar') assert c.namespaces == {} assert not c.connected @@ -788,12 +783,9 @@ def test_handle_disconnect_unknown_namespace(self): c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_disconnect('/baz') - c._trigger_event.assert_any_call( - 'disconnect', namespace='/baz' - ) - c._trigger_event.assert_any_call( - '__disconnect_final', namespace='/baz' - ) + c._trigger_event.assert_any_call('disconnect', '/baz', + c.reason.SERVER_DISCONNECT) + c._trigger_event.assert_any_call('__disconnect_final', '/baz') assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected @@ -803,9 +795,9 @@ def test_handle_disconnect_default_namespace(self): c.namespaces = {'/foo': '1', '/bar': '2'} c._trigger_event = mock.MagicMock() c._handle_disconnect('/') - print(c._trigger_event.call_args_list) - c._trigger_event.assert_any_call('disconnect', namespace='/') - c._trigger_event.assert_any_call('__disconnect_final', namespace='/') + c._trigger_event.assert_any_call('disconnect', '/', + c.reason.SERVER_DISCONNECT) + c._trigger_event.assert_any_call('__disconnect_final', '/') assert c.namespaces == {'/foo': '1', '/bar': '2'} assert c.connected @@ -1003,8 +995,8 @@ class MyNamespace(namespace.ClientNamespace): def on_connect(self, ns): result['result'] = (ns,) - def on_disconnect(self, ns): - result['result'] = ('disconnect', ns) + def on_disconnect(self, ns, reason): + result['result'] = ('disconnect', ns, reason) def on_foo(self, ns, data): result['result'] = (ns, data) @@ -1025,8 +1017,8 @@ def on_baz(self, ns, data1, data2): assert result['result'] == 'bar/foo' c._trigger_event('baz', '/foo', 'a', 'b') assert result['result'] == ('/foo', 'a', 'b') - c._trigger_event('disconnect', '/foo') - assert result['result'] == ('disconnect', '/foo') + c._trigger_event('disconnect', '/foo', 'bar') + assert result['result'] == ('disconnect', '/foo', 'bar') def test_trigger_event_class_namespace(self): c = client.Client() @@ -1286,8 +1278,8 @@ def test_eio_disconnect(self): c.start_background_task = mock.MagicMock() c.sid = 'foo' c.eio.state = 'connected' - c._handle_eio_disconnect() - c._trigger_event.assert_called_once_with('disconnect', namespace='/') + c._handle_eio_disconnect('foo') + c._trigger_event.assert_called_once_with('disconnect', '/', 'foo') assert c.sid is None assert not c.connected @@ -1299,10 +1291,13 @@ def test_eio_disconnect_namespaces(self): c.start_background_task = mock.MagicMock() c.sid = 'foo' c.eio.state = 'connected' - c._handle_eio_disconnect() - c._trigger_event.assert_any_call('disconnect', namespace='/foo') - c._trigger_event.assert_any_call('disconnect', namespace='/bar') - c._trigger_event.assert_any_call('disconnect', namespace='/') + c._handle_eio_disconnect(c.reason.CLIENT_DISCONNECT) + c._trigger_event.assert_any_call('disconnect', '/foo', + c.reason.CLIENT_DISCONNECT) + c._trigger_event.assert_any_call('disconnect', '/bar', + c.reason.CLIENT_DISCONNECT) + c._trigger_event.assert_any_call('disconnect', '/', + c.reason.CLIENT_DISCONNECT) assert c.sid is None assert not c.connected @@ -1310,14 +1305,14 @@ def test_eio_disconnect_reconnect(self): c = client.Client(reconnection=True) c.start_background_task = mock.MagicMock() c.eio.state = 'connected' - c._handle_eio_disconnect() + c._handle_eio_disconnect(c.reason.CLIENT_DISCONNECT) c.start_background_task.assert_called_once_with(c._handle_reconnect) def test_eio_disconnect_self_disconnect(self): c = client.Client(reconnection=True) c.start_background_task = mock.MagicMock() c.eio.state = 'disconnected' - c._handle_eio_disconnect() + c._handle_eio_disconnect(c.reason.CLIENT_DISCONNECT) c.start_background_task.assert_not_called() def test_eio_disconnect_no_reconnect(self): @@ -1328,9 +1323,10 @@ def test_eio_disconnect_no_reconnect(self): c.start_background_task = mock.MagicMock() c.sid = 'foo' c.eio.state = 'connected' - c._handle_eio_disconnect() - c._trigger_event.assert_any_call('disconnect', namespace='/') - c._trigger_event.assert_any_call('__disconnect_final', namespace='/') + c._handle_eio_disconnect(c.reason.TRANSPORT_ERROR) + c._trigger_event.assert_any_call('disconnect', '/', + c.reason.TRANSPORT_ERROR) + c._trigger_event.assert_any_call('__disconnect_final', '/') assert c.sid is None assert not c.connected c.start_background_task.assert_not_called() diff --git a/tests/common/test_namespace.py b/tests/common/test_namespace.py index 8bfa9899..f1476e41 100644 --- a/tests/common/test_namespace.py +++ b/tests/common/test_namespace.py @@ -19,13 +19,25 @@ def on_connect(self, sid, environ): def test_disconnect_event(self): result = {} + class MyNamespace(namespace.Namespace): + def on_disconnect(self, sid, reason): + result['result'] = (sid, reason) + + ns = MyNamespace('/foo') + ns._set_server(mock.MagicMock()) + ns.trigger_event('disconnect', 'sid', 'foo') + assert result['result'] == ('sid', 'foo') + + def test_legacy_disconnect_event(self): + result = {} + class MyNamespace(namespace.Namespace): def on_disconnect(self, sid): result['result'] = sid ns = MyNamespace('/foo') ns._set_server(mock.MagicMock()) - ns.trigger_event('disconnect', 'sid') + ns.trigger_event('disconnect', 'sid', 'foo') assert result['result'] == 'sid' def test_event(self): @@ -216,6 +228,30 @@ def test_disconnect(self): ns.disconnect('sid', namespace='/bar') ns.server.disconnect.assert_called_with('sid', namespace='/bar') + def test_disconnect_event_client(self): + result = {} + + class MyNamespace(namespace.ClientNamespace): + def on_disconnect(self, reason): + result['result'] = reason + + ns = MyNamespace('/foo') + ns._set_client(mock.MagicMock()) + ns.trigger_event('disconnect', 'foo') + assert result['result'] == 'foo' + + def test_legacy_disconnect_event_client(self): + result = {} + + class MyNamespace(namespace.ClientNamespace): + def on_disconnect(self): + result['result'] = 'ok' + + ns = MyNamespace('/foo') + ns._set_client(mock.MagicMock()) + ns.trigger_event('disconnect', 'foo') + assert result['result'] == 'ok' + def test_event_not_found_client(self): result = {} diff --git a/tests/common/test_server.py b/tests/common/test_server.py index 57ddc2f4..445d5d9e 100644 --- a/tests/common/test_server.py +++ b/tests/common/test_server.py @@ -39,7 +39,7 @@ def test_on_event(self, eio): def foo(): pass - def bar(): + def bar(reason): pass s.on('disconnect', bar) @@ -510,8 +510,21 @@ def test_handle_disconnect(self, eio): s.on('disconnect', handler) s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0') - s._handle_eio_disconnect('123') - handler.assert_called_once_with('1') + s._handle_eio_disconnect('123', 'foo') + handler.assert_called_once_with('1', 'foo') + s.manager.disconnect.assert_called_once_with('1', '/', + ignore_queue=True) + assert s.environ == {} + + def test_handle_legacy_disconnect(self, eio): + s = server.Server() + s.manager.disconnect = mock.MagicMock() + handler = mock.MagicMock(side_effect=[TypeError, None]) + s.on('disconnect', handler) + s._handle_eio_connect('123', 'environ') + s._handle_eio_message('123', '0') + s._handle_eio_disconnect('123', 'foo') + handler.assert_called_with('1') s.manager.disconnect.assert_called_once_with('1', '/', ignore_queue=True) assert s.environ == {} @@ -524,9 +537,9 @@ def test_handle_disconnect_namespace(self, eio): s.on('disconnect', handler_namespace, namespace='/foo') s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo,') - s._handle_eio_disconnect('123') + s._handle_eio_disconnect('123', 'foo') handler.assert_not_called() - handler_namespace.assert_called_once_with('1') + handler_namespace.assert_called_once_with('1', 'foo') assert s.environ == {} def test_handle_disconnect_only_namespace(self, eio): @@ -539,13 +552,14 @@ def test_handle_disconnect_only_namespace(self, eio): s._handle_eio_message('123', '0/foo,') s._handle_eio_message('123', '1/foo,') assert handler.call_count == 0 - handler_namespace.assert_called_once_with('1') + handler_namespace.assert_called_once_with( + '1', s.reason.CLIENT_DISCONNECT) assert s.environ == {'123': 'environ'} def test_handle_disconnect_unknown_client(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) - s._handle_eio_disconnect('123') + s._handle_eio_disconnect('123', 'foo') def test_handle_event(self, eio): s = server.Server(async_handlers=False) @@ -596,7 +610,8 @@ def test_handle_event_with_catchall_namespace(self, eio): s._handle_eio_message('123', '2/bar,["msg","a","b"]') s._handle_eio_message('123', '2/foo,["my message","a","b","c"]') s._handle_eio_message('123', '2/bar,["my message","a","b","c"]') - s._trigger_event('disconnect', '/bar', sid_bar) + s._trigger_event('disconnect', '/bar', sid_bar, + s.reason.CLIENT_DISCONNECT) connect_star_handler.assert_called_once_with('/bar', sid_bar) msg_foo_handler.assert_called_once_with(sid_foo, 'a', 'b') msg_star_handler.assert_called_once_with('/bar', sid_bar, 'a', 'b') @@ -825,8 +840,8 @@ class MyNamespace(namespace.Namespace): def on_connect(self, sid, environ): result['result'] = (sid, environ) - def on_disconnect(self, sid): - result['result'] = ('disconnect', sid) + def on_disconnect(self, sid, reason): + result['result'] = ('disconnect', sid, reason) def on_foo(self, sid, data): result['result'] = (sid, data) @@ -849,7 +864,8 @@ def on_baz(self, sid, data1, data2): s._handle_eio_message('123', '2/foo,["baz","a","b"]') assert result['result'] == ('a', 'b') s.disconnect('1', '/foo') - assert result['result'] == ('disconnect', '1') + assert result['result'] == ('disconnect', '1', + s.reason.SERVER_DISCONNECT) def test_catchall_namespace_handler(self, eio): result = {} From 06f50f8671c2c3bd9d3d5ba621049c1790fd41e1 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 18 Dec 2024 17:43:08 +0000 Subject: [PATCH 487/571] Release 5.12.0 --- CHANGES.md | 14 ++++++++++++++ pyproject.toml | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c7e0a060..6f7b06d1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,19 @@ # python-socketio change log +**Release 5.12.0** - 2024-12-18 + +- Added a `reason` argument to the disconnect handler [#1422](https://github.com/miguelgrinberg/python-socketio/issues/1422) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bd8555da8523d1a73432685a00eb5acb4d2261f5)) +- Prevented starting multiple tasks for reconnection [#1369](https://github.com/miguelgrinberg/python-socketio/issues/1369) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b6ee33e56cf2679664c1b894bf7e5d33a30976db)) (thanks **humayunsr**!) +- Fixed `AsyncClient::wait()` unexpected return after success reconnect [#1407](https://github.com/miguelgrinberg/python-socketio/issues/1407) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/78d1124c50cff149051fb89bf6f08000bf184da5)) (thanks **Arseny**!) +- Removed old constructs from old and unsupported Python versions ([commit](https://github.com/miguelgrinberg/python-socketio/commit/db642bb2bd9794eeceddd54abc47665c69e85406)) +- Removed dependency on unittest.TestCase base class in unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/abf336e108b01f44afb473eb86c1dece6360195c)) +- Adopted pyenv-asyncio for async unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/0b5c4638e5e4bff06fcf46476d218ae5ad4ada14)) +- Adopted unittest.mock.AsyncMock in async unit tests ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8f0e66c1cd1cd63dcef703576cc9cb9c99104df7)) +- Fix typo with `AsyncClient.connect` example [#1403](https://github.com/miguelgrinberg/python-socketio/issues/1403) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/72d37ea79f4cf6076591782e4781fd4868a7e0d6)) (thanks **Peter Bierma**!) +- Documentation typo [#1421](https://github.com/miguelgrinberg/python-socketio/issues/1421) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bf5a05ae9bf94b2fbd1367a04884ef5a39cd2671)) (thanks **Masen Furer**!) +- Renamed flask-socketio references to python-socketio in documentation [#1377](https://github.com/miguelgrinberg/python-socketio/issues/1377) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5f83cd0f7b2911705eaf1a8cb9060afbee6eb456)) +- Added Python 3.13 CI builds ([commit](https://github.com/miguelgrinberg/python-socketio/commit/42da5d2f5426e812fd37d4cabcb9277810cae9c1)) + **Release 5.11.4** - 2024-09-02 - Prevent crash when client sends empty event ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1b901de0077322eedb3509318ccba939f5f0bf10)) diff --git a/pyproject.toml b/pyproject.toml index a3ebb6f6..4653b435 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.11.5.dev0" +version = "5.12.0" authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, ] From 2eaa1bd5b17a8a6d15913de10815008ca121cec0 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 18 Dec 2024 17:54:51 +0000 Subject: [PATCH 488/571] Version 5.12.1.dev0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4653b435..ac763bb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.12.0" +version = "5.12.1.dev0" authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, ] From 7e8e884d50b59d90a2a57ff53df990fcdfbb1b2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 16:23:18 +0000 Subject: [PATCH 489/571] Bump jinja2 from 3.1.3 to 3.1.5 in /examples/server/wsgi (#1426) #nolog Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.5. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.5) --- updated-dependencies: - dependency-name: jinja2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/requirements.txt b/examples/server/wsgi/requirements.txt index 36a36420..dd465d06 100644 --- a/examples/server/wsgi/requirements.txt +++ b/examples/server/wsgi/requirements.txt @@ -5,7 +5,7 @@ eventlet==0.35.2 Flask==1.0.2 greenlet==0.4.12 itsdangerous==1.1.0 -Jinja2==3.1.3 +Jinja2==3.1.5 MarkupSafe==1.1.0 packaging==16.8 pyparsing==2.1.10 From 8964dab9d545333646fafad9aae0becd761a1045 Mon Sep 17 00:00:00 2001 From: Carlos Guerrero Date: Wed, 25 Dec 2024 07:17:31 -0500 Subject: [PATCH 490/571] Fixed broken gevent URL in documentation (#1427) --- docs/intro.rst | 2 +- docs/server.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/intro.rst b/docs/intro.rst index b05e072c..9bb301df 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -189,7 +189,7 @@ Server Features - Can be hosted on any `WSGI `_ or `ASGI `_ web server including `Gunicorn `_, `Uvicorn `_, - `eventlet `_ and `gevent `_. + `eventlet `_ and `gevent `_. - Can be integrated with WSGI applications written in frameworks such as Flask, Django, etc. - Can be integrated with `aiohttp `_, diff --git a/docs/server.rst b/docs/server.rst index ed15ed32..eb91c292 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -838,7 +838,7 @@ Gevent When a multi-threaded web server is unable to satisfy the concurrency and scalability requirements of the application, an option to try is -`Gevent `_. Gevent is a coroutine-based concurrency library +`Gevent `_. Gevent is a coroutine-based concurrency library based on greenlets, which are significantly lighter than threads. Instances of class ``socketio.Server`` will automatically use Gevent if the From b75fd31625cfea0d8c67d776070e4f8de99c1e45 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 29 Dec 2024 18:57:54 +0000 Subject: [PATCH 491/571] Fix admin instrumentation to support disconnect reasons (Fixes #1423) --- src/socketio/admin.py | 82 +++++++++++++++---------------------- src/socketio/async_admin.py | 82 +++++++++++++++---------------------- 2 files changed, 68 insertions(+), 96 deletions(-) diff --git a/src/socketio/admin.py b/src/socketio/admin.py index 58c8aff9..8d6fbcf5 100644 --- a/src/socketio/admin.py +++ b/src/socketio/admin.py @@ -77,13 +77,9 @@ def instrument(self): # track socket connection times self.sio.manager._timestamps = {} - # report socket.io connections - self.sio.manager.__connect = self.sio.manager.connect - self.sio.manager.connect = self._connect - - # report socket.io disconnection - self.sio.manager.__disconnect = self.sio.manager.disconnect - self.sio.manager.disconnect = self._disconnect + # report socket.io connections, disconnections and received events + self.sio.__trigger_event = self.sio._trigger_event + self.sio._trigger_event = self._trigger_event # report join rooms self.sio.manager.__basic_enter_room = \ @@ -99,10 +95,6 @@ def instrument(self): self.sio.manager.__emit = self.sio.manager.emit self.sio.manager.emit = self._emit - # report receive events - self.sio.__handle_event_internal = self.sio._handle_event_internal - self.sio._handle_event_internal = self._handle_event_internal - # report engine.io connections self.sio.eio.on('connect', self._handle_eio_connect) self.sio.eio.on('disconnect', self._handle_eio_disconnect) @@ -128,14 +120,12 @@ def instrument(self): def uninstrument(self): # pragma: no cover if self.mode == 'development': - self.sio.manager.connect = self.sio.manager.__connect - self.sio.manager.disconnect = self.sio.manager.__disconnect + self.sio._trigger_event = self.sio.__trigger_event self.sio.manager.basic_enter_room = \ self.sio.manager.__basic_enter_room self.sio.manager.basic_leave_room = \ self.sio.manager.__basic_leave_room self.sio.manager.emit = self.sio.manager.__emit - self.sio._handle_event_internal = self.sio.__handle_event_internal self.sio.eio._ok = self.sio.eio.__ok from engineio.socket import Socket @@ -205,26 +195,34 @@ def shutdown(self): self.stop_stats_event.set() self.stats_task.join() - def _connect(self, eio_sid, namespace): - sid = self.sio.manager.__connect(eio_sid, namespace) + def _trigger_event(self, event, namespace, *args): t = time.time() - self.sio.manager._timestamps[sid] = t - serialized_socket = self.serialize_socket(sid, namespace, eio_sid) - self.sio.emit('socket_connected', ( - serialized_socket, - datetime.utcfromtimestamp(t).isoformat() + 'Z', - ), namespace=self.admin_namespace) - return sid - - def _disconnect(self, sid, namespace, **kwargs): - del self.sio.manager._timestamps[sid] - self.sio.emit('socket_disconnected', ( - namespace, - sid, - 'N/A', - datetime.utcnow().isoformat() + 'Z', - ), namespace=self.admin_namespace) - return self.sio.manager.__disconnect(sid, namespace, **kwargs) + sid = args[0] + if event == 'connect': + eio_sid = self.sio.manager.eio_sid_from_sid(sid, namespace) + self.sio.manager._timestamps[sid] = t + serialized_socket = self.serialize_socket(sid, namespace, eio_sid) + self.sio.emit('socket_connected', ( + serialized_socket, + datetime.utcfromtimestamp(t).isoformat() + 'Z', + ), namespace=self.admin_namespace) + elif event == 'disconnect': + del self.sio.manager._timestamps[sid] + reason = args[1] + self.sio.emit('socket_disconnected', ( + namespace, + sid, + reason, + datetime.utcfromtimestamp(t).isoformat() + 'Z', + ), namespace=self.admin_namespace) + else: + self.sio.emit('event_received', ( + namespace, + sid, + (event, *args[1:]), + datetime.utcfromtimestamp(t).isoformat() + 'Z', + ), namespace=self.admin_namespace) + return self.sio.__trigger_event(event, namespace, *args) def _check_for_upgrade(self, eio_sid, sid, namespace): # pragma: no cover for _ in range(5): @@ -269,7 +267,7 @@ def _emit(self, event, data, namespace, room=None, skip_sid=None, **kwargs) if namespace != self.admin_namespace: event_data = [event] + list(data) if isinstance(data, tuple) \ - else [data] + else [event, data] if not isinstance(skip_sid, list): # pragma: no branch skip_sid = [skip_sid] for sid, _ in self.sio.manager.get_participants(namespace, room): @@ -282,18 +280,6 @@ def _emit(self, event, data, namespace, room=None, skip_sid=None, ), namespace=self.admin_namespace) return ret - def _handle_event_internal(self, server, sid, eio_sid, data, namespace, - id): - ret = self.sio.__handle_event_internal(server, sid, eio_sid, data, - namespace, id) - self.sio.emit('event_received', ( - namespace, - sid, - data, - datetime.utcnow().isoformat() + 'Z', - ), namespace=self.admin_namespace) - return ret - def _handle_eio_connect(self, eio_sid, environ): if self.stop_stats_event is None: self.stop_stats_event = self.sio.eio.create_event() @@ -303,9 +289,9 @@ def _handle_eio_connect(self, eio_sid, environ): self.event_buffer.push('rawConnection') return self.sio._handle_eio_connect(eio_sid, environ) - def _handle_eio_disconnect(self, eio_sid): + def _handle_eio_disconnect(self, eio_sid, reason): self.event_buffer.push('rawDisconnection') - return self.sio._handle_eio_disconnect(eio_sid) + return self.sio._handle_eio_disconnect(eio_sid, reason) def _eio_http_response(self, packets=None, headers=None, jsonp_index=None): ret = self.sio.eio.__ok(packets=packets, headers=headers, diff --git a/src/socketio/async_admin.py b/src/socketio/async_admin.py index 162c5660..68ea289c 100644 --- a/src/socketio/async_admin.py +++ b/src/socketio/async_admin.py @@ -58,13 +58,9 @@ def instrument(self): # track socket connection times self.sio.manager._timestamps = {} - # report socket.io connections - self.sio.manager.__connect = self.sio.manager.connect - self.sio.manager.connect = self._connect - - # report socket.io disconnection - self.sio.manager.__disconnect = self.sio.manager.disconnect - self.sio.manager.disconnect = self._disconnect + # report socket.io connections, disconnections and received events + self.sio.__trigger_event = self.sio._trigger_event + self.sio._trigger_event = self._trigger_event # report join rooms self.sio.manager.__basic_enter_room = \ @@ -80,10 +76,6 @@ def instrument(self): self.sio.manager.__emit = self.sio.manager.emit self.sio.manager.emit = self._emit - # report receive events - self.sio.__handle_event_internal = self.sio._handle_event_internal - self.sio._handle_event_internal = self._handle_event_internal - # report engine.io connections self.sio.eio.on('connect', self._handle_eio_connect) self.sio.eio.on('disconnect', self._handle_eio_disconnect) @@ -109,14 +101,12 @@ def instrument(self): def uninstrument(self): # pragma: no cover if self.mode == 'development': - self.sio.manager.connect = self.sio.manager.__connect - self.sio.manager.disconnect = self.sio.manager.__disconnect + self.sio._trigger_event = self.sio.__trigger_event self.sio.manager.basic_enter_room = \ self.sio.manager.__basic_enter_room self.sio.manager.basic_leave_room = \ self.sio.manager.__basic_leave_room self.sio.manager.emit = self.sio.manager.__emit - self.sio._handle_event_internal = self.sio.__handle_event_internal self.sio.eio._ok = self.sio.eio.__ok from engineio.async_socket import AsyncSocket @@ -193,26 +183,34 @@ async def shutdown(self): self.stop_stats_event.set() await asyncio.gather(self.stats_task) - async def _connect(self, eio_sid, namespace): - sid = await self.sio.manager.__connect(eio_sid, namespace) + async def _trigger_event(self, event, namespace, *args): t = time.time() - self.sio.manager._timestamps[sid] = t - serialized_socket = self.serialize_socket(sid, namespace, eio_sid) - await self.sio.emit('socket_connected', ( - serialized_socket, - datetime.utcfromtimestamp(t).isoformat() + 'Z', - ), namespace=self.admin_namespace) - return sid - - async def _disconnect(self, sid, namespace, **kwargs): - del self.sio.manager._timestamps[sid] - await self.sio.emit('socket_disconnected', ( - namespace, - sid, - 'N/A', - datetime.utcnow().isoformat() + 'Z', - ), namespace=self.admin_namespace) - return await self.sio.manager.__disconnect(sid, namespace, **kwargs) + sid = args[0] + if event == 'connect': + eio_sid = self.sio.manager.eio_sid_from_sid(sid, namespace) + self.sio.manager._timestamps[sid] = t + serialized_socket = self.serialize_socket(sid, namespace, eio_sid) + await self.sio.emit('socket_connected', ( + serialized_socket, + datetime.utcfromtimestamp(t).isoformat() + 'Z', + ), namespace=self.admin_namespace) + elif event == 'disconnect': + del self.sio.manager._timestamps[sid] + reason = args[1] + await self.sio.emit('socket_disconnected', ( + namespace, + sid, + reason, + datetime.utcfromtimestamp(t).isoformat() + 'Z', + ), namespace=self.admin_namespace) + else: + await self.sio.emit('event_received', ( + namespace, + sid, + (event, *args[1:]), + datetime.utcfromtimestamp(t).isoformat() + 'Z', + ), namespace=self.admin_namespace) + return await self.sio.__trigger_event(event, namespace, *args) async def _check_for_upgrade(self, eio_sid, sid, namespace): # pragma: no cover @@ -258,7 +256,7 @@ async def _emit(self, event, data, namespace, room=None, skip_sid=None, callback=callback, **kwargs) if namespace != self.admin_namespace: event_data = [event] + list(data) if isinstance(data, tuple) \ - else [data] + else [event, data] if not isinstance(skip_sid, list): # pragma: no branch skip_sid = [skip_sid] for sid, _ in self.sio.manager.get_participants(namespace, room): @@ -271,18 +269,6 @@ async def _emit(self, event, data, namespace, room=None, skip_sid=None, ), namespace=self.admin_namespace) return ret - async def _handle_event_internal(self, server, sid, eio_sid, data, - namespace, id): - ret = await self.sio.__handle_event_internal(server, sid, eio_sid, - data, namespace, id) - await self.sio.emit('event_received', ( - namespace, - sid, - data, - datetime.utcnow().isoformat() + 'Z', - ), namespace=self.admin_namespace) - return ret - async def _handle_eio_connect(self, eio_sid, environ): if self.stop_stats_event is None: self.stop_stats_event = self.sio.eio.create_event() @@ -292,9 +278,9 @@ async def _handle_eio_connect(self, eio_sid, environ): self.event_buffer.push('rawConnection') return await self.sio._handle_eio_connect(eio_sid, environ) - async def _handle_eio_disconnect(self, eio_sid): + async def _handle_eio_disconnect(self, eio_sid, reason): self.event_buffer.push('rawDisconnection') - return await self.sio._handle_eio_disconnect(eio_sid) + return await self.sio._handle_eio_disconnect(eio_sid, reason) def _eio_http_response(self, packets=None, headers=None, jsonp_index=None): ret = self.sio.eio.__ok(packets=packets, headers=headers, From 8fe012abbb350107b742ab2cf9aa44d328bc23e9 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 29 Dec 2024 19:54:27 +0000 Subject: [PATCH 492/571] Stop using deprecated datetime functions --- src/socketio/admin.py | 18 +++++++++--------- src/socketio/async_admin.py | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/socketio/admin.py b/src/socketio/admin.py index 8d6fbcf5..12b905ea 100644 --- a/src/socketio/admin.py +++ b/src/socketio/admin.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone import functools import os import socket @@ -204,7 +204,7 @@ def _trigger_event(self, event, namespace, *args): serialized_socket = self.serialize_socket(sid, namespace, eio_sid) self.sio.emit('socket_connected', ( serialized_socket, - datetime.utcfromtimestamp(t).isoformat() + 'Z', + datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) elif event == 'disconnect': del self.sio.manager._timestamps[sid] @@ -213,14 +213,14 @@ def _trigger_event(self, event, namespace, *args): namespace, sid, reason, - datetime.utcfromtimestamp(t).isoformat() + 'Z', + datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) else: self.sio.emit('event_received', ( namespace, sid, (event, *args[1:]), - datetime.utcfromtimestamp(t).isoformat() + 'Z', + datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) return self.sio.__trigger_event(event, namespace, *args) @@ -246,7 +246,7 @@ def _basic_enter_room(self, sid, namespace, room, eio_sid=None): namespace, room, sid, - datetime.utcnow().isoformat() + 'Z', + datetime.now(timezone.utc).isoformat(), ), namespace=self.admin_namespace) return ret @@ -256,7 +256,7 @@ def _basic_leave_room(self, sid, namespace, room): namespace, room, sid, - datetime.utcnow().isoformat() + 'Z', + datetime.now(timezone.utc).isoformat(), ), namespace=self.admin_namespace) return self.sio.manager.__basic_leave_room(sid, namespace, room) @@ -276,7 +276,7 @@ def _emit(self, event, data, namespace, room=None, skip_sid=None, namespace, sid, event_data, - datetime.utcnow().isoformat() + 'Z', + datetime.now(timezone.utc).isoformat(), ), namespace=self.admin_namespace) return ret @@ -335,7 +335,7 @@ def _eio_send_ping(socket, self): # pragma: no cover eio_sid) self.sio.emit('socket_connected', ( serialized_socket, - datetime.utcfromtimestamp(t).isoformat() + 'Z', + datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) return socket.__send_ping() @@ -384,7 +384,7 @@ def serialize_socket(self, sid, namespace, eio_sid=None): 'secure': environ.get('wsgi.url_scheme', '') == 'https', 'url': environ.get('PATH_INFO', ''), 'issued': tm * 1000, - 'time': datetime.utcfromtimestamp(tm).isoformat() + 'Z' + 'time': datetime.fromtimestamp(tm, timezone.utc).isoformat() if tm else '', }, 'rooms': self.sio.manager.get_rooms(sid, namespace), diff --git a/src/socketio/async_admin.py b/src/socketio/async_admin.py index 68ea289c..b052d8fe 100644 --- a/src/socketio/async_admin.py +++ b/src/socketio/async_admin.py @@ -1,5 +1,5 @@ import asyncio -from datetime import datetime +from datetime import datetime, timezone import functools import os import socket @@ -192,7 +192,7 @@ async def _trigger_event(self, event, namespace, *args): serialized_socket = self.serialize_socket(sid, namespace, eio_sid) await self.sio.emit('socket_connected', ( serialized_socket, - datetime.utcfromtimestamp(t).isoformat() + 'Z', + datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) elif event == 'disconnect': del self.sio.manager._timestamps[sid] @@ -201,14 +201,14 @@ async def _trigger_event(self, event, namespace, *args): namespace, sid, reason, - datetime.utcfromtimestamp(t).isoformat() + 'Z', + datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) else: await self.sio.emit('event_received', ( namespace, sid, (event, *args[1:]), - datetime.utcfromtimestamp(t).isoformat() + 'Z', + datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) return await self.sio.__trigger_event(event, namespace, *args) @@ -235,7 +235,7 @@ def _basic_enter_room(self, sid, namespace, room, eio_sid=None): namespace, room, sid, - datetime.utcnow().isoformat() + 'Z', + datetime.now(timezone.utc).isoformat(), ))) return ret @@ -245,7 +245,7 @@ def _basic_leave_room(self, sid, namespace, room): namespace, room, sid, - datetime.utcnow().isoformat() + 'Z', + datetime.now(timezone.utc).isoformat(), ))) return self.sio.manager.__basic_leave_room(sid, namespace, room) @@ -265,7 +265,7 @@ async def _emit(self, event, data, namespace, room=None, skip_sid=None, namespace, sid, event_data, - datetime.utcnow().isoformat() + 'Z', + datetime.now(timezone.utc).isoformat(), ), namespace=self.admin_namespace) return ret @@ -324,7 +324,7 @@ async def _eio_send_ping(socket, self): # pragma: no cover eio_sid) await self.sio.emit('socket_connected', ( serialized_socket, - datetime.utcfromtimestamp(t).isoformat() + 'Z', + datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) return await socket.__send_ping() @@ -377,7 +377,7 @@ def serialize_socket(self, sid, namespace, eio_sid=None): 'secure': environ.get('wsgi.url_scheme', '') == 'https', 'url': environ.get('PATH_INFO', ''), 'issued': tm * 1000, - 'time': datetime.utcfromtimestamp(tm).isoformat() + 'Z' + 'time': datetime.fromtimestamp(tm, timezone.utc).isoformat() if tm else '', }, 'rooms': self.sio.manager.get_rooms(sid, namespace), From 269332da8041df115e3a1e2ca04808c3179a72e1 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 29 Dec 2024 20:03:59 +0000 Subject: [PATCH 493/571] Enable instrumentation by default in WSGI and ASGI examples --- examples/server/asgi/app.py | 12 +++++++++++- examples/server/wsgi/app.py | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/examples/server/asgi/app.py b/examples/server/asgi/app.py index d549ab09..996dc272 100644 --- a/examples/server/asgi/app.py +++ b/examples/server/asgi/app.py @@ -2,7 +2,7 @@ # set instrument to `True` to accept connections from the official Socket.IO # Admin UI hosted at https://admin.socket.io -instrument = False +instrument = True admin_login = { 'username': 'admin', 'password': 'python', # change this to a strong secret for production use! @@ -93,4 +93,14 @@ def test_disconnect(sid, reason): if __name__ == '__main__': + if instrument: + print('The server is instrumented for remote administration.') + print( + 'Use the official Socket.IO Admin UI at https://admin.socket.io ' + 'with the following connection details:' + ) + print(' - Server URL: http://localhost:5000') + print(' - Username:', admin_login['username']) + print(' - Password:', admin_login['password']) + print('') uvicorn.run(app, host='127.0.0.1', port=5000) diff --git a/examples/server/wsgi/app.py b/examples/server/wsgi/app.py index 62bd59b1..7fc871b4 100644 --- a/examples/server/wsgi/app.py +++ b/examples/server/wsgi/app.py @@ -5,7 +5,7 @@ # set instrument to `True` to accept connections from the official Socket.IO # Admin UI hosted at https://admin.socket.io -instrument = False +instrument = True admin_login = { 'username': 'admin', 'password': 'python', # change this to a strong secret for production use! @@ -99,6 +99,16 @@ def disconnect(sid, reason): if __name__ == '__main__': + if instrument: + print('The server is instrumented for remote administration.') + print( + 'Use the official Socket.IO Admin UI at https://admin.socket.io ' + 'with the following connection details:' + ) + print(' - Server URL: http://localhost:5000') + print(' - Username:', admin_login['username']) + print(' - Password:', admin_login['password']) + print('') if sio.async_mode == 'threading': # deploy with Werkzeug app.run(threaded=True) From 572648bd0cb18b3c40a9f665a3bd6835730c53fd Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 29 Dec 2024 20:07:59 +0000 Subject: [PATCH 494/571] Release 5.12.1 --- CHANGES.md | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 6f7b06d1..53afe357 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ # python-socketio change log +**Release 5.12.1** - 2024-12-29 + +- Fix admin instrumentation support of disconnect reasons [#1423](https://github.com/miguelgrinberg/python-socketio/issues/1423) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b75fd31625cfea0d8c67d776070e4f8de99c1e45)) +- Stop using deprecated datetime functions ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8fe012abbb350107b742ab2cf9aa44d328bc23e9)) +- Enable admin instrumentation by default in WSGI and ASGI examples ([commit](https://github.com/miguelgrinberg/python-socketio/commit/269332da8041df115e3a1e2ca04808c3179a72e1)) +- Fixed broken gevent URL in documentation [#1427](https://github.com/miguelgrinberg/python-socketio/issues/1427) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/8964dab9d545333646fafad9aae0becd761a1045)) (thanks **Carlos Guerrero**!) + **Release 5.12.0** - 2024-12-18 - Added a `reason` argument to the disconnect handler [#1422](https://github.com/miguelgrinberg/python-socketio/issues/1422) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bd8555da8523d1a73432685a00eb5acb4d2261f5)) diff --git a/pyproject.toml b/pyproject.toml index ac763bb6..02d3e9b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.12.1.dev0" +version = "5.12.1" authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, ] From 387f996bdb9a4effc6d8a358410f04b1199e527c Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 29 Dec 2024 20:11:53 +0000 Subject: [PATCH 495/571] Version 5.12.2.dev0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 02d3e9b5..c3c629d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.12.1" +version = "5.12.2.dev0" authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, ] From 9e43a39cfe562b1c13035f2017cd9d2f78e51f8d Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Mon, 6 Jan 2025 17:47:15 +0000 Subject: [PATCH 496/571] revert to default funding file #nolog --- .github/FUNDING.yml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 527fcb95..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,4 +0,0 @@ -github: miguelgrinberg -patreon: miguelgrinberg -open_collective: python-socketio -custom: https://paypal.me/miguelgrinberg From a598a55cc178268d78274f433bb330fb756bf0a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:23:47 +0000 Subject: [PATCH 497/571] Bump django in /examples/server/wsgi/django_socketio (#1430) #nolog Bumps [django](https://github.com/django/django) from 4.2.17 to 4.2.18. - [Commits](https://github.com/django/django/compare/4.2.17...4.2.18) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/django_socketio/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/django_socketio/requirements.txt b/examples/server/wsgi/django_socketio/requirements.txt index 98d2cf1f..975beff1 100644 --- a/examples/server/wsgi/django_socketio/requirements.txt +++ b/examples/server/wsgi/django_socketio/requirements.txt @@ -1,6 +1,6 @@ asgiref==3.6.0 bidict==0.22.1 -Django==4.2.17 +Django==4.2.18 gunicorn==22.0.0 h11==0.14.0 python-engineio From 7605630bb236b4baf98574ca2a8f0cdba2696ef4 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 5 Feb 2025 23:56:45 +0000 Subject: [PATCH 498/571] Allow custom client subclasses to be used in SimpleClient and AsyncSimpleClient (Fixes #1432) --- src/socketio/async_simple_client.py | 5 ++- src/socketio/simple_client.py | 5 ++- tests/async/test_simple_client.py | 61 ++++++++++++++++------------- tests/common/test_simple_client.py | 44 +++++++++++++-------- 4 files changed, 68 insertions(+), 47 deletions(-) diff --git a/src/socketio/async_simple_client.py b/src/socketio/async_simple_client.py index c6cd4fc1..adac6ead 100644 --- a/src/socketio/async_simple_client.py +++ b/src/socketio/async_simple_client.py @@ -12,6 +12,8 @@ class AsyncSimpleClient: The positional and keyword arguments given in the constructor are passed to the underlying :func:`socketio.AsyncClient` object. """ + client_class = AsyncClient + def __init__(self, *args, **kwargs): self.client_args = args self.client_kwargs = kwargs @@ -60,7 +62,8 @@ async def connect(self, url, headers={}, auth=None, transports=None, self.namespace = namespace self.input_buffer = [] self.input_event.clear() - self.client = AsyncClient(*self.client_args, **self.client_kwargs) + self.client = self.client_class( + *self.client_args, **self.client_kwargs) @self.client.event(namespace=self.namespace) def connect(): # pragma: no cover diff --git a/src/socketio/simple_client.py b/src/socketio/simple_client.py index 67791477..3f046b4b 100644 --- a/src/socketio/simple_client.py +++ b/src/socketio/simple_client.py @@ -12,6 +12,8 @@ class SimpleClient: The positional and keyword arguments given in the constructor are passed to the underlying :func:`socketio.Client` object. """ + client_class = Client + def __init__(self, *args, **kwargs): self.client_args = args self.client_kwargs = kwargs @@ -58,7 +60,8 @@ def connect(self, url, headers={}, auth=None, transports=None, self.namespace = namespace self.input_buffer = [] self.input_event.clear() - self.client = Client(*self.client_args, **self.client_kwargs) + self.client = self.client_class( + *self.client_args, **self.client_kwargs) @self.client.event(namespace=self.namespace) def connect(): # pragma: no cover diff --git a/tests/async/test_simple_client.py b/tests/async/test_simple_client.py index 08926922..bfe2a90f 100644 --- a/tests/async/test_simple_client.py +++ b/tests/async/test_simple_client.py @@ -16,46 +16,51 @@ async def test_constructor(self): assert not client.connected async def test_connect(self): + mock_client = mock.MagicMock() + original_client_class = AsyncSimpleClient.client_class + AsyncSimpleClient.client_class = mock_client + client = AsyncSimpleClient(123, a='b') - with mock.patch('socketio.async_simple_client.AsyncClient') \ - as mock_client: + mock_client.return_value.connect = mock.AsyncMock() + + await client.connect('url', headers='h', auth='a', transports='t', + namespace='n', socketio_path='s', + wait_timeout='w') + mock_client.assert_called_once_with(123, a='b') + assert client.client == mock_client() + mock_client().connect.assert_awaited_once_with( + 'url', headers='h', auth='a', transports='t', + namespaces=['n'], socketio_path='s', wait_timeout='w') + mock_client().event.call_count == 3 + mock_client().on.assert_called_once_with('*', namespace='n') + assert client.namespace == 'n' + assert not client.input_event.is_set() + + AsyncSimpleClient.client_class = original_client_class + + async def test_connect_context_manager(self): + mock_client = mock.MagicMock() + original_client_class = AsyncSimpleClient.client_class + AsyncSimpleClient.client_class = mock_client + + async with AsyncSimpleClient(123, a='b') as client: mock_client.return_value.connect = mock.AsyncMock() - await client.connect('url', headers='h', auth='a', transports='t', - namespace='n', socketio_path='s', - wait_timeout='w') + await client.connect('url', headers='h', auth='a', + transports='t', namespace='n', + socketio_path='s', wait_timeout='w') mock_client.assert_called_once_with(123, a='b') assert client.client == mock_client() mock_client().connect.assert_awaited_once_with( 'url', headers='h', auth='a', transports='t', namespaces=['n'], socketio_path='s', wait_timeout='w') mock_client().event.call_count == 3 - mock_client().on.assert_called_once_with('*', namespace='n') + mock_client().on.assert_called_once_with( + '*', namespace='n') assert client.namespace == 'n' assert not client.input_event.is_set() - async def test_connect_context_manager(self): - async def _t(): - async with AsyncSimpleClient(123, a='b') as client: - with mock.patch('socketio.async_simple_client.AsyncClient') \ - as mock_client: - mock_client.return_value.connect = mock.AsyncMock() - - await client.connect('url', headers='h', auth='a', - transports='t', namespace='n', - socketio_path='s', wait_timeout='w') - mock_client.assert_called_once_with(123, a='b') - assert client.client == mock_client() - mock_client().connect.assert_awaited_once_with( - 'url', headers='h', auth='a', transports='t', - namespaces=['n'], socketio_path='s', wait_timeout='w') - mock_client().event.call_count == 3 - mock_client().on.assert_called_once_with( - '*', namespace='n') - assert client.namespace == 'n' - assert not client.input_event.is_set() - - await _t() + AsyncSimpleClient.client_class = original_client_class async def test_connect_twice(self): client = AsyncSimpleClient(123, a='b') diff --git a/tests/common/test_simple_client.py b/tests/common/test_simple_client.py index 42790573..b17afbcc 100644 --- a/tests/common/test_simple_client.py +++ b/tests/common/test_simple_client.py @@ -14,10 +14,34 @@ def test_constructor(self): assert not client.connected def test_connect(self): + mock_client = mock.MagicMock() + original_client_class = SimpleClient.client_class + SimpleClient.client_class = mock_client + client = SimpleClient(123, a='b') - with mock.patch('socketio.simple_client.Client') as mock_client: + client.connect('url', headers='h', auth='a', transports='t', + namespace='n', socketio_path='s', wait_timeout='w') + mock_client.assert_called_once_with(123, a='b') + assert client.client == mock_client() + mock_client().connect.assert_called_once_with( + 'url', headers='h', auth='a', transports='t', + namespaces=['n'], socketio_path='s', wait_timeout='w') + mock_client().event.call_count == 3 + mock_client().on.assert_called_once_with('*', namespace='n') + assert client.namespace == 'n' + assert not client.input_event.is_set() + + SimpleClient.client_class = original_client_class + + def test_connect_context_manager(self): + mock_client = mock.MagicMock() + original_client_class = SimpleClient.client_class + SimpleClient.client_class = mock_client + + with SimpleClient(123, a='b') as client: client.connect('url', headers='h', auth='a', transports='t', - namespace='n', socketio_path='s', wait_timeout='w') + namespace='n', socketio_path='s', + wait_timeout='w') mock_client.assert_called_once_with(123, a='b') assert client.client == mock_client() mock_client().connect.assert_called_once_with( @@ -28,21 +52,7 @@ def test_connect(self): assert client.namespace == 'n' assert not client.input_event.is_set() - def test_connect_context_manager(self): - with SimpleClient(123, a='b') as client: - with mock.patch('socketio.simple_client.Client') as mock_client: - client.connect('url', headers='h', auth='a', transports='t', - namespace='n', socketio_path='s', - wait_timeout='w') - mock_client.assert_called_once_with(123, a='b') - assert client.client == mock_client() - mock_client().connect.assert_called_once_with( - 'url', headers='h', auth='a', transports='t', - namespaces=['n'], socketio_path='s', wait_timeout='w') - mock_client().event.call_count == 3 - mock_client().on.assert_called_once_with('*', namespace='n') - assert client.namespace == 'n' - assert not client.input_event.is_set() + SimpleClient.client_class = original_client_class def test_connect_twice(self): client = SimpleClient(123, a='b') From 82462f011c70b749270362bd3c352d7e3e155cfd Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Thu, 27 Feb 2025 12:35:57 +0000 Subject: [PATCH 499/571] Update bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5a45f90c..d73d5053 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -23,7 +23,7 @@ Steps to reproduce the behavior: A clear and concise description of what you expected to happen. **Logs** -Please provide relevant logs from the server and the client. On the Python server and client, add the `logger=True` and `engineio_logger=True` arguments to your `Server()` or `Client()` objects to get logs dumped on your terminal. If you are using the JavaScript client, see [here](https://socket.io/docs/logging-and-debugging/) for how to enable logs. +Please provide relevant logs from the server and the client. On the Python server and client, add the `logger=True` and `engineio_logger=True` arguments to your `Server()` or `Client()` objects to get logs dumped on your terminal. If you are using the JavaScript client, see [here](https://socket.io/docs/v4/logging-and-debugging/) for how to enable logs. **Additional context** Add any other context about the problem here. From 781dc9a0305f6a795a0467ebc795f4b765084a0d Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Thu, 27 Feb 2025 12:36:30 +0000 Subject: [PATCH 500/571] Update feature_request.md --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 32257912..968c279f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -17,7 +17,7 @@ A clear and concise description of what you want to happen. A clear and concise description of any alternative solutions or features you've considered. **Logs** -Please provide relevant logs from the server and the client. On the Python server and client, add the `logger=True` and `engineio_logger=True` arguments to your `Server()` or `Client()` objects to get logs dumped on your terminal. If you are using the JavaScript client, see [here](https://socket.io/docs/logging-and-debugging/) for how to enable logs. +Please provide relevant logs from the server and the client. On the Python server and client, add the `logger=True` and `engineio_logger=True` arguments to your `Server()` or `Client()` objects to get logs dumped on your terminal. If you are using the JavaScript client, see [here](https://socket.io/docs/v4/logging-and-debugging/) for how to enable logs. **Additional context** Add any other context or screenshots about the feature request here. From 1b4194360307f45166c5be96ca37e8edf213d91c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Mar 2025 23:12:50 +0000 Subject: [PATCH 501/571] Bump django in /examples/server/wsgi/django_socketio (#1439) #nolog Bumps [django](https://github.com/django/django) from 4.2.18 to 4.2.20. - [Commits](https://github.com/django/django/compare/4.2.18...4.2.20) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/django_socketio/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/django_socketio/requirements.txt b/examples/server/wsgi/django_socketio/requirements.txt index 975beff1..f3edf414 100644 --- a/examples/server/wsgi/django_socketio/requirements.txt +++ b/examples/server/wsgi/django_socketio/requirements.txt @@ -1,6 +1,6 @@ asgiref==3.6.0 bidict==0.22.1 -Django==4.2.18 +Django==4.2.20 gunicorn==22.0.0 h11==0.14.0 python-engineio From 4adebf6ab822083013fc5b4e43d2d2bdb2c5048b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 09:57:09 +0000 Subject: [PATCH 502/571] Bump jinja2 from 3.1.5 to 3.1.6 in /examples/server/wsgi (#1440) #nolog Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.5 to 3.1.6. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/3.1.5...3.1.6) --- updated-dependencies: - dependency-name: jinja2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/requirements.txt b/examples/server/wsgi/requirements.txt index dd465d06..3026d7f9 100644 --- a/examples/server/wsgi/requirements.txt +++ b/examples/server/wsgi/requirements.txt @@ -5,7 +5,7 @@ eventlet==0.35.2 Flask==1.0.2 greenlet==0.4.12 itsdangerous==1.1.0 -Jinja2==3.1.5 +Jinja2==3.1.6 MarkupSafe==1.1.0 packaging==16.8 pyparsing==2.1.10 From 288ebb189d799a05bbc5979a834433034ea2939f Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 9 Mar 2025 14:45:32 +0000 Subject: [PATCH 503/571] Eliminate race conditions on disconnect (Fixes #1441) --- src/socketio/async_client.py | 3 ++- src/socketio/client.py | 2 +- tests/async/test_client.py | 4 ++-- tests/common/test_client.py | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/socketio/async_client.py b/src/socketio/async_client.py index 463073e7..c7f5e86e 100644 --- a/src/socketio/async_client.py +++ b/src/socketio/async_client.py @@ -191,6 +191,7 @@ async def wait(self): if not self._reconnect_task: if self.eio.state == 'connected': # pragma: no cover # connected while sleeping above + print('oops') continue break await self._reconnect_task @@ -324,7 +325,7 @@ async def disconnect(self): for n in self.namespaces: await self._send_packet(self.packet_class(packet.DISCONNECT, namespace=n)) - await self.eio.disconnect(abort=True) + await self.eio.disconnect() async def shutdown(self): """Stop the client. diff --git a/src/socketio/client.py b/src/socketio/client.py index ade2dd6f..746d2c4c 100644 --- a/src/socketio/client.py +++ b/src/socketio/client.py @@ -306,7 +306,7 @@ def disconnect(self): for n in self.namespaces: self._send_packet(self.packet_class( packet.DISCONNECT, namespace=n)) - self.eio.disconnect(abort=True) + self.eio.disconnect() def shutdown(self): """Stop the client. diff --git a/tests/async/test_client.py b/tests/async/test_client.py index 3eec1a88..d7e0f9e7 100644 --- a/tests/async/test_client.py +++ b/tests/async/test_client.py @@ -475,7 +475,7 @@ async def test_disconnect(self): c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) - c.eio.disconnect.assert_awaited_once_with(abort=True) + c.eio.disconnect.assert_awaited_once_with() async def test_disconnect_namespaces(self): c = async_client.AsyncClient() @@ -993,7 +993,7 @@ async def test_shutdown_disconnect(self): c._send_packet.await_args_list[0][0][0].encode() == expected_packet.encode() ) - c.eio.disconnect.assert_awaited_once_with(abort=True) + c.eio.disconnect.assert_awaited_once_with() async def test_shutdown_disconnect_namespaces(self): c = async_client.AsyncClient() diff --git a/tests/common/test_client.py b/tests/common/test_client.py index ac930c79..7ee2bacf 100644 --- a/tests/common/test_client.py +++ b/tests/common/test_client.py @@ -633,7 +633,7 @@ def test_disconnect(self): c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) - c.eio.disconnect.assert_called_once_with(abort=True) + c.eio.disconnect.assert_called_once_with() def test_disconnect_namespaces(self): c = client.Client() @@ -1138,7 +1138,7 @@ def test_shutdown_disconnect(self): c._send_packet.call_args_list[0][0][0].encode() == expected_packet.encode() ) - c.eio.disconnect.assert_called_once_with(abort=True) + c.eio.disconnect.assert_called_once_with() def test_shutdown_disconnect_namespaces(self): c = client.Client() From fc0c1e2fee32a4fce97f03ce7451a320a91d1d82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Mar 2025 12:41:30 +0000 Subject: [PATCH 504/571] Bump gunicorn in /examples/server/wsgi/django_socketio (#1445) #nolog Bumps [gunicorn](https://github.com/benoitc/gunicorn) from 22.0.0 to 23.0.0. - [Release notes](https://github.com/benoitc/gunicorn/releases) - [Commits](https://github.com/benoitc/gunicorn/compare/22.0.0...23.0.0) --- updated-dependencies: - dependency-name: gunicorn dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/django_socketio/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/django_socketio/requirements.txt b/examples/server/wsgi/django_socketio/requirements.txt index f3edf414..8f6c19dd 100644 --- a/examples/server/wsgi/django_socketio/requirements.txt +++ b/examples/server/wsgi/django_socketio/requirements.txt @@ -1,7 +1,7 @@ asgiref==3.6.0 bidict==0.22.1 Django==4.2.20 -gunicorn==22.0.0 +gunicorn==23.0.0 h11==0.14.0 python-engineio python-socketio From 537630b983245cc137f609c3e6247d6d68ebdea5 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 5 Apr 2025 23:29:13 +0100 Subject: [PATCH 505/571] Remove incorrect reference to an "asyncio" installation extra (Fixes #1449) --- docs/server.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/server.rst b/docs/server.rst index eb91c292..9432a172 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -19,12 +19,6 @@ command:: pip install python-socketio -If you plan to build an asynchronous web server based on the ``asyncio`` -package, then you can install this package and some additional dependencies -that are needed with:: - - pip install "python-socketio[asyncio]" - Creating a Server Instance -------------------------- From 6a52e8b50274a7524fadcd2633eb819811a63734 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 6 Apr 2025 16:15:19 +0100 Subject: [PATCH 506/571] Add support for Redis Sentinel (#1448) * Add support for Redis Sentinel * more unit tests --- src/socketio/async_redis_manager.py | 22 ++++++++++--- src/socketio/redis_manager.py | 48 ++++++++++++++++++++++++++--- tests/common/test_redis_manager.py | 38 +++++++++++++++++++++++ 3 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 tests/common/test_redis_manager.py diff --git a/src/socketio/async_redis_manager.py b/src/socketio/async_redis_manager.py index e039c6e9..cc82f4a5 100644 --- a/src/socketio/async_redis_manager.py +++ b/src/socketio/async_redis_manager.py @@ -13,6 +13,7 @@ RedisError = None from .async_pubsub_manager import AsyncPubSubManager +from .redis_manager import parse_redis_sentinel_url class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover @@ -29,15 +30,18 @@ class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover client_manager=socketio.AsyncRedisManager(url)) :param url: The connection URL for the Redis server. For a default Redis - store running on the same host, use ``redis://``. To use an - SSL connection, use ``rediss://``. + store running on the same host, use ``redis://``. To use a + TLS connection, use ``rediss://``. To use Redis Sentinel, use + ``redis+sentinel://`` with a comma-separated list of hosts + and the service name after the db in the URL path. Example: + ``redis+sentinel://user:pw@host1:1234,host2:2345/0/myredis``. :param channel: The channel name on which the server sends and receives notifications. Must be the same in all the servers. :param write_only: If set to ``True``, only initialize to emit events. The default of ``False`` initializes the class for emitting and receiving. :param redis_options: additional keyword arguments to be passed to - ``aioredis.from_url()``. + ``Redis.from_url()`` or ``Sentinel()``. """ name = 'aioredis' @@ -54,8 +58,16 @@ def __init__(self, url='redis://localhost:6379/0', channel='socketio', super().__init__(channel=channel, write_only=write_only, logger=logger) def _redis_connect(self): - self.redis = aioredis.Redis.from_url(self.redis_url, - **self.redis_options) + if not self.redis_url.startswith('redis+sentinel://'): + self.redis = aioredis.Redis.from_url(self.redis_url, + **self.redis_options) + else: + sentinels, service_name, connection_kwargs = \ + parse_redis_sentinel_url(self.redis_url) + kwargs = self.redis_options + kwargs.update(connection_kwargs) + sentinel = aioredis.sentinel.Sentinel(sentinels, **kwargs) + self.redis = sentinel.master_for(service_name or self.channel) self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True) async def _publish(self, data): diff --git a/src/socketio/redis_manager.py b/src/socketio/redis_manager.py index a16fb2c7..df98618c 100644 --- a/src/socketio/redis_manager.py +++ b/src/socketio/redis_manager.py @@ -1,6 +1,7 @@ import logging import pickle import time +from urllib.parse import urlparse try: import redis @@ -12,6 +13,32 @@ logger = logging.getLogger('socketio') +def parse_redis_sentinel_url(url): + """Parse a Redis Sentinel URL with the format: + redis+sentinel://[:password]@host1:port1,host2:port2,.../db/service_name + """ + parsed_url = urlparse(url) + if parsed_url.scheme != 'redis+sentinel': + raise ValueError('Invalid Redis Sentinel URL') + sentinels = [] + for host_port in parsed_url.netloc.split('@')[-1].split(','): + host, port = host_port.rsplit(':', 1) + sentinels.append((host, int(port))) + kwargs = {} + if parsed_url.username: + kwargs['username'] = parsed_url.username + if parsed_url.password: + kwargs['password'] = parsed_url.password + service_name = None + if parsed_url.path: + parts = parsed_url.path.split('/') + if len(parts) >= 2 and parts[1] != '': + kwargs['db'] = int(parts[1]) + if len(parts) >= 3 and parts[2] != '': + service_name = parts[2] + return sentinels, service_name, kwargs + + class RedisManager(PubSubManager): # pragma: no cover """Redis based client manager. @@ -27,15 +54,18 @@ class RedisManager(PubSubManager): # pragma: no cover server = socketio.Server(client_manager=socketio.RedisManager(url)) :param url: The connection URL for the Redis server. For a default Redis - store running on the same host, use ``redis://``. To use an - SSL connection, use ``rediss://``. + store running on the same host, use ``redis://``. To use a + TLS connection, use ``rediss://``. To use Redis Sentinel, use + ``redis+sentinel://`` with a comma-separated list of hosts + and the service name after the db in the URL path. Example: + ``redis+sentinel://user:pw@host1:1234,host2:2345/0/myredis``. :param channel: The channel name on which the server sends and receives notifications. Must be the same in all the servers. :param write_only: If set to ``True``, only initialize to emit events. The default of ``False`` initializes the class for emitting and receiving. :param redis_options: additional keyword arguments to be passed to - ``Redis.from_url()``. + ``Redis.from_url()`` or ``Sentinel()``. """ name = 'redis' @@ -66,8 +96,16 @@ def initialize(self): 'with ' + self.server.async_mode) def _redis_connect(self): - self.redis = redis.Redis.from_url(self.redis_url, - **self.redis_options) + if not self.redis_url.startswith('redis+sentinel://'): + self.redis = redis.Redis.from_url(self.redis_url, + **self.redis_options) + else: + sentinels, service_name, connection_kwargs = \ + parse_redis_sentinel_url(self.redis_url) + kwargs = self.redis_options + kwargs.update(connection_kwargs) + sentinel = redis.sentinel.Sentinel(sentinels, **kwargs) + self.redis = sentinel.master_for(service_name or self.channel) self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True) def _publish(self, data): diff --git a/tests/common/test_redis_manager.py b/tests/common/test_redis_manager.py new file mode 100644 index 00000000..3e5ee1ef --- /dev/null +++ b/tests/common/test_redis_manager.py @@ -0,0 +1,38 @@ +import pytest + +from socketio.redis_manager import parse_redis_sentinel_url + + +class TestPubSubManager: + def test_sentinel_url_parser(self): + with pytest.raises(ValueError): + parse_redis_sentinel_url('redis://localhost:6379/0') + + assert parse_redis_sentinel_url( + 'redis+sentinel://localhost:6379' + ) == ( + [('localhost', 6379)], + None, + {} + ) + assert parse_redis_sentinel_url( + 'redis+sentinel://192.168.0.1:6379,192.168.0.2:6379/' + ) == ( + [('192.168.0.1', 6379), ('192.168.0.2', 6379)], + None, + {} + ) + assert parse_redis_sentinel_url( + 'redis+sentinel://h1:6379,h2:6379/0' + ) == ( + [('h1', 6379), ('h2', 6379)], + None, + {'db': 0} + ) + assert parse_redis_sentinel_url( + 'redis+sentinel://user:password@h1:6379,h2:6379,h1:6380/0/myredis' + ) == ( + [('h1', 6379), ('h2', 6379), ('h1', 6380)], + 'myredis', + {'username': 'user', 'password': 'password', 'db': 0} + ) From 5c93c59648358862514f317838f61498a101ba54 Mon Sep 17 00:00:00 2001 From: Tim Van Baak <40180944+tvanbaak@users.noreply.github.com> Date: Fri, 11 Apr 2025 08:17:23 -0700 Subject: [PATCH 507/571] Preserve exception context in Client.connect (#1450) --- src/socketio/async_client.py | 2 +- src/socketio/client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/socketio/async_client.py b/src/socketio/async_client.py index c7f5e86e..0fa35dc6 100644 --- a/src/socketio/async_client.py +++ b/src/socketio/async_client.py @@ -158,7 +158,7 @@ async def connect(self, url, headers={}, auth=None, transports=None, await self._handle_reconnect() if self.eio.state == 'connected': return - raise exceptions.ConnectionError(exc.args[0]) from None + raise exceptions.ConnectionError(exc.args[0]) from exc if wait: try: diff --git a/src/socketio/client.py b/src/socketio/client.py index 746d2c4c..84b1643d 100644 --- a/src/socketio/client.py +++ b/src/socketio/client.py @@ -156,7 +156,7 @@ def connect(self, url, headers={}, auth=None, transports=None, self._handle_reconnect() if self.eio.state == 'connected': return - raise exceptions.ConnectionError(exc.args[0]) from None + raise exceptions.ConnectionError(exc.args[0]) from exc if wait: while self._connect_event.wait(timeout=wait_timeout): From 8ea5e814c166fdf7064ebea15defb8589d9e7260 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 12 Apr 2025 16:36:56 +0100 Subject: [PATCH 508/571] linter fixes #nolog --- examples/client/async/latency_client.py | 1 - examples/client/sync/latency_client.py | 1 - 2 files changed, 2 deletions(-) diff --git a/examples/client/async/latency_client.py b/examples/client/async/latency_client.py index 8f39549b..57be604d 100644 --- a/examples/client/async/latency_client.py +++ b/examples/client/async/latency_client.py @@ -21,7 +21,6 @@ async def connect(): @sio.event async def pong_from_server(): - global start_timer latency = time.time() - start_timer print(f'latency is {latency * 1000:.2f} ms') await sio.sleep(1) diff --git a/examples/client/sync/latency_client.py b/examples/client/sync/latency_client.py index 240d214c..94dcec9d 100644 --- a/examples/client/sync/latency_client.py +++ b/examples/client/sync/latency_client.py @@ -19,7 +19,6 @@ def connect(): @sio.event def pong_from_server(): - global start_timer latency = time.time() - start_timer print(f'latency is {latency * 1000:.2f} ms') sio.sleep(1) From 80e77170eaf73c3b14e2a899bc8627c0f119da12 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 12 Apr 2025 16:40:43 +0100 Subject: [PATCH 509/571] Release 5.13.0 --- CHANGES.md | 8 ++++++++ pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 53afe357..fd061624 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,13 @@ # python-socketio change log +**Release 5.13.0** - 2025-04-12 + +- Eliminate race conditions on disconnect [#1441](https://github.com/miguelgrinberg/python-socketio/issues/1441) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/288ebb189d799a05bbc5979a834433034ea2939f)) +- Preserve exception context in `Client.connect` and `AsyncClient.connect` [#1450](https://github.com/miguelgrinberg/python-socketio/issues/1450) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5c93c59648358862514f317838f61498a101ba54)) (thanks **Tim Van Baak**!) +- Allow custom client subclasses to be used in SimpleClient and AsyncSimpleClient [#1432](https://github.com/miguelgrinberg/python-socketio/issues/1432) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/7605630bb236b4baf98574ca2a8f0cdba2696ef4)) +- Add support for Redis Sentinel URLs in `RedisManager` and `AsyncRedisManager` [#1448](https://github.com/miguelgrinberg/python-socketio/issues/1448) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6a52e8b50274a7524fadcd2633eb819811a63734)) +- Remove incorrect reference to an `asyncio` installation extra in documentation [#1449](https://github.com/miguelgrinberg/python-socketio/issues/1449) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/537630b983245cc137f609c3e6247d6d68ebdea5)) + **Release 5.12.1** - 2024-12-29 - Fix admin instrumentation support of disconnect reasons [#1423](https://github.com/miguelgrinberg/python-socketio/issues/1423) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b75fd31625cfea0d8c67d776070e4f8de99c1e45)) diff --git a/pyproject.toml b/pyproject.toml index c3c629d9..f9c64b7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.12.2.dev0" +version = "5.13.0" authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, ] From d9ecbee93c29d86faf9b5dd8f88acea76f0b7b44 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 12 Apr 2025 16:47:01 +0100 Subject: [PATCH 510/571] Version 5.13.1.dev0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f9c64b7d..67165258 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.13.0" +version = "5.13.1.dev0" authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, ] From 259de30d436484f8a7add9a79dca34d04ddda100 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 13 Apr 2025 12:22:53 +0100 Subject: [PATCH 511/571] Remove debugging print --- src/socketio/async_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/socketio/async_client.py b/src/socketio/async_client.py index 0fa35dc6..0a0137cb 100644 --- a/src/socketio/async_client.py +++ b/src/socketio/async_client.py @@ -191,7 +191,6 @@ async def wait(self): if not self._reconnect_task: if self.eio.state == 'connected': # pragma: no cover # connected while sleeping above - print('oops') continue break await self._reconnect_task From 81c80cddde06dd9561687c74e5769e25613282d7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 18 Apr 2025 12:46:53 +0200 Subject: [PATCH 512/571] Add SPDX license identifier (#1453) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 67165258..e7b6f65c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "python-socketio" version = "5.13.1.dev0" +license = {text = "MIT"} authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, ] @@ -9,7 +10,6 @@ classifiers = [ "Environment :: Web Environment", "Intended Audience :: Developers", "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] requires-python = ">=3.8" From 059fd90761c831e9b4a5b447ba621a6d011b3e39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 19:04:01 +0100 Subject: [PATCH 513/571] Bump h11 from 0.11.0 to 0.16.0 in /examples/server/asgi (#1458) #nolog Bumps [h11](https://github.com/python-hyper/h11) from 0.11.0 to 0.16.0. - [Commits](https://github.com/python-hyper/h11/compare/v0.11.0...v0.16.0) --- updated-dependencies: - dependency-name: h11 dependency-version: 0.16.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/asgi/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/asgi/requirements.txt b/examples/server/asgi/requirements.txt index 6dc530bd..2faa1466 100644 --- a/examples/server/asgi/requirements.txt +++ b/examples/server/asgi/requirements.txt @@ -1,5 +1,5 @@ Click==7.1.2 -h11==0.11.0 +h11==0.16.0 httptools==0.1.1 python-engineio python_socketio From 96073f4022a805378df880dbe3007a7744b60649 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 19:12:44 +0100 Subject: [PATCH 514/571] Bump h11 from 0.14.0 to 0.16.0 in /examples/server/wsgi/django_socketio (#1459) #nolog Bumps [h11](https://github.com/python-hyper/h11) from 0.14.0 to 0.16.0. - [Commits](https://github.com/python-hyper/h11/compare/v0.14.0...v0.16.0) --- updated-dependencies: - dependency-name: h11 dependency-version: 0.16.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/django_socketio/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/django_socketio/requirements.txt b/examples/server/wsgi/django_socketio/requirements.txt index 8f6c19dd..3789a68a 100644 --- a/examples/server/wsgi/django_socketio/requirements.txt +++ b/examples/server/wsgi/django_socketio/requirements.txt @@ -2,7 +2,7 @@ asgiref==3.6.0 bidict==0.22.1 Django==4.2.20 gunicorn==23.0.0 -h11==0.14.0 +h11==0.16.0 python-engineio python-socketio simple-websocket From 86a7176a5a93365fbc314880b5989159564971b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 May 2025 19:06:32 +0100 Subject: [PATCH 515/571] Bump django in /examples/server/wsgi/django_socketio (#1461) #nolog Bumps [django](https://github.com/django/django) from 4.2.20 to 4.2.21. - [Commits](https://github.com/django/django/compare/4.2.20...4.2.21) --- updated-dependencies: - dependency-name: django dependency-version: 4.2.21 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/django_socketio/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/django_socketio/requirements.txt b/examples/server/wsgi/django_socketio/requirements.txt index 3789a68a..04f61d7e 100644 --- a/examples/server/wsgi/django_socketio/requirements.txt +++ b/examples/server/wsgi/django_socketio/requirements.txt @@ -1,6 +1,6 @@ asgiref==3.6.0 bidict==0.22.1 -Django==4.2.20 +Django==4.2.21 gunicorn==23.0.0 h11==0.16.0 python-engineio From 5e04003dad0140fb1c6acff328e4215e62fbc58a Mon Sep 17 00:00:00 2001 From: Func Date: Fri, 16 May 2025 16:57:34 +0800 Subject: [PATCH 516/571] Add missing `async` on session examples for the async server (#1465) --- src/socketio/async_server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/socketio/async_server.py b/src/socketio/async_server.py index f10fb8ae..fac0f2b0 100644 --- a/src/socketio/async_server.py +++ b/src/socketio/async_server.py @@ -373,15 +373,15 @@ def session(self, sid, namespace=None): context manager block are saved back to the session. Example usage:: @eio.on('connect') - def on_connect(sid, environ): + async def on_connect(sid, environ): username = authenticate_user(environ) if not username: return False - with eio.session(sid) as session: + async with eio.session(sid) as session: session['username'] = username @eio.on('message') - def on_message(sid, msg): + async def on_message(sid, msg): async with eio.session(sid) as session: print('received message from ', session['username']) """ From 473e05c206f3a69874e7e073be083a92fb812fa1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 24 May 2025 14:18:40 +0100 Subject: [PATCH 517/571] Bump tornado from 6.4.2 to 6.5.1 in /examples/server/tornado (#1467) #nolog Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.4.2 to 6.5.1. - [Changelog](https://github.com/tornadoweb/tornado/blob/master/docs/releases.rst) - [Commits](https://github.com/tornadoweb/tornado/compare/v6.4.2...v6.5.1) --- updated-dependencies: - dependency-name: tornado dependency-version: 6.5.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/tornado/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/tornado/requirements.txt b/examples/server/tornado/requirements.txt index 32346eff..4e2915c4 100644 --- a/examples/server/tornado/requirements.txt +++ b/examples/server/tornado/requirements.txt @@ -1,4 +1,4 @@ -tornado==6.4.2 +tornado==6.5.1 python-engineio python_socketio six==1.10.0 From d3a9b82d5816a2f13413af769c05193ecd6d2422 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Jun 2025 00:47:57 +0100 Subject: [PATCH 518/571] Bump django in /examples/server/wsgi/django_socketio (#1470) #nolog Bumps [django](https://github.com/django/django) from 4.2.21 to 4.2.22. - [Commits](https://github.com/django/django/compare/4.2.21...4.2.22) --- updated-dependencies: - dependency-name: django dependency-version: 4.2.22 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/django_socketio/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/django_socketio/requirements.txt b/examples/server/wsgi/django_socketio/requirements.txt index 04f61d7e..39eae80f 100644 --- a/examples/server/wsgi/django_socketio/requirements.txt +++ b/examples/server/wsgi/django_socketio/requirements.txt @@ -1,6 +1,6 @@ asgiref==3.6.0 bidict==0.22.1 -Django==4.2.21 +Django==4.2.22 gunicorn==23.0.0 h11==0.16.0 python-engineio From 3a002e69d9264095f9891343a7492dab9291e629 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 15:05:40 +0100 Subject: [PATCH 519/571] Bump aiohttp from 3.10.11 to 3.12.14 in /examples/server/aiohttp (#1474) #nolog Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.11 to 3.12.14. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.11...v3.12.14) --- updated-dependencies: - dependency-name: aiohttp dependency-version: 3.12.14 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/aiohttp/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/aiohttp/requirements.txt b/examples/server/aiohttp/requirements.txt index 2bda3cff..aec8bf98 100644 --- a/examples/server/aiohttp/requirements.txt +++ b/examples/server/aiohttp/requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.10.11 +aiohttp==3.12.14 async-timeout==1.1.0 chardet==2.3.0 multidict==2.1.4 From efd1247ed9ed61a61d6840dc2c8c92ab02031afb Mon Sep 17 00:00:00 2001 From: Eugnee <77396838+Eugnee@users.noreply.github.com> Date: Tue, 22 Jul 2025 20:44:15 +0300 Subject: [PATCH 520/571] channel was not properly initialized in several pubsub client managers (#1476) --- src/socketio/async_aiopika_manager.py | 2 +- src/socketio/async_redis_manager.py | 2 +- src/socketio/redis_manager.py | 2 +- src/socketio/zmq_manager.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/socketio/async_aiopika_manager.py b/src/socketio/async_aiopika_manager.py index b6f09b8b..003b67bc 100644 --- a/src/socketio/async_aiopika_manager.py +++ b/src/socketio/async_aiopika_manager.py @@ -43,12 +43,12 @@ def __init__(self, url='amqp://guest:guest@localhost:5672//', raise RuntimeError('aio_pika package is not installed ' '(Run "pip install aio_pika" in your ' 'virtualenv).') + super().__init__(channel=channel, write_only=write_only, logger=logger) self.url = url self._lock = asyncio.Lock() self.publisher_connection = None self.publisher_channel = None self.publisher_exchange = None - super().__init__(channel=channel, write_only=write_only, logger=logger) async def _connection(self): return await aio_pika.connect_robust(self.url) diff --git a/src/socketio/async_redis_manager.py b/src/socketio/async_redis_manager.py index cc82f4a5..92109a21 100644 --- a/src/socketio/async_redis_manager.py +++ b/src/socketio/async_redis_manager.py @@ -52,10 +52,10 @@ def __init__(self, url='redis://localhost:6379/0', channel='socketio', '(Run "pip install redis" in your virtualenv).') if not hasattr(aioredis.Redis, 'from_url'): raise RuntimeError('Version 2 of aioredis package is required.') + super().__init__(channel=channel, write_only=write_only, logger=logger) self.redis_url = url self.redis_options = redis_options or {} self._redis_connect() - super().__init__(channel=channel, write_only=write_only, logger=logger) def _redis_connect(self): if not self.redis_url.startswith('redis+sentinel://'): diff --git a/src/socketio/redis_manager.py b/src/socketio/redis_manager.py index df98618c..c4407dfe 100644 --- a/src/socketio/redis_manager.py +++ b/src/socketio/redis_manager.py @@ -75,10 +75,10 @@ def __init__(self, url='redis://localhost:6379/0', channel='socketio', raise RuntimeError('Redis package is not installed ' '(Run "pip install redis" in your ' 'virtualenv).') + super().__init__(channel=channel, write_only=write_only, logger=logger) self.redis_url = url self.redis_options = redis_options or {} self._redis_connect() - super().__init__(channel=channel, write_only=write_only, logger=logger) def initialize(self): super().initialize() diff --git a/src/socketio/zmq_manager.py b/src/socketio/zmq_manager.py index 468dc268..aa5a49a2 100644 --- a/src/socketio/zmq_manager.py +++ b/src/socketio/zmq_manager.py @@ -57,6 +57,7 @@ def __init__(self, url='zmq+tcp://localhost:5555+5556', if not (url.startswith('zmq+tcp://') and r.search(url)): raise RuntimeError('unexpected connection string: ' + url) + super().__init__(channel=channel, write_only=write_only, logger=logger) url = url.replace('zmq+', '') (sink_url, sub_port) = url.split('+') sink_port = sink_url.split(':')[-1] @@ -72,7 +73,6 @@ def __init__(self, url='zmq+tcp://localhost:5555+5556', self.sink = sink self.sub = sub self.channel = channel - super().__init__(channel=channel, write_only=write_only, logger=logger) def _publish(self, data): pickled_data = pickle.dumps( From b01b197df1ea5914fdaa2e12758e916ebd60162a Mon Sep 17 00:00:00 2001 From: Eugnee <77396838+Eugnee@users.noreply.github.com> Date: Sun, 24 Aug 2025 13:44:50 +0300 Subject: [PATCH 521/571] redis manager, verbose error logging (#1479) --- src/socketio/async_redis_manager.py | 20 +++++++++++++------- src/socketio/redis_manager.py | 17 ++++++++++++----- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/socketio/async_redis_manager.py b/src/socketio/async_redis_manager.py index 92109a21..41ce2cea 100644 --- a/src/socketio/async_redis_manager.py +++ b/src/socketio/async_redis_manager.py @@ -78,14 +78,19 @@ async def _publish(self, data): self._redis_connect() return await self.redis.publish( self.channel, pickle.dumps(data)) - except RedisError: + except RedisError as exc: if retry: - self._get_logger().error('Cannot publish to redis... ' - 'retrying') + self._get_logger().error( + 'Cannot publish to redis... ' + 'retrying', + extra={"redis_exception": str(exc)}) retry = False else: - self._get_logger().error('Cannot publish to redis... ' - 'giving up') + self._get_logger().error( + 'Cannot publish to redis... ' + 'giving up', + extra={"redis_exception": str(exc)}) + break async def _redis_listen_with_retries(self): @@ -99,10 +104,11 @@ async def _redis_listen_with_retries(self): retry_sleep = 1 async for message in self.pubsub.listen(): yield message - except RedisError: + except RedisError as exc: self._get_logger().error('Cannot receive from redis... ' 'retrying in ' - '{} secs'.format(retry_sleep)) + '{} secs'.format(retry_sleep), + extra={"redis_exception": str(exc)}) connect = True await asyncio.sleep(retry_sleep) retry_sleep *= 2 diff --git a/src/socketio/redis_manager.py b/src/socketio/redis_manager.py index c4407dfe..73758fce 100644 --- a/src/socketio/redis_manager.py +++ b/src/socketio/redis_manager.py @@ -115,12 +115,18 @@ def _publish(self, data): if not retry: self._redis_connect() return self.redis.publish(self.channel, pickle.dumps(data)) - except redis.exceptions.RedisError: + except redis.exceptions.RedisError as exc: if retry: - logger.error('Cannot publish to redis... retrying') + logger.error( + 'Cannot publish to redis... retrying', + extra={"redis_exception": str(exc)} + ) retry = False else: - logger.error('Cannot publish to redis... giving up') + logger.error( + 'Cannot publish to redis... giving up', + extra={"redis_exception": str(exc)} + ) break def _redis_listen_with_retries(self): @@ -133,9 +139,10 @@ def _redis_listen_with_retries(self): self.pubsub.subscribe(self.channel) retry_sleep = 1 yield from self.pubsub.listen() - except redis.exceptions.RedisError: + except redis.exceptions.RedisError as exc: logger.error('Cannot receive from redis... ' - 'retrying in {} secs'.format(retry_sleep)) + 'retrying in {} secs'.format(retry_sleep), + extra={"redis_exception": str(exc)}) connect = True time.sleep(retry_sleep) retry_sleep *= 2 From 3625fe821debf33e5430bff6375ec85056a5d95f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 30 Aug 2025 10:51:56 +0100 Subject: [PATCH 522/571] Bump eventlet from 0.35.2 to 0.40.3 in /examples/server/wsgi (#1491) #nolog Bumps [eventlet](https://github.com/eventlet/eventlet) from 0.35.2 to 0.40.3. - [Changelog](https://github.com/eventlet/eventlet/blob/master/NEWS) - [Commits](https://github.com/eventlet/eventlet/compare/v0.35.2...0.40.3) --- updated-dependencies: - dependency-name: eventlet dependency-version: 0.40.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/requirements.txt b/examples/server/wsgi/requirements.txt index 3026d7f9..f4b71137 100644 --- a/examples/server/wsgi/requirements.txt +++ b/examples/server/wsgi/requirements.txt @@ -1,7 +1,7 @@ Click==7.0 enum-compat==0.0.2 enum34==1.1.6 -eventlet==0.35.2 +eventlet==0.40.3 Flask==1.0.2 greenlet==0.4.12 itsdangerous==1.1.0 From b3da354ed9eb46c0fb847c628b379ccae475a970 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 24 Sep 2025 20:28:55 +0100 Subject: [PATCH 523/571] Add message queue deployment recommendations --- docs/server.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/server.rst b/docs/server.rst index 9432a172..508bd46f 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -1089,6 +1089,25 @@ The RabbitMQ queue is configured through the mgr = socketio.AsyncAioPikaManager('amqp://') sio = socketio.AsyncServer(client_manager=mgr) +Deploying the Message Queue for Production +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For a production deployment there are a few recommendations to keep your +application secure. + +First of all, the message queue should never be listening on a public network +interface, to ensure that external clients never connect to it. The use of a +private network (VPC), where the communication between servers can happen +privately is highly recommended. + +In addition, all message queues support authentication and encryption. +Authentication ensures that only the Socket.IO servers and related processes +have access, while encryption prevents data to be collected by a third-party +listening on the network. + +Access credentials can be included in the connection URLs that are passed to the +client managers. + Horizontal Scaling ~~~~~~~~~~~~~~~~~~ From 5dc2aea077469ad318e47b28a84845c5efb6bdcf Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 28 Sep 2025 10:16:15 +0100 Subject: [PATCH 524/571] keep track of which namespaces failed to connect (#1496) --- src/socketio/async_client.py | 8 ++++++-- src/socketio/base_client.py | 1 + src/socketio/client.py | 10 +++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/socketio/async_client.py b/src/socketio/async_client.py index 0a0137cb..d84988a6 100644 --- a/src/socketio/async_client.py +++ b/src/socketio/async_client.py @@ -139,6 +139,7 @@ async def connect(self, url, headers={}, auth=None, transports=None, namespaces = [namespaces] self.connection_namespaces = namespaces self.namespaces = {} + self.failed_namespaces = [] if self._connect_event is None: self._connect_event = self.eio.create_event() else: @@ -166,14 +167,16 @@ async def connect(self, url, headers={}, auth=None, transports=None, await asyncio.wait_for(self._connect_event.wait(), wait_timeout) self._connect_event.clear() - if set(self.namespaces) == set(self.connection_namespaces): + if len(self.namespaces) + len(self.failed_namespaces) == \ + len(self.connection_namespaces): break except asyncio.TimeoutError: pass if set(self.namespaces) != set(self.connection_namespaces): await self.disconnect() raise exceptions.ConnectionError( - 'One or more namespaces failed to connect') + 'One or more namespaces failed to connect' + ', '.join(self.failed_namespaces)) self.connected = True @@ -448,6 +451,7 @@ async def _handle_error(self, namespace, data): elif not isinstance(data, (tuple, list)): data = (data,) await self._trigger_event('connect_error', namespace, *data) + self.failed_namespaces.append(namespace) self._connect_event.set() if namespace in self.namespaces: del self.namespaces[namespace] diff --git a/src/socketio/base_client.py b/src/socketio/base_client.py index 7bf44207..0232dca7 100644 --- a/src/socketio/base_client.py +++ b/src/socketio/base_client.py @@ -93,6 +93,7 @@ def __init__(self, reconnection=True, reconnection_attempts=0, self.connected = False #: Indicates if the client is connected or not. self.namespaces = {} #: set of connected namespaces. + self.failed_namespaces = [] self.handlers = {} self.namespace_handlers = {} self.callbacks = {} diff --git a/src/socketio/client.py b/src/socketio/client.py index 84b1643d..1935821d 100644 --- a/src/socketio/client.py +++ b/src/socketio/client.py @@ -137,6 +137,7 @@ def connect(self, url, headers={}, auth=None, transports=None, namespaces = [namespaces] self.connection_namespaces = namespaces self.namespaces = {} + self.failed_namespaces = [] if self._connect_event is None: self._connect_event = self.eio.create_event() else: @@ -161,12 +162,14 @@ def connect(self, url, headers={}, auth=None, transports=None, if wait: while self._connect_event.wait(timeout=wait_timeout): self._connect_event.clear() - if set(self.namespaces) == set(self.connection_namespaces): + if len(self.namespaces) + len(self.failed_namespaces) == \ + len(self.connection_namespaces): break if set(self.namespaces) != set(self.connection_namespaces): self.disconnect() raise exceptions.ConnectionError( - 'One or more namespaces failed to connect') + 'One or more namespaces failed to connect: ' + ', '.join(self.failed_namespaces)) self.connected = True @@ -425,6 +428,7 @@ def _handle_error(self, namespace, data): elif not isinstance(data, (tuple, list)): data = (data,) self._trigger_event('connect_error', namespace, *data) + self.failed_namespaces.append(namespace) self._connect_event.set() if namespace in self.namespaces: del self.namespaces[namespace] @@ -439,7 +443,7 @@ def _trigger_event(self, event, namespace, *args): if handler: try: return handler(*args) - except TypeError: + except TypeError: # pragma: no cover # the legacy disconnect event does not take a reason argument if event == 'disconnect': return handler(*args[:-1]) From 36a89226a2fb18f876dcba48125a8c51904586ec Mon Sep 17 00:00:00 2001 From: phi-friday Date: Sun, 28 Sep 2025 18:17:55 +0900 Subject: [PATCH 525/571] Add support for valkey in the Redis client managers (#1488) --- src/socketio/async_redis_manager.py | 54 +++++++++++++++++++++------ src/socketio/redis_manager.py | 58 ++++++++++++++++++++++------- tests/common/test_redis_manager.py | 14 ++++--- 3 files changed, 96 insertions(+), 30 deletions(-) diff --git a/src/socketio/async_redis_manager.py b/src/socketio/async_redis_manager.py index 41ce2cea..b37e9059 100644 --- a/src/socketio/async_redis_manager.py +++ b/src/socketio/async_redis_manager.py @@ -1,5 +1,6 @@ import asyncio import pickle +from urllib.parse import urlparse try: # pragma: no cover from redis import asyncio as aioredis @@ -12,6 +13,13 @@ aioredis = None RedisError = None +try: # pragma: no cover + from valkey import asyncio as valkey + from valkey.exceptions import ValkeyError +except ImportError: # pragma: no cover + valkey = None + ValkeyError = None + from .async_pubsub_manager import AsyncPubSubManager from .redis_manager import parse_redis_sentinel_url @@ -47,38 +55,61 @@ class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover def __init__(self, url='redis://localhost:6379/0', channel='socketio', write_only=False, logger=None, redis_options=None): - if aioredis is None: + if aioredis is None and valkey is None: raise RuntimeError('Redis package is not installed ' - '(Run "pip install redis" in your virtualenv).') - if not hasattr(aioredis.Redis, 'from_url'): + '(Run "pip install redis" or ' + '"pip install valkey" ' + 'in your virtualenv).') + if aioredis and not hasattr(aioredis.Redis, 'from_url'): raise RuntimeError('Version 2 of aioredis package is required.') super().__init__(channel=channel, write_only=write_only, logger=logger) self.redis_url = url self.redis_options = redis_options or {} self._redis_connect() + def _get_redis_module_and_error(self): + parsed_url = urlparse(self.redis_url) + schema = parsed_url.scheme.split('+', 1)[0].lower() + if schema == 'redis': + if aioredis is None or RedisError is None: + raise RuntimeError('Redis package is not installed ' + '(Run "pip install redis" ' + 'in your virtualenv).') + return aioredis, RedisError + if schema == 'valkey': + if valkey is None or ValkeyError is None: + raise RuntimeError('Valkey package is not installed ' + '(Run "pip install valkey" ' + 'in your virtualenv).') + return valkey, ValkeyError + error_msg = f'Unsupported Redis URL schema: {schema}' + raise ValueError(error_msg) + def _redis_connect(self): - if not self.redis_url.startswith('redis+sentinel://'): - self.redis = aioredis.Redis.from_url(self.redis_url, - **self.redis_options) - else: + module, _ = self._get_redis_module_and_error() + parsed_url = urlparse(self.redis_url) + if parsed_url.scheme in {"redis+sentinel", "valkey+sentinel"}: sentinels, service_name, connection_kwargs = \ parse_redis_sentinel_url(self.redis_url) kwargs = self.redis_options kwargs.update(connection_kwargs) - sentinel = aioredis.sentinel.Sentinel(sentinels, **kwargs) + sentinel = module.sentinel.Sentinel(sentinels, **kwargs) self.redis = sentinel.master_for(service_name or self.channel) + else: + self.redis = module.Redis.from_url(self.redis_url, + **self.redis_options) self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True) async def _publish(self, data): retry = True + _, error = self._get_redis_module_and_error() while True: try: if not retry: self._redis_connect() return await self.redis.publish( self.channel, pickle.dumps(data)) - except RedisError as exc: + except error as exc: if retry: self._get_logger().error( 'Cannot publish to redis... ' @@ -96,6 +127,7 @@ async def _publish(self, data): async def _redis_listen_with_retries(self): retry_sleep = 1 connect = False + _, error = self._get_redis_module_and_error() while True: try: if connect: @@ -104,10 +136,10 @@ async def _redis_listen_with_retries(self): retry_sleep = 1 async for message in self.pubsub.listen(): yield message - except RedisError as exc: + except error as exc: self._get_logger().error('Cannot receive from redis... ' 'retrying in ' - '{} secs'.format(retry_sleep), + f'{retry_sleep} secs', extra={"redis_exception": str(exc)}) connect = True await asyncio.sleep(retry_sleep) diff --git a/src/socketio/redis_manager.py b/src/socketio/redis_manager.py index 73758fce..2e68c31c 100644 --- a/src/socketio/redis_manager.py +++ b/src/socketio/redis_manager.py @@ -3,10 +3,19 @@ import time from urllib.parse import urlparse -try: +try: # pragma: no cover import redis + from redis.exceptions import RedisError except ImportError: redis = None + RedisError = None + +try: # pragma: no cover + import valkey + from valkey.exceptions import ValkeyError +except ImportError: + valkey = None + ValkeyError = None from .pubsub_manager import PubSubManager @@ -18,7 +27,7 @@ def parse_redis_sentinel_url(url): redis+sentinel://[:password]@host1:port1,host2:port2,.../db/service_name """ parsed_url = urlparse(url) - if parsed_url.scheme != 'redis+sentinel': + if parsed_url.scheme not in {'redis+sentinel', 'valkey+sentinel'}: raise ValueError('Invalid Redis Sentinel URL') sentinels = [] for host_port in parsed_url.netloc.split('@')[-1].split(','): @@ -71,10 +80,11 @@ class RedisManager(PubSubManager): # pragma: no cover def __init__(self, url='redis://localhost:6379/0', channel='socketio', write_only=False, logger=None, redis_options=None): - if redis is None: + if redis is None and valkey is None: raise RuntimeError('Redis package is not installed ' - '(Run "pip install redis" in your ' - 'virtualenv).') + '(Run "pip install redis" ' + 'or "pip install valkey" ' + 'in your virtualenv).') super().__init__(channel=channel, write_only=write_only, logger=logger) self.redis_url = url self.redis_options = redis_options or {} @@ -95,27 +105,48 @@ def initialize(self): 'Redis requires a monkey patched socket library to work ' 'with ' + self.server.async_mode) + def _get_redis_module_and_error(self): + parsed_url = urlparse(self.redis_url) + schema = parsed_url.scheme.split('+', 1)[0].lower() + if schema == 'redis': + if redis is None or RedisError is None: + raise RuntimeError('Redis package is not installed ' + '(Run "pip install redis" ' + 'in your virtualenv).') + return redis, RedisError + if schema == 'valkey': + if valkey is None or ValkeyError is None: + raise RuntimeError('Valkey package is not installed ' + '(Run "pip install valkey" ' + 'in your virtualenv).') + return valkey, ValkeyError + error_msg = f'Unsupported Redis URL schema: {schema}' + raise ValueError(error_msg) + def _redis_connect(self): - if not self.redis_url.startswith('redis+sentinel://'): - self.redis = redis.Redis.from_url(self.redis_url, - **self.redis_options) - else: + module, _ = self._get_redis_module_and_error() + parsed_url = urlparse(self.redis_url) + if parsed_url.scheme in {"redis+sentinel", "valkey+sentinel"}: sentinels, service_name, connection_kwargs = \ parse_redis_sentinel_url(self.redis_url) kwargs = self.redis_options kwargs.update(connection_kwargs) - sentinel = redis.sentinel.Sentinel(sentinels, **kwargs) + sentinel = module.sentinel.Sentinel(sentinels, **kwargs) self.redis = sentinel.master_for(service_name or self.channel) + else: + self.redis = module.Redis.from_url(self.redis_url, + **self.redis_options) self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True) def _publish(self, data): retry = True + _, error = self._get_redis_module_and_error() while True: try: if not retry: self._redis_connect() return self.redis.publish(self.channel, pickle.dumps(data)) - except redis.exceptions.RedisError as exc: + except error as exc: if retry: logger.error( 'Cannot publish to redis... retrying', @@ -132,6 +163,7 @@ def _publish(self, data): def _redis_listen_with_retries(self): retry_sleep = 1 connect = False + _, error = self._get_redis_module_and_error() while True: try: if connect: @@ -139,9 +171,9 @@ def _redis_listen_with_retries(self): self.pubsub.subscribe(self.channel) retry_sleep = 1 yield from self.pubsub.listen() - except redis.exceptions.RedisError as exc: + except error as exc: logger.error('Cannot receive from redis... ' - 'retrying in {} secs'.format(retry_sleep), + f'retrying in {retry_sleep} secs', extra={"redis_exception": str(exc)}) connect = True time.sleep(retry_sleep) diff --git a/tests/common/test_redis_manager.py b/tests/common/test_redis_manager.py index 3e5ee1ef..48dfb4a9 100644 --- a/tests/common/test_redis_manager.py +++ b/tests/common/test_redis_manager.py @@ -4,33 +4,35 @@ class TestPubSubManager: - def test_sentinel_url_parser(self): + @pytest.mark.parametrize('rtype', ['redis', 'valkey']) + def test_sentinel_url_parser(self, rtype): with pytest.raises(ValueError): - parse_redis_sentinel_url('redis://localhost:6379/0') + parse_redis_sentinel_url(f'{rtype}://localhost:6379/0') assert parse_redis_sentinel_url( - 'redis+sentinel://localhost:6379' + f'{rtype}+sentinel://localhost:6379' ) == ( [('localhost', 6379)], None, {} ) assert parse_redis_sentinel_url( - 'redis+sentinel://192.168.0.1:6379,192.168.0.2:6379/' + f'{rtype}+sentinel://192.168.0.1:6379,192.168.0.2:6379/' ) == ( [('192.168.0.1', 6379), ('192.168.0.2', 6379)], None, {} ) assert parse_redis_sentinel_url( - 'redis+sentinel://h1:6379,h2:6379/0' + f'{rtype}+sentinel://h1:6379,h2:6379/0' ) == ( [('h1', 6379), ('h2', 6379)], None, {'db': 0} ) assert parse_redis_sentinel_url( - 'redis+sentinel://user:password@h1:6379,h2:6379,h1:6380/0/myredis' + f'{rtype}+sentinel://' + 'user:password@h1:6379,h2:6379,h1:6380/0/myredis' ) == ( [('h1', 6379), ('h2', 6379), ('h1', 6380)], 'myredis', From e59acf146550d5c07f530a4766dfdfffda21e20c Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 28 Sep 2025 22:06:51 +0100 Subject: [PATCH 526/571] Address failures of test suite on Mac (#1497) --- tests/async/test_admin.py | 48 +++++++++++++++++++------------------- tests/common/test_admin.py | 42 ++++++++++++++++----------------- tests/web_server.py | 2 +- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/tests/async/test_admin.py b/tests/async/test_admin.py index a1cf97c4..62806cbb 100644 --- a/tests/async/test_admin.py +++ b/tests/async/test_admin.py @@ -107,23 +107,23 @@ def test_missing_auth(self): @with_instrumented_server(auth=False) def test_admin_connect_with_no_auth(self): - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) @with_instrumented_server(auth={'foo': 'bar'}) def test_admin_connect_with_dict_auth(self): - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: with pytest.raises(ConnectionError): admin_client.connect( 'http://localhost:8900', namespace='/admin', auth={'foo': 'baz'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: with pytest.raises(ConnectionError): admin_client.connect( 'http://localhost:8900', namespace='/admin') @@ -131,52 +131,52 @@ def test_admin_connect_with_dict_auth(self): @with_instrumented_server(auth=[{'foo': 'bar'}, {'u': 'admin', 'p': 'secret'}]) def test_admin_connect_with_list_auth(self): - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'u': 'admin', 'p': 'secret'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'baz'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin') @with_instrumented_server(auth=_custom_auth) def test_admin_connect_with_function_auth(self): - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'baz'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin') @with_instrumented_server(auth=_async_custom_auth) def test_admin_connect_with_async_function_auth(self): - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'baz'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin') @with_instrumented_server() def test_admin_connect_only_admin(self): - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') sid = admin_client.sid events = self._expect({'config': 1, 'all_sockets': 1, @@ -201,10 +201,10 @@ def test_admin_connect_only_admin(self): @with_instrumented_server() def test_admin_connect_with_others(self): - with socketio.SimpleClient() as client1, \ - socketio.SimpleClient() as client2, \ - socketio.SimpleClient() as client3, \ - socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as client1, \ + socketio.SimpleClient(reconnection=False) as client2, \ + socketio.SimpleClient(reconnection=False) as client3, \ + socketio.SimpleClient(reconnection=False) as admin_client: client1.connect('http://localhost:8900') client1.emit('enter_room', 'room') sid1 = client1.sid @@ -251,7 +251,7 @@ def test_admin_connect_with_others(self): @with_instrumented_server(mode='production', read_only=True) def test_admin_connect_production(self): - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') events = self._expect({'config': 1, 'server_stats': 2}, admin_client) @@ -272,9 +272,9 @@ def test_admin_connect_production(self): @with_instrumented_server() def test_admin_features(self): - with socketio.SimpleClient() as client1, \ - socketio.SimpleClient() as client2, \ - socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as client1, \ + socketio.SimpleClient(reconnection=False) as client2, \ + socketio.SimpleClient(reconnection=False) as admin_client: client1.connect('http://localhost:8900') client2.connect('http://localhost:8900') admin_client.connect('http://localhost:8900', namespace='/admin') diff --git a/tests/common/test_admin.py b/tests/common/test_admin.py index e7667311..a3847898 100644 --- a/tests/common/test_admin.py +++ b/tests/common/test_admin.py @@ -97,23 +97,23 @@ def test_missing_auth(self): @with_instrumented_server(auth=False) def test_admin_connect_with_no_auth(self): - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) @with_instrumented_server(auth={'foo': 'bar'}) def test_admin_connect_with_dict_auth(self): - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: with pytest.raises(ConnectionError): admin_client.connect( 'http://localhost:8900', namespace='/admin', auth={'foo': 'baz'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: with pytest.raises(ConnectionError): admin_client.connect( 'http://localhost:8900', namespace='/admin') @@ -121,38 +121,38 @@ def test_admin_connect_with_dict_auth(self): @with_instrumented_server(auth=[{'foo': 'bar'}, {'u': 'admin', 'p': 'secret'}]) def test_admin_connect_with_list_auth(self): - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'u': 'admin', 'p': 'secret'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'baz'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin') @with_instrumented_server(auth=_custom_auth) def test_admin_connect_with_function_auth(self): - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'bar'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin', auth={'foo': 'baz'}) - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: with pytest.raises(ConnectionError): admin_client.connect('http://localhost:8900', namespace='/admin') @with_instrumented_server() def test_admin_connect_only_admin(self): - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') sid = admin_client.sid events = self._expect({'config': 1, 'all_sockets': 1, @@ -177,10 +177,10 @@ def test_admin_connect_only_admin(self): @with_instrumented_server() def test_admin_connect_with_others(self): - with socketio.SimpleClient() as client1, \ - socketio.SimpleClient() as client2, \ - socketio.SimpleClient() as client3, \ - socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as client1, \ + socketio.SimpleClient(reconnection=False) as client2, \ + socketio.SimpleClient(reconnection=False) as client3, \ + socketio.SimpleClient(reconnection=False) as admin_client: client1.connect('http://localhost:8900') client1.emit('enter_room', 'room') sid1 = client1.sid @@ -227,7 +227,7 @@ def test_admin_connect_with_others(self): @with_instrumented_server(mode='production', read_only=True) def test_admin_connect_production(self): - with socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as admin_client: admin_client.connect('http://localhost:8900', namespace='/admin') events = self._expect({'config': 1, 'server_stats': 2}, admin_client) @@ -248,9 +248,9 @@ def test_admin_connect_production(self): @with_instrumented_server() def test_admin_features(self): - with socketio.SimpleClient() as client1, \ - socketio.SimpleClient() as client2, \ - socketio.SimpleClient() as admin_client: + with socketio.SimpleClient(reconnection=False) as client1, \ + socketio.SimpleClient(reconnection=False) as client2, \ + socketio.SimpleClient(reconnection=False) as admin_client: client1.connect('http://localhost:8900') client2.connect('http://localhost:8900') admin_client.connect('http://localhost:8900', namespace='/admin') diff --git a/tests/web_server.py b/tests/web_server.py index cb24668e..113d475e 100644 --- a/tests/web_server.py +++ b/tests/web_server.py @@ -47,7 +47,7 @@ def get_environ(self): env['gunicorn.socket'] = self.connection return env - self.httpd = make_server('', port, self._app_wrapper, + self.httpd = make_server('localhost', port, self._app_wrapper, ThreadingWSGIServer, WebSocketRequestHandler) self.thread = threading.Thread(target=self.httpd.serve_forever) self.thread.start() From 23556fb3dcb37074020494df40fb4495d47e7efe Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Mon, 29 Sep 2025 07:32:12 +0100 Subject: [PATCH 527/571] Fixed transport property of the simple clients to be a string as documented (Fixes #1499) --- src/socketio/async_simple_client.py | 2 +- src/socketio/simple_client.py | 2 +- tests/async/test_simple_client.py | 2 +- tests/common/test_simple_client.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/socketio/async_simple_client.py b/src/socketio/async_simple_client.py index adac6ead..61425220 100644 --- a/src/socketio/async_simple_client.py +++ b/src/socketio/async_simple_client.py @@ -105,7 +105,7 @@ def transport(self): The transport is returned as a string and can be one of ``polling`` and ``websocket``. """ - return self.client.transport if self.client else '' + return self.client.transport() if self.client else '' async def emit(self, event, data=None): """Emit an event to the server. diff --git a/src/socketio/simple_client.py b/src/socketio/simple_client.py index 3f046b4b..9bc5390c 100644 --- a/src/socketio/simple_client.py +++ b/src/socketio/simple_client.py @@ -103,7 +103,7 @@ def transport(self): The transport is returned as a string and can be one of ``polling`` and ``websocket``. """ - return self.client.transport if self.client else '' + return self.client.transport() if self.client else '' def emit(self, event, data=None): """Emit an event to the server. diff --git a/tests/async/test_simple_client.py b/tests/async/test_simple_client.py index bfe2a90f..0f3e5045 100644 --- a/tests/async/test_simple_client.py +++ b/tests/async/test_simple_client.py @@ -72,7 +72,7 @@ async def test_connect_twice(self): async def test_properties(self): client = AsyncSimpleClient() - client.client = mock.MagicMock(transport='websocket') + client.client = mock.MagicMock(transport=lambda: 'websocket') client.client.get_sid.return_value = 'sid' client.connected_event.set() client.connected = True diff --git a/tests/common/test_simple_client.py b/tests/common/test_simple_client.py index b17afbcc..6adcb382 100644 --- a/tests/common/test_simple_client.py +++ b/tests/common/test_simple_client.py @@ -64,7 +64,7 @@ def test_connect_twice(self): def test_properties(self): client = SimpleClient() - client.client = mock.MagicMock(transport='websocket') + client.client = mock.MagicMock(transport=lambda: 'websocket') client.client.get_sid.return_value = 'sid' client.connected_event.set() client.connected = True From f61e0bec3750a83de619db9b779f9d93e449eade Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Mon, 29 Sep 2025 09:11:08 +0100 Subject: [PATCH 528/571] wait for client to end background tasks on disconnect (#1500) --- src/socketio/async_client.py | 2 +- src/socketio/client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/socketio/async_client.py b/src/socketio/async_client.py index d84988a6..53c669bb 100644 --- a/src/socketio/async_client.py +++ b/src/socketio/async_client.py @@ -407,7 +407,7 @@ async def _handle_disconnect(self, namespace): del self.namespaces[namespace] if not self.namespaces: self.connected = False - await self.eio.disconnect(abort=True) + await self.eio.disconnect() async def _handle_event(self, namespace, id, data): namespace = namespace or '/' diff --git a/src/socketio/client.py b/src/socketio/client.py index 1935821d..4fc36f4e 100644 --- a/src/socketio/client.py +++ b/src/socketio/client.py @@ -387,7 +387,7 @@ def _handle_disconnect(self, namespace): del self.namespaces[namespace] if not self.namespaces: self.connected = False - self.eio.disconnect(abort=True) + self.eio.disconnect() def _handle_event(self, namespace, id, data): namespace = namespace or '/' From a59c6f520059eb095ab4472e5192ce3e486875d9 Mon Sep 17 00:00:00 2001 From: James Thistlewood Date: Tue, 30 Sep 2025 20:13:56 +0100 Subject: [PATCH 529/571] Fix: SimpleClient.call does not raise TimeoutError on timeout (#1501) --- src/socketio/async_simple_client.py | 2 ++ src/socketio/simple_client.py | 2 ++ tests/async/test_simple_client.py | 11 +++++++++++ tests/common/test_simple_client.py | 10 ++++++++++ 4 files changed, 25 insertions(+) diff --git a/src/socketio/async_simple_client.py b/src/socketio/async_simple_client.py index 61425220..a4d0928d 100644 --- a/src/socketio/async_simple_client.py +++ b/src/socketio/async_simple_client.py @@ -163,6 +163,8 @@ async def call(self, event, data=None, timeout=60): return await self.client.call(event, data, namespace=self.namespace, timeout=timeout) + except TimeoutError: + raise except SocketIOError: pass diff --git a/src/socketio/simple_client.py b/src/socketio/simple_client.py index 9bc5390c..7e1a8a5b 100644 --- a/src/socketio/simple_client.py +++ b/src/socketio/simple_client.py @@ -155,6 +155,8 @@ def call(self, event, data=None, timeout=60): try: return self.client.call(event, data, namespace=self.namespace, timeout=timeout) + except TimeoutError: + raise except SocketIOError: pass diff --git a/tests/async/test_simple_client.py b/tests/async/test_simple_client.py index 0f3e5045..b7f0193c 100644 --- a/tests/async/test_simple_client.py +++ b/tests/async/test_simple_client.py @@ -142,6 +142,17 @@ async def test_call_retries(self): client.client.call.assert_awaited_with('foo', 'bar', namespace='/', timeout=60) + async def test_call_timeout(self): + client = AsyncSimpleClient() + client.connected_event.set() + client.connected = True + client.client = mock.MagicMock() + client.client.call = mock.AsyncMock() + client.client.call.side_effect = TimeoutError() + + with pytest.raises(TimeoutError): + await client.call('foo', 'bar') + async def test_receive_with_input_buffer(self): client = AsyncSimpleClient() client.input_buffer = ['foo', 'bar'] diff --git a/tests/common/test_simple_client.py b/tests/common/test_simple_client.py index 6adcb382..43236fe8 100644 --- a/tests/common/test_simple_client.py +++ b/tests/common/test_simple_client.py @@ -130,6 +130,16 @@ def test_call_retries(self): client.client.call.assert_called_with('foo', 'bar', namespace='/', timeout=60) + def test_call_timeout(self): + client = SimpleClient() + client.connected_event.set() + client.connected = True + client.client = mock.MagicMock() + client.client.call.side_effect = TimeoutError() + + with pytest.raises(TimeoutError): + client.call('foo', 'bar') + def test_receive_with_input_buffer(self): client = SimpleClient() client.input_buffer = ['foo', 'bar'] From 53f6be094257ed81476b0e212c8cddd6d06ca39a Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 30 Sep 2025 20:22:45 +0100 Subject: [PATCH 530/571] Replace pickle with json (#1502) --- docs/server.rst | 22 +++++++++++----------- src/socketio/async_aiopika_manager.py | 6 +++--- src/socketio/async_pubsub_manager.py | 15 ++++----------- src/socketio/async_redis_manager.py | 4 ++-- src/socketio/kafka_manager.py | 6 +++--- src/socketio/kombu_manager.py | 4 ++-- src/socketio/pubsub_manager.py | 15 ++++----------- src/socketio/redis_manager.py | 4 ++-- src/socketio/zmq_manager.py | 10 +++++----- tests/async/test_pubsub_manager.py | 13 ++++++------- tests/common/test_pubsub_manager.py | 13 ++++++------- 11 files changed, 48 insertions(+), 64 deletions(-) diff --git a/docs/server.rst b/docs/server.rst index 508bd46f..9261a425 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -1096,17 +1096,17 @@ For a production deployment there are a few recommendations to keep your application secure. First of all, the message queue should never be listening on a public network -interface, to ensure that external clients never connect to it. The use of a -private network (VPC), where the communication between servers can happen -privately is highly recommended. - -In addition, all message queues support authentication and encryption. -Authentication ensures that only the Socket.IO servers and related processes -have access, while encryption prevents data to be collected by a third-party -listening on the network. - -Access credentials can be included in the connection URLs that are passed to the -client managers. +interface, to ensure that external clients never connect to it. For a single +node deployment, the queue should only listen on `localhost`. For a multi-node +system the use of a private network (VPC), where the communication between +servers can happen privately is highly recommended. + +In addition, all message queues support authentication and encryption, which +can strenthen the security of the deployment. Authentication ensures that only +the Socket.IO servers and related processes have access, while encryption +prevents data from being collected by a third-party that is listening on the +network. Access credentials can be included in the connection URLs that are +passed to the client managers. Horizontal Scaling ~~~~~~~~~~~~~~~~~~ diff --git a/src/socketio/async_aiopika_manager.py b/src/socketio/async_aiopika_manager.py index 003b67bc..1485d37a 100644 --- a/src/socketio/async_aiopika_manager.py +++ b/src/socketio/async_aiopika_manager.py @@ -1,6 +1,6 @@ import asyncio -import pickle +from engineio import json from .async_pubsub_manager import AsyncPubSubManager try: @@ -82,7 +82,7 @@ async def _publish(self, data): try: await self.publisher_exchange.publish( aio_pika.Message( - body=pickle.dumps(data), + body=json.dumps(data), delivery_mode=aio_pika.DeliveryMode.PERSISTENT ), routing_key='*', ) @@ -113,7 +113,7 @@ async def _listen(self): async with queue.iterator() as queue_iter: async for message in queue_iter: async with message.process(): - yield pickle.loads(message.body) + yield message.body retry_sleep = 1 except aio_pika.AMQPException: self._get_logger().error( diff --git a/src/socketio/async_pubsub_manager.py b/src/socketio/async_pubsub_manager.py index 72946eb2..9ccc3382 100644 --- a/src/socketio/async_pubsub_manager.py +++ b/src/socketio/async_pubsub_manager.py @@ -3,7 +3,6 @@ import uuid from engineio import json -import pickle from .async_manager import AsyncManager @@ -202,16 +201,10 @@ async def _thread(self): if isinstance(message, dict): data = message else: - if isinstance(message, bytes): # pragma: no cover - try: - data = pickle.loads(message) - except: - pass - if data is None: - try: - data = json.loads(message) - except: - pass + try: + data = json.loads(message) + except: + pass if data and 'method' in data: self._get_logger().debug('pubsub message: {}'.format( data['method'])) diff --git a/src/socketio/async_redis_manager.py b/src/socketio/async_redis_manager.py index b37e9059..4f9e3264 100644 --- a/src/socketio/async_redis_manager.py +++ b/src/socketio/async_redis_manager.py @@ -1,5 +1,4 @@ import asyncio -import pickle from urllib.parse import urlparse try: # pragma: no cover @@ -20,6 +19,7 @@ valkey = None ValkeyError = None +from engineio import json from .async_pubsub_manager import AsyncPubSubManager from .redis_manager import parse_redis_sentinel_url @@ -108,7 +108,7 @@ async def _publish(self, data): if not retry: self._redis_connect() return await self.redis.publish( - self.channel, pickle.dumps(data)) + self.channel, json.dumps(data)) except error as exc: if retry: self._get_logger().error( diff --git a/src/socketio/kafka_manager.py b/src/socketio/kafka_manager.py index 11b87ad8..a9f1a075 100644 --- a/src/socketio/kafka_manager.py +++ b/src/socketio/kafka_manager.py @@ -1,11 +1,11 @@ import logging -import pickle try: import kafka except ImportError: kafka = None +from engineio import json from .pubsub_manager import PubSubManager logger = logging.getLogger('socketio') @@ -53,7 +53,7 @@ def __init__(self, url='kafka://localhost:9092', channel='socketio', bootstrap_servers=self.kafka_urls) def _publish(self, data): - self.producer.send(self.channel, value=pickle.dumps(data)) + self.producer.send(self.channel, value=json.dumps(data)) self.producer.flush() def _kafka_listen(self): @@ -62,4 +62,4 @@ def _kafka_listen(self): def _listen(self): for message in self._kafka_listen(): if message.topic == self.channel: - yield pickle.loads(message.value) + yield message.value diff --git a/src/socketio/kombu_manager.py b/src/socketio/kombu_manager.py index 09e260c9..bc84bbb9 100644 --- a/src/socketio/kombu_manager.py +++ b/src/socketio/kombu_manager.py @@ -1,4 +1,3 @@ -import pickle import time import uuid @@ -7,6 +6,7 @@ except ImportError: kombu = None +from engineio import json from .pubsub_manager import PubSubManager @@ -102,7 +102,7 @@ def _publish(self, data): try: producer_publish = self._producer_publish( self.publisher_connection) - producer_publish(pickle.dumps(data)) + producer_publish(json.dumps(data)) break except (OSError, kombu.exceptions.KombuError): if retry: diff --git a/src/socketio/pubsub_manager.py b/src/socketio/pubsub_manager.py index 3270b4cb..80744f2f 100644 --- a/src/socketio/pubsub_manager.py +++ b/src/socketio/pubsub_manager.py @@ -2,7 +2,6 @@ import uuid from engineio import json -import pickle from .manager import Manager @@ -196,16 +195,10 @@ def _thread(self): if isinstance(message, dict): data = message else: - if isinstance(message, bytes): # pragma: no cover - try: - data = pickle.loads(message) - except: - pass - if data is None: - try: - data = json.loads(message) - except: - pass + try: + data = json.loads(message) + except: + pass if data and 'method' in data: self._get_logger().debug('pubsub message: {}'.format( data['method'])) diff --git a/src/socketio/redis_manager.py b/src/socketio/redis_manager.py index 2e68c31c..4f701b92 100644 --- a/src/socketio/redis_manager.py +++ b/src/socketio/redis_manager.py @@ -1,5 +1,4 @@ import logging -import pickle import time from urllib.parse import urlparse @@ -17,6 +16,7 @@ valkey = None ValkeyError = None +from engineio import json from .pubsub_manager import PubSubManager logger = logging.getLogger('socketio') @@ -145,7 +145,7 @@ def _publish(self, data): try: if not retry: self._redis_connect() - return self.redis.publish(self.channel, pickle.dumps(data)) + return self.redis.publish(self.channel, json.dumps(data)) except error as exc: if retry: logger.error( diff --git a/src/socketio/zmq_manager.py b/src/socketio/zmq_manager.py index aa5a49a2..a71b869c 100644 --- a/src/socketio/zmq_manager.py +++ b/src/socketio/zmq_manager.py @@ -1,6 +1,6 @@ -import pickle import re +from engineio import json from .pubsub_manager import PubSubManager @@ -75,14 +75,14 @@ def __init__(self, url='zmq+tcp://localhost:5555+5556', self.channel = channel def _publish(self, data): - pickled_data = pickle.dumps( + packed_data = json.dumps( { 'type': 'message', 'channel': self.channel, 'data': data } - ) - return self.sink.send(pickled_data) + ).encode() + return self.sink.send(packed_data) def zmq_listen(self): while True: @@ -94,7 +94,7 @@ def _listen(self): for message in self.zmq_listen(): if isinstance(message, bytes): try: - message = pickle.loads(message) + message = json.loads(message) except Exception: pass if isinstance(message, dict) and \ diff --git a/tests/async/test_pubsub_manager.py b/tests/async/test_pubsub_manager.py index 71d948a6..4a7012ee 100644 --- a/tests/async/test_pubsub_manager.py +++ b/tests/async/test_pubsub_manager.py @@ -1,5 +1,6 @@ import asyncio import functools +import json from unittest import mock import pytest @@ -482,22 +483,20 @@ async def test_background_thread(self): host_id = self.pm.host_id async def messages(): - import pickle - yield {'method': 'emit', 'value': 'foo', 'host_id': 'x'} yield {'missing': 'method', 'host_id': 'x'} yield '{"method": "callback", "value": "bar", "host_id": "x"}' yield {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', 'host_id': 'x'} yield {'method': 'bogus', 'host_id': 'x'} - yield pickle.dumps({'method': 'close_room', 'value': 'baz', - 'host_id': 'x'}) + yield json.dumps({'method': 'close_room', 'value': 'baz', + 'host_id': 'x'}) yield {'method': 'enter_room', 'sid': '123', 'namespace': '/foo', 'room': 'room', 'host_id': 'x'} yield {'method': 'leave_room', 'sid': '123', 'namespace': '/foo', 'room': 'room', 'host_id': 'x'} yield 'bad json' - yield b'bad pickled' + yield b'bad data' # these should not publish anything on the queue, as they come from # the same host @@ -505,8 +504,8 @@ async def messages(): yield {'method': 'callback', 'value': 'bar', 'host_id': host_id} yield {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', 'host_id': host_id} - yield pickle.dumps({'method': 'close_room', 'value': 'baz', - 'host_id': host_id}) + yield json.dumps({'method': 'close_room', 'value': 'baz', + 'host_id': host_id}) self.pm._listen = messages await self.pm._thread() diff --git a/tests/common/test_pubsub_manager.py b/tests/common/test_pubsub_manager.py index 6d8eda75..32c7d41d 100644 --- a/tests/common/test_pubsub_manager.py +++ b/tests/common/test_pubsub_manager.py @@ -1,4 +1,5 @@ import functools +import json import logging from unittest import mock @@ -465,22 +466,20 @@ def test_background_thread(self): host_id = self.pm.host_id def messages(): - import pickle - yield {'method': 'emit', 'value': 'foo', 'host_id': 'x'} yield {'missing': 'method', 'host_id': 'x'} yield '{"method": "callback", "value": "bar", "host_id": "x"}' yield {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', 'host_id': 'x'} yield {'method': 'bogus', 'host_id': 'x'} - yield pickle.dumps({'method': 'close_room', 'value': 'baz', - 'host_id': 'x'}) + yield json.dumps({'method': 'close_room', 'value': 'baz', + 'host_id': 'x'}) yield {'method': 'enter_room', 'sid': '123', 'namespace': '/foo', 'room': 'room', 'host_id': 'x'} yield {'method': 'leave_room', 'sid': '123', 'namespace': '/foo', 'room': 'room', 'host_id': 'x'} yield 'bad json' - yield b'bad pickled' + yield b'bad data' # these should not publish anything on the queue, as they come from # the same host @@ -488,8 +487,8 @@ def messages(): yield {'method': 'callback', 'value': 'bar', 'host_id': host_id} yield {'method': 'disconnect', 'sid': '123', 'namespace': '/foo', 'host_id': host_id} - yield pickle.dumps({'method': 'close_room', 'value': 'baz', - 'host_id': host_id}) + yield json.dumps({'method': 'close_room', 'value': 'baz', + 'host_id': host_id}) self.pm._listen = mock.MagicMock(side_effect=messages) try: From 400200e00625a49796b84baa44a09cd711fabe50 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 30 Sep 2025 20:28:51 +0100 Subject: [PATCH 531/571] Release 5.14.0 --- CHANGES.md | 14 ++++++++++++++ pyproject.toml | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index fd061624..9e618839 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,19 @@ # python-socketio change log +**Release 5.14.0** - 2025-09-30 + +- Replace pickle with json in message queue communications [#1502](https://github.com/miguelgrinberg/python-socketio/issues/1502) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/53f6be094257ed81476b0e212c8cddd6d06ca39a)) +- Add support for Valkey in the Redis client managers [#1488](https://github.com/miguelgrinberg/python-socketio/issues/1488) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/36a89226a2fb18f876dcba48125a8c51904586ec)) (thanks **phi-friday**!) +- Keep track of which namespaces failed to connect [#1496](https://github.com/miguelgrinberg/python-socketio/issues/1496) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5dc2aea077469ad318e47b28a84845c5efb6bdcf)) +- Fixed transport property of the simple clients to be a string as documented [#1499](https://github.com/miguelgrinberg/python-socketio/issues/1499) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/23556fb3dcb37074020494df40fb4495d47e7efe)) +- SimpleClient.call does not raise `TimeoutError` on timeout [#1501](https://github.com/miguelgrinberg/python-socketio/issues/1501) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a59c6f520059eb095ab4472e5192ce3e486875d9)) (thanks **James Thistlewood**!) +- Wait for client to end background tasks on disconnect [#1500](https://github.com/miguelgrinberg/python-socketio/issues/1500) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f61e0bec3750a83de619db9b779f9d93e449eade)) +- Better error logging for the Redis managers [#1479](https://github.com/miguelgrinberg/python-socketio/issues/1479) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b01b197df1ea5914fdaa2e12758e916ebd60162a)) (thanks **Eugnee**!) +- Channel was not properly initialized in several pubsub client managers [#1476](https://github.com/miguelgrinberg/python-socketio/issues/1476) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/efd1247ed9ed61a61d6840dc2c8c92ab02031afb)) (thanks **Eugnee**!) +- Add message queue deployment recommendations for security ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b3da354ed9eb46c0fb847c628b379ccae475a970)) +- Add missing `async` on session examples for the async server [#1465](https://github.com/miguelgrinberg/python-socketio/issues/1465) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/5e04003dad0140fb1c6acff328e4215e62fbc58a)) (thanks **Func**!) +- Add SPDX license identifier [#1453](https://github.com/miguelgrinberg/python-socketio/issues/1453) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/81c80cddde06dd9561687c74e5769e25613282d7)) (thanks **Marc Mueller**!) + **Release 5.13.0** - 2025-04-12 - Eliminate race conditions on disconnect [#1441](https://github.com/miguelgrinberg/python-socketio/issues/1441) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/288ebb189d799a05bbc5979a834433034ea2939f)) diff --git a/pyproject.toml b/pyproject.toml index e7b6f65c..7b578083 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.13.1.dev0" +version = "5.14.0" license = {text = "MIT"} authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, From 9e42f4650901f99d071fd5e21a68415454b571cb Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 30 Sep 2025 20:30:42 +0100 Subject: [PATCH 532/571] Version 5.14.1.dev0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7b578083..a322f165 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.14.0" +version = "5.14.1.dev0" license = {text = "MIT"} authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, From a8deb3a8f3ee51d75c124157efa7fc9289fd592b Mon Sep 17 00:00:00 2001 From: Darren Chang Date: Thu, 2 Oct 2025 17:19:21 +0800 Subject: [PATCH 533/571] Add support for unix-sock protocol (#1503) --- src/socketio/async_redis_manager.py | 2 +- src/socketio/redis_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/socketio/async_redis_manager.py b/src/socketio/async_redis_manager.py index 4f9e3264..b099d9eb 100644 --- a/src/socketio/async_redis_manager.py +++ b/src/socketio/async_redis_manager.py @@ -70,7 +70,7 @@ def __init__(self, url='redis://localhost:6379/0', channel='socketio', def _get_redis_module_and_error(self): parsed_url = urlparse(self.redis_url) schema = parsed_url.scheme.split('+', 1)[0].lower() - if schema == 'redis': + if schema in ['redis', 'unix']: if aioredis is None or RedisError is None: raise RuntimeError('Redis package is not installed ' '(Run "pip install redis" ' diff --git a/src/socketio/redis_manager.py b/src/socketio/redis_manager.py index 4f701b92..13d20228 100644 --- a/src/socketio/redis_manager.py +++ b/src/socketio/redis_manager.py @@ -108,7 +108,7 @@ def initialize(self): def _get_redis_module_and_error(self): parsed_url = urlparse(self.redis_url) schema = parsed_url.scheme.split('+', 1)[0].lower() - if schema == 'redis': + if schema in ['redis', 'unix']: if redis is None or RedisError is None: raise RuntimeError('Redis package is not installed ' '(Run "pip install redis" ' From 6e2d0de12bb4e4a99fdfc30bed0706ded620822c Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Thu, 2 Oct 2025 12:06:19 +0100 Subject: [PATCH 534/571] Restore support for rediss:// URLs, and add valkeys:// --- src/socketio/async_redis_manager.py | 47 ++++++------ src/socketio/redis_manager.py | 42 ++++++----- tests/async/test_redis_manager.py | 107 ++++++++++++++++++++++++++++ tests/common/test_redis_manager.py | 104 ++++++++++++++++++++++++++- tox.ini | 2 + 5 files changed, 263 insertions(+), 39 deletions(-) create mode 100644 tests/async/test_redis_manager.py diff --git a/src/socketio/async_redis_manager.py b/src/socketio/async_redis_manager.py index b099d9eb..b8ac4a0f 100644 --- a/src/socketio/async_redis_manager.py +++ b/src/socketio/async_redis_manager.py @@ -1,7 +1,7 @@ import asyncio from urllib.parse import urlparse -try: # pragma: no cover +try: from redis import asyncio as aioredis from redis.exceptions import RedisError except ImportError: # pragma: no cover @@ -12,11 +12,11 @@ aioredis = None RedisError = None -try: # pragma: no cover - from valkey import asyncio as valkey +try: + from valkey import asyncio as aiovalkey from valkey.exceptions import ValkeyError except ImportError: # pragma: no cover - valkey = None + aiovalkey = None ValkeyError = None from engineio import json @@ -24,7 +24,7 @@ from .redis_manager import parse_redis_sentinel_url -class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover +class AsyncRedisManager(AsyncPubSubManager): """Redis based client manager for asyncio servers. This class implements a Redis backend for event sharing across multiple @@ -55,12 +55,8 @@ class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover def __init__(self, url='redis://localhost:6379/0', channel='socketio', write_only=False, logger=None, redis_options=None): - if aioredis is None and valkey is None: - raise RuntimeError('Redis package is not installed ' - '(Run "pip install redis" or ' - '"pip install valkey" ' - 'in your virtualenv).') - if aioredis and not hasattr(aioredis.Redis, 'from_url'): + if aioredis and \ + not hasattr(aioredis.Redis, 'from_url'): # pragma: no cover raise RuntimeError('Version 2 of aioredis package is required.') super().__init__(channel=channel, write_only=write_only, logger=logger) self.redis_url = url @@ -69,20 +65,31 @@ def __init__(self, url='redis://localhost:6379/0', channel='socketio', def _get_redis_module_and_error(self): parsed_url = urlparse(self.redis_url) - schema = parsed_url.scheme.split('+', 1)[0].lower() - if schema in ['redis', 'unix']: + scheme = parsed_url.scheme.split('+', 1)[0].lower() + if scheme in ['redis', 'rediss']: if aioredis is None or RedisError is None: raise RuntimeError('Redis package is not installed ' '(Run "pip install redis" ' 'in your virtualenv).') return aioredis, RedisError - if schema == 'valkey': - if valkey is None or ValkeyError is None: + if scheme in ['valkey', 'valkeys']: + if aiovalkey is None or ValkeyError is None: raise RuntimeError('Valkey package is not installed ' '(Run "pip install valkey" ' 'in your virtualenv).') - return valkey, ValkeyError - error_msg = f'Unsupported Redis URL schema: {schema}' + return aiovalkey, ValkeyError + if scheme == 'unix': + if aioredis is None or RedisError is None: + if aiovalkey is None or ValkeyError is None: + raise RuntimeError('Redis package is not installed ' + '(Run "pip install redis" ' + 'or "pip install valkey" ' + 'in your virtualenv).') + else: + return aiovalkey, ValkeyError + else: + return aioredis, RedisError + error_msg = f'Unsupported Redis URL scheme: {scheme}' raise ValueError(error_msg) def _redis_connect(self): @@ -100,7 +107,7 @@ def _redis_connect(self): **self.redis_options) self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True) - async def _publish(self, data): + async def _publish(self, data): # pragma: no cover retry = True _, error = self._get_redis_module_and_error() while True: @@ -124,7 +131,7 @@ async def _publish(self, data): break - async def _redis_listen_with_retries(self): + async def _redis_listen_with_retries(self): # pragma: no cover retry_sleep = 1 connect = False _, error = self._get_redis_module_and_error() @@ -147,7 +154,7 @@ async def _redis_listen_with_retries(self): if retry_sleep > 60: retry_sleep = 60 - async def _listen(self): + async def _listen(self): # pragma: no cover channel = self.channel.encode('utf-8') await self.pubsub.subscribe(self.channel) async for message in self._redis_listen_with_retries(): diff --git a/src/socketio/redis_manager.py b/src/socketio/redis_manager.py index 13d20228..5e58ef41 100644 --- a/src/socketio/redis_manager.py +++ b/src/socketio/redis_manager.py @@ -2,17 +2,17 @@ import time from urllib.parse import urlparse -try: # pragma: no cover +try: import redis from redis.exceptions import RedisError -except ImportError: +except ImportError: # pragma: no cover redis = None RedisError = None -try: # pragma: no cover +try: import valkey from valkey.exceptions import ValkeyError -except ImportError: +except ImportError: # pragma: no cover valkey = None ValkeyError = None @@ -48,7 +48,7 @@ def parse_redis_sentinel_url(url): return sentinels, service_name, kwargs -class RedisManager(PubSubManager): # pragma: no cover +class RedisManager(PubSubManager): """Redis based client manager. This class implements a Redis backend for event sharing across multiple @@ -80,17 +80,12 @@ class RedisManager(PubSubManager): # pragma: no cover def __init__(self, url='redis://localhost:6379/0', channel='socketio', write_only=False, logger=None, redis_options=None): - if redis is None and valkey is None: - raise RuntimeError('Redis package is not installed ' - '(Run "pip install redis" ' - 'or "pip install valkey" ' - 'in your virtualenv).') super().__init__(channel=channel, write_only=write_only, logger=logger) self.redis_url = url self.redis_options = redis_options or {} self._redis_connect() - def initialize(self): + def initialize(self): # pragma: no cover super().initialize() monkey_patched = True @@ -107,20 +102,31 @@ def initialize(self): def _get_redis_module_and_error(self): parsed_url = urlparse(self.redis_url) - schema = parsed_url.scheme.split('+', 1)[0].lower() - if schema in ['redis', 'unix']: + scheme = parsed_url.scheme.split('+', 1)[0].lower() + if scheme in ['redis', 'rediss']: if redis is None or RedisError is None: raise RuntimeError('Redis package is not installed ' '(Run "pip install redis" ' 'in your virtualenv).') return redis, RedisError - if schema == 'valkey': + if scheme in ['valkey', 'valkeys']: if valkey is None or ValkeyError is None: raise RuntimeError('Valkey package is not installed ' '(Run "pip install valkey" ' 'in your virtualenv).') return valkey, ValkeyError - error_msg = f'Unsupported Redis URL schema: {schema}' + if scheme == 'unix': + if redis is None or RedisError is None: + if valkey is None or ValkeyError is None: + raise RuntimeError('Redis package is not installed ' + '(Run "pip install redis" ' + 'or "pip install valkey" ' + 'in your virtualenv).') + else: + return valkey, ValkeyError + else: + return redis, RedisError + error_msg = f'Unsupported Redis URL scheme: {scheme}' raise ValueError(error_msg) def _redis_connect(self): @@ -138,7 +144,7 @@ def _redis_connect(self): **self.redis_options) self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True) - def _publish(self, data): + def _publish(self, data): # pragma: no cover retry = True _, error = self._get_redis_module_and_error() while True: @@ -160,7 +166,7 @@ def _publish(self, data): ) break - def _redis_listen_with_retries(self): + def _redis_listen_with_retries(self): # pragma: no cover retry_sleep = 1 connect = False _, error = self._get_redis_module_and_error() @@ -181,7 +187,7 @@ def _redis_listen_with_retries(self): if retry_sleep > 60: retry_sleep = 60 - def _listen(self): + def _listen(self): # pragma: no cover channel = self.channel.encode('utf-8') self.pubsub.subscribe(self.channel) for message in self._redis_listen_with_retries(): diff --git a/tests/async/test_redis_manager.py b/tests/async/test_redis_manager.py new file mode 100644 index 00000000..01c0c375 --- /dev/null +++ b/tests/async/test_redis_manager.py @@ -0,0 +1,107 @@ +import pytest +import redis +import valkey + +from socketio import async_redis_manager +from socketio.async_redis_manager import AsyncRedisManager + + +class TestAsyncRedisManager: + def test_redis_not_installed(self): + saved_redis = async_redis_manager.aioredis + async_redis_manager.aioredis = None + + with pytest.raises(RuntimeError): + AsyncRedisManager('redis://') + assert AsyncRedisManager('unix:///var/sock/redis.sock') is not None + + async_redis_manager.aioredis = saved_redis + + def test_valkey_not_installed(self): + saved_valkey = async_redis_manager.aiovalkey + async_redis_manager.aiovalkey = None + + with pytest.raises(RuntimeError): + AsyncRedisManager('valkey://') + assert AsyncRedisManager('unix:///var/sock/redis.sock') is not None + + async_redis_manager.aiovalkey = saved_valkey + + def test_redis_valkey_not_installed(self): + saved_redis = async_redis_manager.aioredis + async_redis_manager.aioredis = None + saved_valkey = async_redis_manager.aiovalkey + async_redis_manager.aiovalkey = None + + with pytest.raises(RuntimeError): + AsyncRedisManager('redis://') + with pytest.raises(RuntimeError): + AsyncRedisManager('valkey://') + with pytest.raises(RuntimeError): + AsyncRedisManager('unix:///var/sock/redis.sock') + + async_redis_manager.aioredis = saved_redis + async_redis_manager.aiovalkey = saved_valkey + + def test_bad_url(self): + with pytest.raises(ValueError): + AsyncRedisManager('http://localhost:6379') + + def test_redis_connect(self): + urls = [ + 'redis://localhost:6379', + 'redis://localhost:6379/0', + 'redis://:password@localhost:6379', + 'redis://:password@localhost:6379/0', + 'redis://user:password@localhost:6379', + 'redis://user:password@localhost:6379/0', + + 'rediss://localhost:6379', + 'rediss://localhost:6379/0', + 'rediss://:password@localhost:6379', + 'rediss://:password@localhost:6379/0', + 'rediss://user:password@localhost:6379', + 'rediss://user:password@localhost:6379/0', + + 'unix:///var/sock/redis.sock', + 'unix:///var/sock/redis.sock?db=0', + 'unix://user@/var/sock/redis.sock', + 'unix://user@/var/sock/redis.sock?db=0', + + 'redis+sentinel://192.168.0.1:6379,192.168.0.2:6379/' + ] + for url in urls: + c = AsyncRedisManager(url) + assert isinstance(c.redis, redis.asyncio.Redis) + + def test_valkey_connect(self): + saved_redis = async_redis_manager.aioredis + async_redis_manager.aioredis = None + + urls = [ + 'valkey://localhost:6379', + 'valkey://localhost:6379/0', + 'valkey://:password@localhost:6379', + 'valkey://:password@localhost:6379/0', + 'valkey://user:password@localhost:6379', + 'valkey://user:password@localhost:6379/0', + + 'valkeys://localhost:6379', + 'valkeys://localhost:6379/0', + 'valkeys://:password@localhost:6379', + 'valkeys://:password@localhost:6379/0', + 'valkeys://user:password@localhost:6379', + 'valkeys://user:password@localhost:6379/0', + + 'unix:///var/sock/redis.sock', + 'unix:///var/sock/redis.sock?db=0', + 'unix://user@/var/sock/redis.sock', + 'unix://user@/var/sock/redis.sock?db=0', + + 'valkey+sentinel://192.168.0.1:6379,192.168.0.2:6379/' + ] + for url in urls: + c = AsyncRedisManager(url) + assert isinstance(c.redis, valkey.asyncio.Valkey) + + async_redis_manager.aioredis = saved_redis diff --git a/tests/common/test_redis_manager.py b/tests/common/test_redis_manager.py index 48dfb4a9..3beadf3b 100644 --- a/tests/common/test_redis_manager.py +++ b/tests/common/test_redis_manager.py @@ -1,9 +1,111 @@ import pytest +import redis +import valkey -from socketio.redis_manager import parse_redis_sentinel_url +from socketio import redis_manager +from socketio.redis_manager import RedisManager, parse_redis_sentinel_url class TestPubSubManager: + def test_redis_not_installed(self): + saved_redis = redis_manager.redis + redis_manager.redis = None + + with pytest.raises(RuntimeError): + RedisManager('redis://') + assert RedisManager('unix:///var/sock/redis.sock') is not None + + redis_manager.redis = saved_redis + + def test_valkey_not_installed(self): + saved_valkey = redis_manager.valkey + redis_manager.valkey = None + + with pytest.raises(RuntimeError): + RedisManager('valkey://') + assert RedisManager('unix:///var/sock/redis.sock') is not None + + redis_manager.valkey = saved_valkey + + def test_redis_valkey_not_installed(self): + saved_redis = redis_manager.redis + redis_manager.redis = None + saved_valkey = redis_manager.valkey + redis_manager.valkey = None + + with pytest.raises(RuntimeError): + RedisManager('redis://') + with pytest.raises(RuntimeError): + RedisManager('valkey://') + with pytest.raises(RuntimeError): + RedisManager('unix:///var/sock/redis.sock') + + redis_manager.redis = saved_redis + redis_manager.valkey = saved_valkey + + def test_bad_url(self): + with pytest.raises(ValueError): + RedisManager('http://localhost:6379') + + def test_redis_connect(self): + urls = [ + 'redis://localhost:6379', + 'redis://localhost:6379/0', + 'redis://:password@localhost:6379', + 'redis://:password@localhost:6379/0', + 'redis://user:password@localhost:6379', + 'redis://user:password@localhost:6379/0', + + 'rediss://localhost:6379', + 'rediss://localhost:6379/0', + 'rediss://:password@localhost:6379', + 'rediss://:password@localhost:6379/0', + 'rediss://user:password@localhost:6379', + 'rediss://user:password@localhost:6379/0', + + 'unix:///var/sock/redis.sock', + 'unix:///var/sock/redis.sock?db=0', + 'unix://user@/var/sock/redis.sock', + 'unix://user@/var/sock/redis.sock?db=0', + + 'redis+sentinel://192.168.0.1:6379,192.168.0.2:6379/' + ] + for url in urls: + c = RedisManager(url) + assert isinstance(c.redis, redis.Redis) + + def test_valkey_connect(self): + saved_redis = redis_manager.redis + redis_manager.redis = None + + urls = [ + 'valkey://localhost:6379', + 'valkey://localhost:6379/0', + 'valkey://:password@localhost:6379', + 'valkey://:password@localhost:6379/0', + 'valkey://user:password@localhost:6379', + 'valkey://user:password@localhost:6379/0', + + 'valkeys://localhost:6379', + 'valkeys://localhost:6379/0', + 'valkeys://:password@localhost:6379', + 'valkeys://:password@localhost:6379/0', + 'valkeys://user:password@localhost:6379', + 'valkeys://user:password@localhost:6379/0', + + 'unix:///var/sock/redis.sock', + 'unix:///var/sock/redis.sock?db=0', + 'unix://user@/var/sock/redis.sock', + 'unix://user@/var/sock/redis.sock?db=0', + + 'valkey+sentinel://192.168.0.1:6379,192.168.0.2:6379/' + ] + for url in urls: + c = RedisManager(url) + assert isinstance(c.redis, valkey.Valkey) + + redis_manager.redis = saved_redis + @pytest.mark.parametrize('rtype', ['redis', 'valkey']) def test_sentinel_url_parser(self, rtype): with pytest.raises(ValueError): diff --git a/tox.ini b/tox.ini index fc0116cb..bb830590 100644 --- a/tox.ini +++ b/tox.ini @@ -23,6 +23,8 @@ deps= websocket-client aiohttp msgpack + redis + valkey pytest pytest-asyncio pytest-timeout From 0acbf198c631fa235753651f2275b7d5133eb4a4 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Thu, 2 Oct 2025 19:41:26 +0100 Subject: [PATCH 535/571] Release 5.14.1 --- CHANGES.md | 5 +++++ pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 9e618839..fe5406bb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ # python-socketio change log +**Release 5.14.1** - 2025-10-02 + +- Restore support for `rediss://` URLs, and add support for `valkeys://` as well ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6e2d0de12bb4e4a99fdfc30bed0706ded620822c)) +- Add support for Redis connections using unix sockets [#1503](https://github.com/miguelgrinberg/python-socketio/issues/1503) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/a8deb3a8f3ee51d75c124157efa7fc9289fd592b)) (thanks **Darren Chang**!) + **Release 5.14.0** - 2025-09-30 - Replace pickle with json in message queue communications [#1502](https://github.com/miguelgrinberg/python-socketio/issues/1502) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/53f6be094257ed81476b0e212c8cddd6d06ca39a)) diff --git a/pyproject.toml b/pyproject.toml index a322f165..a7e6aff0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.14.1.dev0" +version = "5.14.1" license = {text = "MIT"} authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, From 2ffdd5535aed9f0840003a284a027f677333cd89 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Thu, 2 Oct 2025 19:44:56 +0100 Subject: [PATCH 536/571] Version 5.14.2.dev0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a7e6aff0..f3daf2dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.14.1" +version = "5.14.2.dev0" license = {text = "MIT"} authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, From f298c9b54d76ab09ff72935937e1b9575bc45ffd Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 4 Oct 2025 11:38:53 +0100 Subject: [PATCH 537/571] Fix formatting of client connection error (Fixes #1507) --- src/socketio/async_client.py | 2 +- src/socketio/client.py | 2 +- tests/async/test_client.py | 54 ++++++++++++++++++++++++++++++++++ tests/common/test_client.py | 56 ++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/socketio/async_client.py b/src/socketio/async_client.py index 53c669bb..a4f42671 100644 --- a/src/socketio/async_client.py +++ b/src/socketio/async_client.py @@ -175,7 +175,7 @@ async def connect(self, url, headers={}, auth=None, transports=None, if set(self.namespaces) != set(self.connection_namespaces): await self.disconnect() raise exceptions.ConnectionError( - 'One or more namespaces failed to connect' + 'One or more namespaces failed to connect: ' + ', '.join(self.failed_namespaces)) self.connected = True diff --git a/src/socketio/client.py b/src/socketio/client.py index 4fc36f4e..bf1bee45 100644 --- a/src/socketio/client.py +++ b/src/socketio/client.py @@ -168,7 +168,7 @@ def connect(self, url, headers={}, auth=None, transports=None, if set(self.namespaces) != set(self.connection_namespaces): self.disconnect() raise exceptions.ConnectionError( - 'One or more namespaces failed to connect: ' + 'One or more namespaces failed to connect: ' + ', '.join(self.failed_namespaces)) self.connected = True diff --git a/tests/async/test_client.py b/tests/async/test_client.py index d7e0f9e7..b4b0c6c5 100644 --- a/tests/async/test_client.py +++ b/tests/async/test_client.py @@ -203,6 +203,60 @@ async def mock_connect(): assert c.connected is True assert c.namespaces == {'/bar': '123', '/foo': '456'} + async def test_connect_wait_one_namespaces_error(self): + c = async_client.AsyncClient() + c.eio.connect = mock.AsyncMock() + c._connect_event = mock.MagicMock() + + async def mock_connect(): + if c.failed_namespaces == []: + c.failed_namespaces = ['/foo'] + return True + return False + + c._connect_event.wait = mock_connect + with pytest.raises(exceptions.ConnectionError, + match='failed to connect: /foo'): + await c.connect( + 'url', + namespaces=['/foo'], + wait=True, + wait_timeout=0.01, + ) + assert c.connected is False + assert c.namespaces == {} + assert c.failed_namespaces == ['/foo'] + + async def test_connect_wait_three_namespaces_error(self): + c = async_client.AsyncClient() + c.eio.connect = mock.AsyncMock() + c._connect_event = mock.MagicMock() + + async def mock_connect(): + if c.namespaces == {}: + c.namespaces = {'/bar': '123'} + return True + elif c.namespaces == {'/bar': '123'} and c.failed_namespaces == []: + c.failed_namespaces = ['/baz'] + return True + elif c.failed_namespaces == ['/baz']: + c.failed_namespaces = ['/baz', '/foo'] + return True + return False + + c._connect_event.wait = mock_connect + with pytest.raises(exceptions.ConnectionError, + match='failed to connect: /baz, /foo'): + await c.connect( + 'url', + namespaces=['/foo', '/bar', '/baz'], + wait=True, + wait_timeout=0.01, + ) + assert c.connected is False + assert c.namespaces == {'/bar': '123'} + assert c.failed_namespaces == ['/baz', '/foo'] + async def test_connect_timeout(self): c = async_client.AsyncClient() c.eio.connect = mock.AsyncMock() diff --git a/tests/common/test_client.py b/tests/common/test_client.py index 7ee2bacf..cbda3f1f 100644 --- a/tests/common/test_client.py +++ b/tests/common/test_client.py @@ -350,6 +350,62 @@ def mock_connect(timeout): assert c.connected is True assert c.namespaces == {'/bar': '123', '/foo': '456'} + def test_connect_wait_one_namespaces_error(self): + c = client.Client() + c.eio.connect = mock.MagicMock() + c._connect_event = mock.MagicMock() + + def mock_connect(timeout): + assert timeout == 0.01 + if c.failed_namespaces == []: + c.failed_namespaces = ['/foo'] + return True + return False + + c._connect_event.wait = mock_connect + with pytest.raises(exceptions.ConnectionError, + match='failed to connect: /foo'): + c.connect( + 'url', + namespaces=['/foo'], + wait=True, + wait_timeout=0.01, + ) + assert c.connected is False + assert c.namespaces == {} + assert c.failed_namespaces == ['/foo'] + + def test_connect_wait_three_namespaces_error(self): + c = client.Client() + c.eio.connect = mock.MagicMock() + c._connect_event = mock.MagicMock() + + def mock_connect(timeout): + assert timeout == 0.01 + if c.namespaces == {}: + c.namespaces = {'/bar': '123'} + return True + elif c.namespaces == {'/bar': '123'} and c.failed_namespaces == []: + c.failed_namespaces = ['/baz'] + return True + elif c.failed_namespaces == ['/baz']: + c.failed_namespaces = ['/baz', '/foo'] + return True + return False + + c._connect_event.wait = mock_connect + with pytest.raises(exceptions.ConnectionError, + match='failed to connect: /baz, /foo'): + c.connect( + 'url', + namespaces=['/foo', '/bar', '/baz'], + wait=True, + wait_timeout=0.01, + ) + assert c.connected is False + assert c.namespaces == {'/bar': '123'} + assert c.failed_namespaces == ['/baz', '/foo'] + def test_connect_timeout(self): c = client.Client() c.eio.connect = mock.MagicMock() From 33722a0d96036f005188b07b8b46a5ef091fe65f Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 4 Oct 2025 19:21:26 +0100 Subject: [PATCH 538/571] Improve documentation of the `BaseManager.get_participants()` method --- src/socketio/base_manager.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/socketio/base_manager.py b/src/socketio/base_manager.py index dafa60ac..58706db4 100644 --- a/src/socketio/base_manager.py +++ b/src/socketio/base_manager.py @@ -29,7 +29,13 @@ def get_namespaces(self): return self.rooms.keys() def get_participants(self, namespace, room): - """Return an iterable with the active participants in a room.""" + """Return an iterable with the active participants in a room. + + Note that in a multi-server scenario this method only returns the + participants connect to the server in which the method is called. There + is currently no functionality to assemble a complete list of users across + multiple servers. + """ ns = self.rooms.get(namespace, {}) if hasattr(room, '__len__') and not isinstance(room, str): participants = ns[room[0]]._fwdm.copy() if room[0] in ns else {} From 383eeaf8a6534ee582abe26cb914c33a06daf99d Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 4 Oct 2025 19:30:23 +0100 Subject: [PATCH 539/571] linter fixes #nolog --- src/socketio/async_client.py | 4 ++-- src/socketio/base_manager.py | 4 ++-- src/socketio/client.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/socketio/async_client.py b/src/socketio/async_client.py index a4f42671..678743a2 100644 --- a/src/socketio/async_client.py +++ b/src/socketio/async_client.py @@ -175,8 +175,8 @@ async def connect(self, url, headers={}, auth=None, transports=None, if set(self.namespaces) != set(self.connection_namespaces): await self.disconnect() raise exceptions.ConnectionError( - 'One or more namespaces failed to connect: ' + - ', '.join(self.failed_namespaces)) + 'One or more namespaces failed to connect: ' + + ', '.join(self.failed_namespaces)) self.connected = True diff --git a/src/socketio/base_manager.py b/src/socketio/base_manager.py index 58706db4..ae4530bd 100644 --- a/src/socketio/base_manager.py +++ b/src/socketio/base_manager.py @@ -33,8 +33,8 @@ def get_participants(self, namespace, room): Note that in a multi-server scenario this method only returns the participants connect to the server in which the method is called. There - is currently no functionality to assemble a complete list of users across - multiple servers. + is currently no functionality to assemble a complete list of users + across multiple servers. """ ns = self.rooms.get(namespace, {}) if hasattr(room, '__len__') and not isinstance(room, str): diff --git a/src/socketio/client.py b/src/socketio/client.py index bf1bee45..5282e0a1 100644 --- a/src/socketio/client.py +++ b/src/socketio/client.py @@ -168,8 +168,8 @@ def connect(self, url, headers={}, auth=None, transports=None, if set(self.namespaces) != set(self.connection_namespaces): self.disconnect() raise exceptions.ConnectionError( - 'One or more namespaces failed to connect: ' + - ', '.join(self.failed_namespaces)) + 'One or more namespaces failed to connect: ' + + ', '.join(self.failed_namespaces)) self.connected = True From bab4a10f48aaae11d7f832ebe5c30ad3f85d31b3 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 15 Oct 2025 09:17:11 +0100 Subject: [PATCH 540/571] Restore binary message support in message queue setups (Fixes #1508) (#1509) --- src/socketio/async_pubsub_manager.py | 14 +++++- src/socketio/packet.py | 40 +++++++++-------- src/socketio/pubsub_manager.py | 14 +++++- tests/async/test_pubsub_manager.py | 67 ++++++++++++++++++++++++++++ tests/common/test_packet.py | 24 ++++++---- tests/common/test_pubsub_manager.py | 65 +++++++++++++++++++++++++++ 6 files changed, 194 insertions(+), 30 deletions(-) diff --git a/src/socketio/async_pubsub_manager.py b/src/socketio/async_pubsub_manager.py index 9ccc3382..1bfcf9dc 100644 --- a/src/socketio/async_pubsub_manager.py +++ b/src/socketio/async_pubsub_manager.py @@ -1,10 +1,12 @@ import asyncio +import base64 from functools import partial import uuid from engineio import json from .async_manager import AsyncManager +from .packet import Packet class AsyncPubSubManager(AsyncManager): @@ -64,8 +66,12 @@ async def emit(self, event, data, namespace=None, room=None, skip_sid=None, callback = (room, namespace, id) else: callback = None + binary = Packet.data_is_binary(data) + if binary: + data, attachments = Packet.deconstruct_binary(data) + data = [data, *[base64.b64encode(a).decode() for a in attachments]] message = {'method': 'emit', 'event': event, 'data': data, - 'namespace': namespace, 'room': room, + 'binary': binary, 'namespace': namespace, 'room': room, 'skip_sid': skip_sid, 'callback': callback, 'host_id': self.host_id} await self._handle_emit(message) # handle in this host @@ -145,7 +151,11 @@ async def _handle_emit(self, message): *remote_callback) else: callback = None - await super().emit(message['event'], message['data'], + data = message['data'] + if message.get('binary'): + attachments = [base64.b64decode(a) for a in data[1:]] + data = Packet.reconstruct_binary(data[0], attachments) + await super().emit(message['event'], data, namespace=message.get('namespace'), room=message.get('room'), skip_sid=message.get('skip_sid'), diff --git a/src/socketio/packet.py b/src/socketio/packet.py index f7ad87e6..3deba7fb 100644 --- a/src/socketio/packet.py +++ b/src/socketio/packet.py @@ -29,7 +29,7 @@ def __init__(self, packet_type=EVENT, data=None, namespace=None, id=None, self.namespace = namespace self.id = id if self.uses_binary_events and \ - (binary or (binary is None and self._data_is_binary( + (binary or (binary is None and self.data_is_binary( self.data))): if self.packet_type == EVENT: self.packet_type = BINARY_EVENT @@ -51,7 +51,7 @@ def encode(self): """ encoded_packet = str(self.packet_type) if self.packet_type == BINARY_EVENT or self.packet_type == BINARY_ACK: - data, attachments = self._deconstruct_binary(self.data) + data, attachments = self.deconstruct_binary(self.data) encoded_packet += str(len(attachments)) + '-' else: data = self.data @@ -119,61 +119,65 @@ def add_attachment(self, attachment): raise ValueError('Unexpected binary attachment') self.attachments.append(attachment) if self.attachment_count == len(self.attachments): - self.reconstruct_binary(self.attachments) + self.data = self.reconstruct_binary(self.data, self.attachments) return True return False - def reconstruct_binary(self, attachments): + @classmethod + def reconstruct_binary(cls, data, attachments): """Reconstruct a decoded packet using the given list of binary attachments. """ - self.data = self._reconstruct_binary_internal(self.data, - self.attachments) + return cls._reconstruct_binary_internal(data, attachments) - def _reconstruct_binary_internal(self, data, attachments): + @classmethod + def _reconstruct_binary_internal(cls, data, attachments): if isinstance(data, list): - return [self._reconstruct_binary_internal(item, attachments) + return [cls._reconstruct_binary_internal(item, attachments) for item in data] elif isinstance(data, dict): if data.get('_placeholder') and 'num' in data: return attachments[data['num']] else: - return {key: self._reconstruct_binary_internal(value, - attachments) + return {key: cls._reconstruct_binary_internal(value, + attachments) for key, value in data.items()} else: return data - def _deconstruct_binary(self, data): + @classmethod + def deconstruct_binary(cls, data): """Extract binary components in the packet.""" attachments = [] - data = self._deconstruct_binary_internal(data, attachments) + data = cls._deconstruct_binary_internal(data, attachments) return data, attachments - def _deconstruct_binary_internal(self, data, attachments): + @classmethod + def _deconstruct_binary_internal(cls, data, attachments): if isinstance(data, bytes): attachments.append(data) return {'_placeholder': True, 'num': len(attachments) - 1} elif isinstance(data, list): - return [self._deconstruct_binary_internal(item, attachments) + return [cls._deconstruct_binary_internal(item, attachments) for item in data] elif isinstance(data, dict): - return {key: self._deconstruct_binary_internal(value, attachments) + return {key: cls._deconstruct_binary_internal(value, attachments) for key, value in data.items()} else: return data - def _data_is_binary(self, data): + @classmethod + def data_is_binary(cls, data): """Check if the data contains binary components.""" if isinstance(data, bytes): return True elif isinstance(data, list): return functools.reduce( - lambda a, b: a or b, [self._data_is_binary(item) + lambda a, b: a or b, [cls.data_is_binary(item) for item in data], False) elif isinstance(data, dict): return functools.reduce( - lambda a, b: a or b, [self._data_is_binary(item) + lambda a, b: a or b, [cls.data_is_binary(item) for item in data.values()], False) else: diff --git a/src/socketio/pubsub_manager.py b/src/socketio/pubsub_manager.py index 80744f2f..3528d228 100644 --- a/src/socketio/pubsub_manager.py +++ b/src/socketio/pubsub_manager.py @@ -1,9 +1,11 @@ +import base64 from functools import partial import uuid from engineio import json from .manager import Manager +from .packet import Packet class PubSubManager(Manager): @@ -61,8 +63,12 @@ def emit(self, event, data, namespace=None, room=None, skip_sid=None, callback = (room, namespace, id) else: callback = None + binary = Packet.data_is_binary(data) + if binary: + data, attachments = Packet.deconstruct_binary(data) + data = [data, *[base64.b64encode(a).decode() for a in attachments]] message = {'method': 'emit', 'event': event, 'data': data, - 'namespace': namespace, 'room': room, + 'binary': binary, 'namespace': namespace, 'room': room, 'skip_sid': skip_sid, 'callback': callback, 'host_id': self.host_id} self._handle_emit(message) # handle in this host @@ -141,7 +147,11 @@ def _handle_emit(self, message): *remote_callback) else: callback = None - super().emit(message['event'], message['data'], + data = message['data'] + if message.get('binary'): + attachments = [base64.b64decode(a) for a in data[1:]] + data = Packet.reconstruct_binary(data[0], attachments) + super().emit(message['event'], data, namespace=message.get('namespace'), room=message.get('room'), skip_sid=message.get('skip_sid'), callback=callback) diff --git a/tests/async/test_pubsub_manager.py b/tests/async/test_pubsub_manager.py index 4a7012ee..5d34e530 100644 --- a/tests/async/test_pubsub_manager.py +++ b/tests/async/test_pubsub_manager.py @@ -57,6 +57,7 @@ async def test_emit(self): { 'method': 'emit', 'event': 'foo', + 'binary': False, 'data': 'bar', 'namespace': '/', 'room': None, @@ -66,6 +67,36 @@ async def test_emit(self): } ) + async def test_emit_binary(self): + await self.pm.emit('foo', b'bar') + self.pm._publish.assert_awaited_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + await self.pm.emit('foo', {'foo': b'bar'}) + self.pm._publish.assert_awaited_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + async def test_emit_with_to(self): sid = 'room-mate' await self.pm.emit('foo', 'bar', to=sid) @@ -73,6 +104,7 @@ async def test_emit_with_to(self): { 'method': 'emit', 'event': 'foo', + 'binary': False, 'data': 'bar', 'namespace': '/', 'room': sid, @@ -88,6 +120,7 @@ async def test_emit_with_namespace(self): { 'method': 'emit', 'event': 'foo', + 'binary': False, 'data': 'bar', 'namespace': '/baz', 'room': None, @@ -103,6 +136,7 @@ async def test_emit_with_room(self): { 'method': 'emit', 'event': 'foo', + 'binary': False, 'data': 'bar', 'namespace': '/', 'room': 'baz', @@ -118,6 +152,7 @@ async def test_emit_with_skip_sid(self): { 'method': 'emit', 'event': 'foo', + 'binary': False, 'data': 'bar', 'namespace': '/', 'room': None, @@ -136,6 +171,7 @@ async def test_emit_with_callback(self): { 'method': 'emit', 'event': 'foo', + 'binary': False, 'data': 'bar', 'namespace': '/', 'room': 'baz', @@ -241,6 +277,37 @@ async def test_handle_emit(self): callback=None, ) + async def test_handle_emit_binary(self): + with mock.patch.object( + async_manager.AsyncManager, 'emit' + ) as super_emit: + await self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'], + }) + super_emit.assert_awaited_once_with( + 'foo', + b'bar', + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + await self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'], + }) + super_emit.assert_awaited_with( + 'foo', + {'foo': b'bar'}, + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + async def test_handle_emit_with_namespace(self): with mock.patch.object( async_manager.AsyncManager, 'emit' diff --git a/tests/common/test_packet.py b/tests/common/test_packet.py index 5682dab0..e15babfd 100644 --- a/tests/common/test_packet.py +++ b/tests/common/test_packet.py @@ -266,16 +266,24 @@ def test_decode_dash_in_payload(self): assert pkt.data["a"] == "0123456789-" assert pkt.attachment_count == 0 + def test_deconstruct_binary(self): + datas = [b'foo', [b'foo', b'bar'], ['foo', b'bar'], {'foo': b'bar'}, + {'foo': 'bar', 'baz': b'qux'}, {'foo': [b'bar']}] + for data in datas: + bdata, attachments = packet.Packet.deconstruct_binary(data) + rdata = packet.Packet.reconstruct_binary(bdata, attachments) + assert data == rdata + def test_data_is_binary_list(self): pkt = packet.Packet() - assert not pkt._data_is_binary(['foo']) - assert not pkt._data_is_binary([]) - assert pkt._data_is_binary([b'foo']) - assert pkt._data_is_binary(['foo', b'bar']) + assert not pkt.data_is_binary(['foo']) + assert not pkt.data_is_binary([]) + assert pkt.data_is_binary([b'foo']) + assert pkt.data_is_binary(['foo', b'bar']) def test_data_is_binary_dict(self): pkt = packet.Packet() - assert not pkt._data_is_binary({'a': 'foo'}) - assert not pkt._data_is_binary({}) - assert pkt._data_is_binary({'a': b'foo'}) - assert pkt._data_is_binary({'a': 'foo', 'b': b'bar'}) + assert not pkt.data_is_binary({'a': 'foo'}) + assert not pkt.data_is_binary({}) + assert pkt.data_is_binary({'a': b'foo'}) + assert pkt.data_is_binary({'a': 'foo', 'b': b'bar'}) diff --git a/tests/common/test_pubsub_manager.py b/tests/common/test_pubsub_manager.py index 32c7d41d..5ef54734 100644 --- a/tests/common/test_pubsub_manager.py +++ b/tests/common/test_pubsub_manager.py @@ -69,6 +69,7 @@ def test_emit(self): { 'method': 'emit', 'event': 'foo', + 'binary': False, 'data': 'bar', 'namespace': '/', 'room': None, @@ -78,6 +79,36 @@ def test_emit(self): } ) + def test_emit_binary(self): + self.pm.emit('foo', b'bar') + self.pm._publish.assert_called_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + self.pm.emit('foo', {'foo': b'bar'}) + self.pm._publish.assert_called_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + def test_emit_with_to(self): sid = "ferris" self.pm.emit('foo', 'bar', to=sid) @@ -85,6 +116,7 @@ def test_emit_with_to(self): { 'method': 'emit', 'event': 'foo', + 'binary': False, 'data': 'bar', 'namespace': '/', 'room': sid, @@ -100,6 +132,7 @@ def test_emit_with_namespace(self): { 'method': 'emit', 'event': 'foo', + 'binary': False, 'data': 'bar', 'namespace': '/baz', 'room': None, @@ -115,6 +148,7 @@ def test_emit_with_room(self): { 'method': 'emit', 'event': 'foo', + 'binary': False, 'data': 'bar', 'namespace': '/', 'room': 'baz', @@ -130,6 +164,7 @@ def test_emit_with_skip_sid(self): { 'method': 'emit', 'event': 'foo', + 'binary': False, 'data': 'bar', 'namespace': '/', 'room': None, @@ -148,6 +183,7 @@ def test_emit_with_callback(self): { 'method': 'emit', 'event': 'foo', + 'binary': False, 'data': 'bar', 'namespace': '/', 'room': 'baz', @@ -250,6 +286,35 @@ def test_handle_emit(self): callback=None, ) + def test_handle_emit_binary(self): + with mock.patch.object(manager.Manager, 'emit') as super_emit: + self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'], + }) + super_emit.assert_called_once_with( + 'foo', + b'bar', + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'], + }) + super_emit.assert_called_with( + 'foo', + {'foo': b'bar'}, + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + def test_handle_emit_with_namespace(self): with mock.patch.object(manager.Manager, 'emit') as super_emit: self.pm._handle_emit( From 1f4cd3b025c294f25208ec3c05b5f8df6209e403 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 15 Oct 2025 19:30:30 +0100 Subject: [PATCH 541/571] Add 3.14 and pypy-3.11 CI tasks --- .github/workflows/tests.yml | 4 ++-- pyproject.toml | 3 +++ tox.ini | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9d2f4759..ee924cf3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,11 +16,11 @@ jobs: strategy: matrix: os: [windows-latest, macos-latest, ubuntu-latest] - python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.10'] + python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14', 'pypy-3.11'] exclude: # pypy3 currently fails to run on Windows - os: windows-latest - python: pypy-3.10 + python: pypy-3.11 fail-fast: false runs-on: ${{ matrix.os }} steps: diff --git a/pyproject.toml b/pyproject.toml index f3daf2dc..12289037 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,9 @@ client = [ asyncio_client = [ "aiohttp >= 3.4", ] +dev = [ + "tox", +] docs = [ "sphinx", ] diff --git a/tox.ini b/tox.ini index bb830590..b73e4bbc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=flake8,py{38,39,310,311,312,313},docs +envlist=flake8,py{38,39,310,311,312,313,314},docs skip_missing_interpreters=True [gh-actions] @@ -10,6 +10,7 @@ python = 3.11: py311 3.12: py312 3.13: py313 + 3.14: py314 pypy-3: pypy3 [testenv] From 8fcbd80e769695cbe2ec980ee39fb8a7a7e738e6 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 15 Oct 2025 19:57:20 +0100 Subject: [PATCH 542/571] Remove old build matrix exceptions #nolog (#1510) --- .github/workflows/tests.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ee924cf3..6a8433e4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,10 +17,6 @@ jobs: matrix: os: [windows-latest, macos-latest, ubuntu-latest] python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14', 'pypy-3.11'] - exclude: - # pypy3 currently fails to run on Windows - - os: windows-latest - python: pypy-3.11 fail-fast: false runs-on: ${{ matrix.os }} steps: From 08f65c22bb5437a2cdbde03212c2c60dd3b447b7 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 15 Oct 2025 19:59:08 +0100 Subject: [PATCH 543/571] Release 5.14.2 --- CHANGES.md | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index fe5406bb..60955af1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ # python-socketio change log +**Release 5.14.2** - 2025-10-15 + +- Restore binary message support in message queue setups [#1509](https://github.com/miguelgrinberg/python-socketio/issues/1509) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bab4a10f48aaae11d7f832ebe5c30ad3f85d31b3)) +- Fix formatting of client connection error [#1507](https://github.com/miguelgrinberg/python-socketio/issues/1507) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f298c9b54d76ab09ff72935937e1b9575bc45ffd)) +- Add 3.14 and pypy-3.11 CI tasks ([commit](https://github.com/miguelgrinberg/python-socketio/commit/1f4cd3b025c294f25208ec3c05b5f8df6209e403)) +- Improve documentation of the `BaseManager.get_participants()` method ([commit](https://github.com/miguelgrinberg/python-socketio/commit/33722a0d96036f005188b07b8b46a5ef091fe65f)) + **Release 5.14.1** - 2025-10-02 - Restore support for `rediss://` URLs, and add support for `valkeys://` as well ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6e2d0de12bb4e4a99fdfc30bed0706ded620822c)) diff --git a/pyproject.toml b/pyproject.toml index 12289037..7e3b2377 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.14.2.dev0" +version = "5.14.2" license = {text = "MIT"} authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, From f8a1d435b23df74e720cb1c384955ff5dd1c442c Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 15 Oct 2025 19:59:45 +0100 Subject: [PATCH 544/571] Version 5.14.3.dev0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7e3b2377..1148e501 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.14.2" +version = "5.14.3.dev0" license = {text = "MIT"} authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, From 194e1b7f277b5f72e1de78d3f614e7b8b6c788ac Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 28 Oct 2025 20:24:54 +0000 Subject: [PATCH 545/571] Only push binary data to aiopika (Fixes #1513) (#1514) --- src/socketio/async_aiopika_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/socketio/async_aiopika_manager.py b/src/socketio/async_aiopika_manager.py index 1485d37a..60be8877 100644 --- a/src/socketio/async_aiopika_manager.py +++ b/src/socketio/async_aiopika_manager.py @@ -82,7 +82,7 @@ async def _publish(self, data): try: await self.publisher_exchange.publish( aio_pika.Message( - body=json.dumps(data), + body=json.dumps(data).encode(), delivery_mode=aio_pika.DeliveryMode.PERSISTENT ), routing_key='*', ) From f3b18bde3f16437b223491d4c3e440ea37105fe3 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 28 Oct 2025 23:43:05 +0000 Subject: [PATCH 546/571] Support Python's ConnectionRefusedError to reject a connection (#1515) * Support Python's ConnectionRefusedError to reject a connection * unit tests --- docs/server.rst | 2 ++ src/socketio/async_server.py | 3 +++ src/socketio/server.py | 3 +++ tests/async/test_server.py | 15 +++++++++++++++ tests/common/test_server.py | 14 ++++++++++++++ 5 files changed, 37 insertions(+) diff --git a/docs/server.rst b/docs/server.rst index 9261a425..1bc4c68a 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -246,6 +246,8 @@ a :class:`socketio.exceptions.ConnectionRefusedError` exception can be raised, and all of its arguments will be sent to the client with the rejection message:: + from socketio.exceptions import ConnectionRefusedError + @sio.event def connect(sid, environ, auth): raise ConnectionRefusedError('authentication failed') diff --git a/src/socketio/async_server.py b/src/socketio/async_server.py index fac0f2b0..6c9e3ca3 100644 --- a/src/socketio/async_server.py +++ b/src/socketio/async_server.py @@ -561,6 +561,9 @@ async def _handle_connect(self, eio_sid, namespace, data): except exceptions.ConnectionRefusedError as exc: fail_reason = exc.error_args success = False + except ConnectionRefusedError: + fail_reason = {"message": "Connection refused by server"} + success = False if success is False: if self.always_connect: diff --git a/src/socketio/server.py b/src/socketio/server.py index 71c702de..f3257081 100644 --- a/src/socketio/server.py +++ b/src/socketio/server.py @@ -543,6 +543,9 @@ def _handle_connect(self, eio_sid, namespace, data): except exceptions.ConnectionRefusedError as exc: fail_reason = exc.error_args success = False + except ConnectionRefusedError: + fail_reason = {"message": "Connection refused by server"} + success = False if success is False: if self.always_connect: diff --git a/tests/async/test_server.py b/tests/async/test_server.py index f60de27a..575f2097 100644 --- a/tests/async/test_server.py +++ b/tests/async/test_server.py @@ -482,6 +482,21 @@ async def test_handle_connect_rejected_with_exception(self, eio): '123', '4{"message":"fail_reason"}') assert s.environ == {'123': 'environ'} + async def test_handle_connect_rejected_with_python_exception(self, eio): + eio.return_value.send = mock.AsyncMock() + s = async_server.AsyncServer() + handler = mock.MagicMock( + side_effect=ConnectionRefusedError() + ) + s.on('connect', handler) + await s._handle_eio_connect('123', 'environ') + await s._handle_eio_message('123', '0') + assert not s.manager.is_connected('1', '/') + handler.assert_called_once_with('1', 'environ') + s.eio.send.assert_awaited_once_with( + '123', '4{"message":"Connection refused by server"}') + assert s.environ == {'123': 'environ'} + async def test_handle_connect_rejected_with_empty_exception(self, eio): eio.return_value.send = mock.AsyncMock() s = async_server.AsyncServer() diff --git a/tests/common/test_server.py b/tests/common/test_server.py index 445d5d9e..bdbbfe07 100644 --- a/tests/common/test_server.py +++ b/tests/common/test_server.py @@ -462,6 +462,20 @@ def test_handle_connect_rejected_with_exception(self, eio): s.eio.send.assert_called_once_with('123', '4{"message":"fail_reason"}') assert s.environ == {'123': 'environ'} + def test_handle_connect_rejected_with_python_exception(self, eio): + s = server.Server() + handler = mock.MagicMock( + side_effect=ConnectionRefusedError() + ) + s.on('connect', handler) + s._handle_eio_connect('123', 'environ') + s._handle_eio_message('123', '0') + assert not s.manager.is_connected('1', '/') + handler.assert_called_once_with('1', 'environ') + s.eio.send.assert_called_once_with( + '123', '4{"message":"Connection refused by server"}') + assert s.environ == {'123': 'environ'} + def test_handle_connect_rejected_with_empty_exception(self, eio): s = server.Server() handler = mock.MagicMock( From 227e53bdf7d6877f1c7445d4ffeb8793a18c79f8 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 29 Oct 2025 09:42:20 +0000 Subject: [PATCH 547/571] Release 5.14.3 --- CHANGES.md | 5 +++++ pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 60955af1..fe2db445 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ # python-socketio change log +**Release 5.14.3** - 2025-10-29 + +- Support Python's native `ConnectionRefusedError` exception to reject a connection [#1515](https://github.com/miguelgrinberg/python-socketio/issues/1515) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f3b18bde3f16437b223491d4c3e440ea37105fe3)) +- Push binary data to the aiopika client manager [#1514](https://github.com/miguelgrinberg/python-socketio/issues/1514) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/194e1b7f277b5f72e1de78d3f614e7b8b6c788ac)) + **Release 5.14.2** - 2025-10-15 - Restore binary message support in message queue setups [#1509](https://github.com/miguelgrinberg/python-socketio/issues/1509) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/bab4a10f48aaae11d7f832ebe5c30ad3f85d31b3)) diff --git a/pyproject.toml b/pyproject.toml index 1148e501..4d8cad9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.14.3.dev0" +version = "5.14.3" license = {text = "MIT"} authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, From 54b4382b5fba05307694954e375f60bcae1a5a17 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 29 Oct 2025 09:42:55 +0000 Subject: [PATCH 548/571] Version 5.14.4.dev0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4d8cad9d..f444d74d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.14.3" +version = "5.14.4.dev0" license = {text = "MIT"} authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, From c52e93b4a328d98a968bfbdec0cfd598b73ee913 Mon Sep 17 00:00:00 2001 From: Gritty_dev <101377478+codomposer@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:21:35 -0400 Subject: [PATCH 549/571] fix recreate binding on failure (#1516) --- src/socketio/async_aiopika_manager.py | 36 +++++++++++++-------------- src/socketio/kombu_manager.py | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/socketio/async_aiopika_manager.py b/src/socketio/async_aiopika_manager.py index 60be8877..34171c46 100644 --- a/src/socketio/async_aiopika_manager.py +++ b/src/socketio/async_aiopika_manager.py @@ -101,26 +101,26 @@ async def _publish(self, data): raise asyncio.CancelledError() async def _listen(self): - async with (await self._connection()) as connection: - channel = await self._channel(connection) - await channel.set_qos(prefetch_count=1) - exchange = await self._exchange(channel) - queue = await self._queue(channel, exchange) - - retry_sleep = 1 - while True: - try: + retry_sleep = 1 + while True: + try: + async with (await self._connection()) as connection: + channel = await self._channel(connection) + await channel.set_qos(prefetch_count=1) + exchange = await self._exchange(channel) + queue = await self._queue(channel, exchange) + async with queue.iterator() as queue_iter: async for message in queue_iter: async with message.process(): yield message.body retry_sleep = 1 - except aio_pika.AMQPException: - self._get_logger().error( - 'Cannot receive from rabbitmq... ' - 'retrying in {} secs'.format(retry_sleep)) - await asyncio.sleep(retry_sleep) - retry_sleep = min(retry_sleep * 2, 60) - except aio_pika.exceptions.ChannelInvalidStateError: - # aio_pika raises this exception when the task is cancelled - raise asyncio.CancelledError() + except aio_pika.AMQPException: + self._get_logger().error( + 'Cannot receive from rabbitmq... ' + 'retrying in {} secs'.format(retry_sleep)) + await asyncio.sleep(retry_sleep) + retry_sleep = min(retry_sleep * 2, 60) + except aio_pika.exceptions.ChannelInvalidStateError: + # aio_pika raises this exception when the task is cancelled + raise asyncio.CancelledError() diff --git a/src/socketio/kombu_manager.py b/src/socketio/kombu_manager.py index bc84bbb9..ae9d1393 100644 --- a/src/socketio/kombu_manager.py +++ b/src/socketio/kombu_manager.py @@ -115,10 +115,10 @@ def _publish(self, data): break def _listen(self): - reader_queue = self._queue() retry_sleep = 1 while True: try: + reader_queue = self._queue() with self._connection() as connection: with connection.SimpleQueue(reader_queue) as queue: while True: From b423d0e38eef559b7e81acb7e32059de305f982c Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Fri, 31 Oct 2025 09:11:18 +0000 Subject: [PATCH 550/571] Improvements to the logging documentation --- docs/client.rst | 11 ++++++++--- docs/server.rst | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/client.rst b/docs/client.rst index e3e1fb2c..64cbec88 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -198,9 +198,14 @@ terminal:: The ``logger`` argument controls logging related to the Socket.IO protocol, while ``engineio_logger`` controls logs that originate in the low-level -Engine.IO transport. These arguments can be set to ``True`` to output logs to -``stderr``, or to an object compatible with Python's ``logging`` package -where the logs should be emitted to. A value of ``False`` disables logging. +Engine.IO transport. The value given to these arguments controls logging +behavior: + +* ``True``: Enables log output to ``stderr`` at the ``INFO`` level. +* ``False``: Enables log output to ``stderr`` at the ``ERROR`` level. This is + the default. +* A ``logging.Logger`` instance: Uses the provided logger without additional + configuration. Logging can help identify the cause of connection problems, unexpected disconnections and other issues. diff --git a/docs/server.rst b/docs/server.rst index 1bc4c68a..f31e6680 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -657,9 +657,14 @@ terminal:: The ``logger`` argument controls logging related to the Socket.IO protocol, while ``engineio_logger`` controls logs that originate in the low-level -Engine.IO transport. These arguments can be set to ``True`` to output logs to -``stderr``, or to an object compatible with Python's ``logging`` package -where the logs should be emitted to. A value of ``False`` disables logging. +Engine.IO transport. The value given to these arguments controls logging +behavior: + +* ``True``: Enables log output to ``stderr`` at the ``INFO`` level. +* ``False``: Enables log output to ``stderr`` at the ``ERROR`` level. This is + the default. +* A ``logging.Logger`` instance: Uses the provided logger without additional + configuration. Logging can help identify the cause of connection problems, 400 responses, bad performance and other issues. From f4b4a98bda0e8c68f0424bf1306eebb860bb9366 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:35:42 +0000 Subject: [PATCH 551/571] Bump django in /examples/server/wsgi/django_socketio (#1519) #nolog Bumps [django](https://github.com/django/django) from 4.2.22 to 4.2.26. - [Commits](https://github.com/django/django/compare/4.2.22...4.2.26) --- updated-dependencies: - dependency-name: django dependency-version: 4.2.26 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/django_socketio/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/django_socketio/requirements.txt b/examples/server/wsgi/django_socketio/requirements.txt index 39eae80f..065a4405 100644 --- a/examples/server/wsgi/django_socketio/requirements.txt +++ b/examples/server/wsgi/django_socketio/requirements.txt @@ -1,6 +1,6 @@ asgiref==3.6.0 bidict==0.22.1 -Django==4.2.22 +Django==4.2.26 gunicorn==23.0.0 h11==0.16.0 python-engineio From db3f1c2a0105c30cb833ddfca8f05fe4320468fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Nam=20Kh=C3=A1nh?= <55955273+khanhkhanhlele@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:32:33 +0700 Subject: [PATCH 552/571] Fix typos in src/socketio/async_client.py (#1520) --- src/socketio/async_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/socketio/async_client.py b/src/socketio/async_client.py index 678743a2..ead963d0 100644 --- a/src/socketio/async_client.py +++ b/src/socketio/async_client.py @@ -489,7 +489,7 @@ async def _trigger_event(self, event, namespace, *args): raise return ret - # or else, forward the event to a namepsace handler if one exists + # or else, forward the event to a namespace handler if one exists handler, args = self._get_namespace_handler(namespace, args) if handler: return await handler.trigger_event(event, *args) From 6c9b9974f72e2efdf62407ecab24ee6995448098 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 16 Nov 2025 13:38:29 +0000 Subject: [PATCH 553/571] Support sending bytestrings when using pub/sub managers --- src/socketio/packet.py | 4 ++-- tests/async/test_pubsub_manager.py | 30 +++++++++++++++++++++++++++++ tests/common/test_packet.py | 4 ++++ tests/common/test_pubsub_manager.py | 30 +++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/socketio/packet.py b/src/socketio/packet.py index 3deba7fb..101455ea 100644 --- a/src/socketio/packet.py +++ b/src/socketio/packet.py @@ -154,7 +154,7 @@ def deconstruct_binary(cls, data): @classmethod def _deconstruct_binary_internal(cls, data, attachments): - if isinstance(data, bytes): + if isinstance(data, (bytes, bytearray)): attachments.append(data) return {'_placeholder': True, 'num': len(attachments) - 1} elif isinstance(data, list): @@ -169,7 +169,7 @@ def _deconstruct_binary_internal(cls, data, attachments): @classmethod def data_is_binary(cls, data): """Check if the data contains binary components.""" - if isinstance(data, bytes): + if isinstance(data, (bytes, bytearray)): return True elif isinstance(data, list): return functools.reduce( diff --git a/tests/async/test_pubsub_manager.py b/tests/async/test_pubsub_manager.py index 5d34e530..f9cfb7eb 100644 --- a/tests/async/test_pubsub_manager.py +++ b/tests/async/test_pubsub_manager.py @@ -97,6 +97,36 @@ async def test_emit_binary(self): } ) + async def test_emit_bytearray(self): + await self.pm.emit('foo', bytearray(b'bar')) + self.pm._publish.assert_awaited_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + await self.pm.emit('foo', {'foo': bytearray(b'bar')}) + self.pm._publish.assert_awaited_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + async def test_emit_with_to(self): sid = 'room-mate' await self.pm.emit('foo', 'bar', to=sid) diff --git a/tests/common/test_packet.py b/tests/common/test_packet.py index e15babfd..e965313a 100644 --- a/tests/common/test_packet.py +++ b/tests/common/test_packet.py @@ -279,11 +279,15 @@ def test_data_is_binary_list(self): assert not pkt.data_is_binary(['foo']) assert not pkt.data_is_binary([]) assert pkt.data_is_binary([b'foo']) + assert pkt.data_is_binary([bytearray(b'foo')]) assert pkt.data_is_binary(['foo', b'bar']) + assert pkt.data_is_binary(['foo', bytearray(b'bar')]) def test_data_is_binary_dict(self): pkt = packet.Packet() assert not pkt.data_is_binary({'a': 'foo'}) assert not pkt.data_is_binary({}) assert pkt.data_is_binary({'a': b'foo'}) + assert pkt.data_is_binary({'a': bytearray(b'foo')}) assert pkt.data_is_binary({'a': 'foo', 'b': b'bar'}) + assert pkt.data_is_binary({'a': 'foo', 'b': bytearray(b'bar')}) diff --git a/tests/common/test_pubsub_manager.py b/tests/common/test_pubsub_manager.py index 5ef54734..abef2bf2 100644 --- a/tests/common/test_pubsub_manager.py +++ b/tests/common/test_pubsub_manager.py @@ -109,6 +109,36 @@ def test_emit_binary(self): } ) + def test_emit_bytearray(self): + self.pm.emit('foo', bytearray(b'bar')) + self.pm._publish.assert_called_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + self.pm.emit('foo', {'foo': bytearray(b'bar')}) + self.pm._publish.assert_called_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + def test_emit_with_to(self): sid = "ferris" self.pm.emit('foo', 'bar', to=sid) From 208925344a48485d2cd56e40eb74266c3bcb5311 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Fri, 21 Nov 2025 23:22:49 +0000 Subject: [PATCH 554/571] Support ext_type in the MsgPackPacket class (#1521) --- src/socketio/msgpack_packet.py | 30 +++++++- tests/async/test_client.py | 20 ++++++ tests/async/test_server.py | 20 ++++++ tests/common/test_client.py | 20 ++++++ tests/common/test_msgpack_packet.py | 104 ++++++++++++++++++++++++++++ tests/common/test_server.py | 20 ++++++ 6 files changed, 212 insertions(+), 2 deletions(-) diff --git a/src/socketio/msgpack_packet.py b/src/socketio/msgpack_packet.py index 27462634..732d1882 100644 --- a/src/socketio/msgpack_packet.py +++ b/src/socketio/msgpack_packet.py @@ -4,14 +4,40 @@ class MsgPackPacket(packet.Packet): uses_binary_events = False + dumps_default = None + ext_hook = msgpack.ExtType + + @classmethod + def configure(cls, dumps_default=None, ext_hook=msgpack.ExtType): + """Change the default options for msgpack encoding and decoding. + + :param dumps_default: a function called for objects that cannot be + serialized by default msgpack. The function + receives one argument, the object to serialize. + It should return a serializable object or a + ``msgpack.ExtType`` instance. + :param ext_hook: a function called when a ``msgpack.ExtType`` object is + seen during decoding. The function receives two + arguments, the code and the data. It should return the + decoded object. + """ + class CustomMsgPackPacket(MsgPackPacket): + dumps_default = None + ext_hook = None + + CustomMsgPackPacket.dumps_default = dumps_default + CustomMsgPackPacket.ext_hook = ext_hook + return CustomMsgPackPacket def encode(self): """Encode the packet for transmission.""" - return msgpack.dumps(self._to_dict()) + return msgpack.dumps(self._to_dict(), + default=self.__class__.dumps_default) def decode(self, encoded_packet): """Decode a transmitted package.""" - decoded = msgpack.loads(encoded_packet) + decoded = msgpack.loads(encoded_packet, + ext_hook=self.__class__.ext_hook) self.packet_type = decoded['type'] self.data = decoded.get('data') self.id = decoded.get('id') diff --git a/tests/async/test_client.py b/tests/async/test_client.py index b4b0c6c5..7a7bfa7c 100644 --- a/tests/async/test_client.py +++ b/tests/async/test_client.py @@ -1,5 +1,6 @@ import asyncio from unittest import mock +from datetime import datetime, timezone, timedelta import pytest @@ -8,6 +9,7 @@ from engineio import exceptions as engineio_exceptions from socketio import exceptions from socketio import packet +from socketio.msgpack_packet import MsgPackPacket class TestAsyncClient: @@ -1242,3 +1244,21 @@ async def test_eio_disconnect_no_reconnect(self): assert c.sid is None assert not c.connected c.start_background_task.assert_not_called() + + def test_serializer_args_with_msgpack(self): + def default(o): + if isinstance(o, datetime): + return o.isoformat() + raise TypeError("Unknown type") + + data = {"current": datetime.now(timezone(timedelta(0)))} + c = async_client.AsyncClient( + serializer=MsgPackPacket.configure(dumps_default=default)) + p = c.packet_class(data=data) + p2 = c.packet_class(encoded_packet=p.encode()) + + assert p.data != p2.data + assert isinstance(p2.data, dict) + assert "current" in p2.data + assert isinstance(p2.data["current"], str) + assert default(data["current"]) == p2.data["current"] diff --git a/tests/async/test_server.py b/tests/async/test_server.py index 575f2097..10d7ba14 100644 --- a/tests/async/test_server.py +++ b/tests/async/test_server.py @@ -1,6 +1,7 @@ import asyncio import logging from unittest import mock +from datetime import datetime, timezone, timedelta from engineio import json from engineio import packet as eio_packet @@ -11,6 +12,7 @@ from socketio import exceptions from socketio import namespace from socketio import packet +from socketio.msgpack_packet import MsgPackPacket @mock.patch('socketio.server.engineio.AsyncServer', **{ @@ -1089,3 +1091,21 @@ async def test_sleep(self, eio): s = async_server.AsyncServer() await s.sleep(1.23) s.eio.sleep.assert_awaited_once_with(1.23) + + def test_serializer_args_with_msgpack(self, eio): + def default(o): + if isinstance(o, datetime): + return o.isoformat() + raise TypeError("Unknown type") + + data = {"current": datetime.now(timezone(timedelta(0)))} + s = async_server.AsyncServer( + serializer=MsgPackPacket.configure(dumps_default=default)) + p = s.packet_class(data=data) + p2 = s.packet_class(encoded_packet=p.encode()) + + assert p.data != p2.data + assert isinstance(p2.data, dict) + assert "current" in p2.data + assert isinstance(p2.data["current"], str) + assert default(data["current"]) == p2.data["current"] diff --git a/tests/common/test_client.py b/tests/common/test_client.py index cbda3f1f..d386a9c3 100644 --- a/tests/common/test_client.py +++ b/tests/common/test_client.py @@ -1,6 +1,7 @@ import logging import time from unittest import mock +from datetime import datetime, timezone, timedelta from engineio import exceptions as engineio_exceptions from engineio import json @@ -13,6 +14,7 @@ from socketio import msgpack_packet from socketio import namespace from socketio import packet +from socketio.msgpack_packet import MsgPackPacket class TestClient: @@ -1386,3 +1388,21 @@ def test_eio_disconnect_no_reconnect(self): assert c.sid is None assert not c.connected c.start_background_task.assert_not_called() + + def test_serializer_args_with_msgpack(self): + def default(o): + if isinstance(o, datetime): + return o.isoformat() + raise TypeError("Unknown type") + + data = {"current": datetime.now(timezone(timedelta(0)))} + c = client.Client( + serializer=MsgPackPacket.configure(dumps_default=default)) + p = c.packet_class(data=data) + p2 = c.packet_class(encoded_packet=p.encode()) + + assert p.data != p2.data + assert isinstance(p2.data, dict) + assert "current" in p2.data + assert isinstance(p2.data["current"], str) + assert default(data["current"]) == p2.data["current"] diff --git a/tests/common/test_msgpack_packet.py b/tests/common/test_msgpack_packet.py index e0197a27..0fad0292 100644 --- a/tests/common/test_msgpack_packet.py +++ b/tests/common/test_msgpack_packet.py @@ -1,3 +1,8 @@ +from datetime import datetime, timedelta, timezone + +import pytest +import msgpack + from socketio import msgpack_packet from socketio import packet @@ -32,3 +37,102 @@ def test_encode_binary_ack_packet(self): assert p.packet_type == packet.ACK p2 = msgpack_packet.MsgPackPacket(encoded_packet=p.encode()) assert p2.data == {'foo': b'bar'} + + def test_encode_with_dumps_default(self): + def default(obj): + if isinstance(obj, datetime): + return obj.isoformat() + raise TypeError('Unknown type') + + data = { + 'current': datetime.now(tz=timezone(timedelta(0))), + 'key': 'value', + } + p = msgpack_packet.MsgPackPacket.configure(dumps_default=default)( + data=data) + p2 = msgpack_packet.MsgPackPacket(encoded_packet=p.encode()) + assert p.packet_type == p2.packet_type + assert p.id == p2.id + assert p.namespace == p2.namespace + assert p.data != p2.data + + assert isinstance(p2.data, dict) + assert 'current' in p2.data + assert isinstance(p2.data['current'], str) + assert default(data['current']) == p2.data['current'] + + data.pop('current') + p2_data_without_current = p2.data.copy() + p2_data_without_current.pop('current') + assert data == p2_data_without_current + + def test_encode_without_dumps_default(self): + data = { + 'current': datetime.now(tz=timezone(timedelta(0))), + 'key': 'value', + } + p_without_default = msgpack_packet.MsgPackPacket(data=data) + with pytest.raises(TypeError): + p_without_default.encode() + + def test_encode_decode_with_ext_hook(self): + class Custom: + def __init__(self, value): + self.value = value + + def __eq__(self, value: object) -> bool: + return isinstance(value, Custom) and self.value == value.value + + def default(obj): + if isinstance(obj, Custom): + return msgpack.ExtType(1, obj.value) + raise TypeError('Unknown type') + + def ext_hook(code, data): + if code == 1: + return Custom(data) + raise TypeError('Unknown ext type') + + data = {'custom': Custom(b'custom_data'), 'key': 'value'} + p = msgpack_packet.MsgPackPacket.configure(dumps_default=default)( + data=data) + p2 = msgpack_packet.MsgPackPacket.configure(ext_hook=ext_hook)( + encoded_packet=p.encode() + ) + assert p.packet_type == p2.packet_type + assert p.id == p2.id + assert p.data == p2.data + assert p.namespace == p2.namespace + + def test_encode_decode_without_ext_hook(self): + class Custom: + def __init__(self, value): + self.value = value + + def __eq__(self, value: object) -> bool: + return isinstance(value, Custom) and self.value == value.value + + def default(obj): + if isinstance(obj, Custom): + return msgpack.ExtType(1, obj.value) + raise TypeError('Unknown type') + + data = {'custom': Custom(b'custom_data'), 'key': 'value'} + p = msgpack_packet.MsgPackPacket.configure(dumps_default=default)( + data=data) + p2 = msgpack_packet.MsgPackPacket(encoded_packet=p.encode()) + assert p.packet_type == p2.packet_type + assert p.id == p2.id + assert p.namespace == p2.namespace + assert p.data != p2.data + + assert isinstance(p2.data, dict) + assert 'custom' in p2.data + assert isinstance(p2.data['custom'], msgpack.ExtType) + assert p2.data['custom'].code == 1 + assert p2.data['custom'].data == b'custom_data' + + data.pop('custom') + p2_data_without_custom = p2.data.copy() + p2_data_without_custom.pop('custom') + assert data == p2_data_without_custom diff --git a/tests/common/test_server.py b/tests/common/test_server.py index bdbbfe07..4c2c8071 100644 --- a/tests/common/test_server.py +++ b/tests/common/test_server.py @@ -1,5 +1,6 @@ import logging from unittest import mock +from datetime import datetime, timezone, timedelta from engineio import json from engineio import packet as eio_packet @@ -10,6 +11,7 @@ from socketio import namespace from socketio import packet from socketio import server +from socketio.msgpack_packet import MsgPackPacket @mock.patch('socketio.server.engineio.Server', **{ @@ -1032,3 +1034,21 @@ def test_sleep(self, eio): s = server.Server() s.sleep(1.23) s.eio.sleep.assert_called_once_with(1.23) + + def test_serializer_args_with_msgpack(self, eio): + def default(o): + if isinstance(o, datetime): + return o.isoformat() + raise TypeError("Unknown type") + + data = {"current": datetime.now(timezone(timedelta(0)))} + s = server.Server( + serializer=MsgPackPacket.configure(dumps_default=default)) + p = s.packet_class(data=data) + p2 = s.packet_class(encoded_packet=p.encode()) + + assert p.data != p2.data + assert isinstance(p2.data, dict) + assert "current" in p2.data + assert isinstance(p2.data["current"], str) + assert default(data["current"]) == p2.data["current"] From 1e903e173a2d7b04599c4f7f9630c1abbb531fad Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 22 Nov 2025 12:21:54 +0000 Subject: [PATCH 555/571] Retry initial Redis connection (Fixes #1534) (#1536) --- src/socketio/async_redis_manager.py | 20 ++++++++++---------- src/socketio/redis_manager.py | 20 ++++++++++---------- tests/async/test_redis_manager.py | 16 ++++++++++------ tests/common/test_redis_manager.py | 16 ++++++++++------ 4 files changed, 40 insertions(+), 32 deletions(-) diff --git a/src/socketio/async_redis_manager.py b/src/socketio/async_redis_manager.py index b8ac4a0f..1cb7fb9d 100644 --- a/src/socketio/async_redis_manager.py +++ b/src/socketio/async_redis_manager.py @@ -61,7 +61,9 @@ def __init__(self, url='redis://localhost:6379/0', channel='socketio', super().__init__(channel=channel, write_only=write_only, logger=logger) self.redis_url = url self.redis_options = redis_options or {} - self._redis_connect() + self.connected = False + self.redis = None + self.pubsub = None def _get_redis_module_and_error(self): parsed_url = urlparse(self.redis_url) @@ -106,23 +108,23 @@ def _redis_connect(self): self.redis = module.Redis.from_url(self.redis_url, **self.redis_options) self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True) + self.connected = True async def _publish(self, data): # pragma: no cover - retry = True _, error = self._get_redis_module_and_error() - while True: + for retries_left in range(1, -1, -1): # 2 attempts try: - if not retry: + if not self.connected: self._redis_connect() return await self.redis.publish( self.channel, json.dumps(data)) except error as exc: - if retry: + if retries_left > 0: self._get_logger().error( 'Cannot publish to redis... ' 'retrying', extra={"redis_exception": str(exc)}) - retry = False + self.connected = False else: self._get_logger().error( 'Cannot publish to redis... ' @@ -133,11 +135,10 @@ async def _publish(self, data): # pragma: no cover async def _redis_listen_with_retries(self): # pragma: no cover retry_sleep = 1 - connect = False _, error = self._get_redis_module_and_error() while True: try: - if connect: + if not self.connected: self._redis_connect() await self.pubsub.subscribe(self.channel) retry_sleep = 1 @@ -148,7 +149,7 @@ async def _redis_listen_with_retries(self): # pragma: no cover 'retrying in ' f'{retry_sleep} secs', extra={"redis_exception": str(exc)}) - connect = True + self.connected = False await asyncio.sleep(retry_sleep) retry_sleep *= 2 if retry_sleep > 60: @@ -156,7 +157,6 @@ async def _redis_listen_with_retries(self): # pragma: no cover async def _listen(self): # pragma: no cover channel = self.channel.encode('utf-8') - await self.pubsub.subscribe(self.channel) async for message in self._redis_listen_with_retries(): if message['channel'] == channel and \ message['type'] == 'message' and 'data' in message: diff --git a/src/socketio/redis_manager.py b/src/socketio/redis_manager.py index 5e58ef41..827918b7 100644 --- a/src/socketio/redis_manager.py +++ b/src/socketio/redis_manager.py @@ -83,7 +83,9 @@ def __init__(self, url='redis://localhost:6379/0', channel='socketio', super().__init__(channel=channel, write_only=write_only, logger=logger) self.redis_url = url self.redis_options = redis_options or {} - self._redis_connect() + self.connected = False + self.redis = None + self.pubsub = None def initialize(self): # pragma: no cover super().initialize() @@ -143,22 +145,22 @@ def _redis_connect(self): self.redis = module.Redis.from_url(self.redis_url, **self.redis_options) self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True) + self.connected = True def _publish(self, data): # pragma: no cover - retry = True _, error = self._get_redis_module_and_error() - while True: + for retries_left in range(1, -1, -1): # 2 attempts try: - if not retry: + if not self.connected: self._redis_connect() return self.redis.publish(self.channel, json.dumps(data)) except error as exc: - if retry: + if retries_left > 0: logger.error( 'Cannot publish to redis... retrying', extra={"redis_exception": str(exc)} ) - retry = False + self.connected = False else: logger.error( 'Cannot publish to redis... giving up', @@ -168,11 +170,10 @@ def _publish(self, data): # pragma: no cover def _redis_listen_with_retries(self): # pragma: no cover retry_sleep = 1 - connect = False _, error = self._get_redis_module_and_error() while True: try: - if connect: + if not self.connected: self._redis_connect() self.pubsub.subscribe(self.channel) retry_sleep = 1 @@ -181,7 +182,7 @@ def _redis_listen_with_retries(self): # pragma: no cover logger.error('Cannot receive from redis... ' f'retrying in {retry_sleep} secs', extra={"redis_exception": str(exc)}) - connect = True + self.connected = False time.sleep(retry_sleep) retry_sleep *= 2 if retry_sleep > 60: @@ -189,7 +190,6 @@ def _redis_listen_with_retries(self): # pragma: no cover def _listen(self): # pragma: no cover channel = self.channel.encode('utf-8') - self.pubsub.subscribe(self.channel) for message in self._redis_listen_with_retries(): if message['channel'] == channel and \ message['type'] == 'message' and 'data' in message: diff --git a/tests/async/test_redis_manager.py b/tests/async/test_redis_manager.py index 01c0c375..046c26d2 100644 --- a/tests/async/test_redis_manager.py +++ b/tests/async/test_redis_manager.py @@ -12,7 +12,7 @@ def test_redis_not_installed(self): async_redis_manager.aioredis = None with pytest.raises(RuntimeError): - AsyncRedisManager('redis://') + AsyncRedisManager('redis://')._redis_connect() assert AsyncRedisManager('unix:///var/sock/redis.sock') is not None async_redis_manager.aioredis = saved_redis @@ -22,7 +22,7 @@ def test_valkey_not_installed(self): async_redis_manager.aiovalkey = None with pytest.raises(RuntimeError): - AsyncRedisManager('valkey://') + AsyncRedisManager('valkey://')._redis_connect() assert AsyncRedisManager('unix:///var/sock/redis.sock') is not None async_redis_manager.aiovalkey = saved_valkey @@ -34,18 +34,18 @@ def test_redis_valkey_not_installed(self): async_redis_manager.aiovalkey = None with pytest.raises(RuntimeError): - AsyncRedisManager('redis://') + AsyncRedisManager('redis://')._redis_connect() with pytest.raises(RuntimeError): - AsyncRedisManager('valkey://') + AsyncRedisManager('valkey://')._redis_connect() with pytest.raises(RuntimeError): - AsyncRedisManager('unix:///var/sock/redis.sock') + AsyncRedisManager('unix:///var/sock/redis.sock')._redis_connect() async_redis_manager.aioredis = saved_redis async_redis_manager.aiovalkey = saved_valkey def test_bad_url(self): with pytest.raises(ValueError): - AsyncRedisManager('http://localhost:6379') + AsyncRedisManager('http://localhost:6379')._redis_connect() def test_redis_connect(self): urls = [ @@ -72,6 +72,8 @@ def test_redis_connect(self): ] for url in urls: c = AsyncRedisManager(url) + assert c.redis is None + c._redis_connect() assert isinstance(c.redis, redis.asyncio.Redis) def test_valkey_connect(self): @@ -102,6 +104,8 @@ def test_valkey_connect(self): ] for url in urls: c = AsyncRedisManager(url) + assert c.redis is None + c._redis_connect() assert isinstance(c.redis, valkey.asyncio.Valkey) async_redis_manager.aioredis = saved_redis diff --git a/tests/common/test_redis_manager.py b/tests/common/test_redis_manager.py index 3beadf3b..dbc927e4 100644 --- a/tests/common/test_redis_manager.py +++ b/tests/common/test_redis_manager.py @@ -12,7 +12,7 @@ def test_redis_not_installed(self): redis_manager.redis = None with pytest.raises(RuntimeError): - RedisManager('redis://') + RedisManager('redis://')._redis_connect() assert RedisManager('unix:///var/sock/redis.sock') is not None redis_manager.redis = saved_redis @@ -22,7 +22,7 @@ def test_valkey_not_installed(self): redis_manager.valkey = None with pytest.raises(RuntimeError): - RedisManager('valkey://') + RedisManager('valkey://')._redis_connect() assert RedisManager('unix:///var/sock/redis.sock') is not None redis_manager.valkey = saved_valkey @@ -34,18 +34,18 @@ def test_redis_valkey_not_installed(self): redis_manager.valkey = None with pytest.raises(RuntimeError): - RedisManager('redis://') + RedisManager('redis://')._redis_connect() with pytest.raises(RuntimeError): - RedisManager('valkey://') + RedisManager('valkey://')._redis_connect() with pytest.raises(RuntimeError): - RedisManager('unix:///var/sock/redis.sock') + RedisManager('unix:///var/sock/redis.sock')._redis_connect() redis_manager.redis = saved_redis redis_manager.valkey = saved_valkey def test_bad_url(self): with pytest.raises(ValueError): - RedisManager('http://localhost:6379') + RedisManager('http://localhost:6379')._redis_connect() def test_redis_connect(self): urls = [ @@ -72,6 +72,8 @@ def test_redis_connect(self): ] for url in urls: c = RedisManager(url) + assert c.redis is None + c._redis_connect() assert isinstance(c.redis, redis.Redis) def test_valkey_connect(self): @@ -102,6 +104,8 @@ def test_valkey_connect(self): ] for url in urls: c = RedisManager(url) + assert c.redis is None + c._redis_connect() assert isinstance(c.redis, valkey.Valkey) redis_manager.redis = saved_redis From 5e898a9b93526e6e667767e54c60f4c84589989d Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 22 Nov 2025 18:15:59 +0000 Subject: [PATCH 556/571] Retry initial Redis connection (Fixes #1534) --- src/socketio/async_redis_manager.py | 7 ++++--- src/socketio/redis_manager.py | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/socketio/async_redis_manager.py b/src/socketio/async_redis_manager.py index 1cb7fb9d..a43807ca 100644 --- a/src/socketio/async_redis_manager.py +++ b/src/socketio/async_redis_manager.py @@ -134,11 +134,12 @@ async def _publish(self, data): # pragma: no cover break async def _redis_listen_with_retries(self): # pragma: no cover - retry_sleep = 1 _, error = self._get_redis_module_and_error() + retry_sleep = 1 + subscribed = False while True: try: - if not self.connected: + if not subscribed: self._redis_connect() await self.pubsub.subscribe(self.channel) retry_sleep = 1 @@ -149,7 +150,7 @@ async def _redis_listen_with_retries(self): # pragma: no cover 'retrying in ' f'{retry_sleep} secs', extra={"redis_exception": str(exc)}) - self.connected = False + subscribed = False await asyncio.sleep(retry_sleep) retry_sleep *= 2 if retry_sleep > 60: diff --git a/src/socketio/redis_manager.py b/src/socketio/redis_manager.py index 827918b7..4a9d69d1 100644 --- a/src/socketio/redis_manager.py +++ b/src/socketio/redis_manager.py @@ -169,11 +169,12 @@ def _publish(self, data): # pragma: no cover break def _redis_listen_with_retries(self): # pragma: no cover - retry_sleep = 1 _, error = self._get_redis_module_and_error() + retry_sleep = 1 + subscribed = False while True: try: - if not self.connected: + if not subscribed: self._redis_connect() self.pubsub.subscribe(self.channel) retry_sleep = 1 @@ -182,7 +183,7 @@ def _redis_listen_with_retries(self): # pragma: no cover logger.error('Cannot receive from redis... ' f'retrying in {retry_sleep} secs', extra={"redis_exception": str(exc)}) - self.connected = False + subscribed = False time.sleep(retry_sleep) retry_sleep *= 2 if retry_sleep > 60: From a63007d17ad9beb6e4cd1da0d906ad9bea76d3ad Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 22 Nov 2025 18:38:45 +0000 Subject: [PATCH 557/571] Release 5.15.0 --- CHANGES.md | 9 +++++++++ pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index fe2db445..b384da0e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,14 @@ # python-socketio change log +**Release 5.15.0** - 2025-11-22 + +- Retry initial Redis connection [#1534](https://github.com/miguelgrinberg/python-socketio/issues/1536) ([commit #1](https://github.com/miguelgrinberg/python-socketio/commit/1e903e173a2d7b04599c4f7f9630c1abbb531fad) [commit #2](https://github.com/miguelgrinberg/python-socketio/commit/5e898a9b93526e6e667767e54c60f4c84589989d)) +- Correctly regenerate RabbitMQ binding after a connection failure [#1516](https://github.com/miguelgrinberg/python-socketio/issues/1516) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c52e93b4a328d98a968bfbdec0cfd598b73ee913)) (thanks **Gritty_dev**!) +- Support `ext_type` in the `MsgPackPacket` class [#1521](https://github.com/miguelgrinberg/python-socketio/issues/1521) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/208925344a48485d2cd56e40eb74266c3bcb5311)) +- Support sending `bytesarray`s when using pub/sub managers ([commit](https://github.com/miguelgrinberg/python-socketio/commit/6c9b9974f72e2efdf62407ecab24ee6995448098)) +- Fix typos in documentation [#1520](https://github.com/miguelgrinberg/python-socketio/issues/1520) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/db3f1c2a0105c30cb833ddfca8f05fe4320468fd)) (thanks **Lê Nam Khánh**!) +- Improvements to the logging documentation ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b423d0e38eef559b7e81acb7e32059de305f982c)) + **Release 5.14.3** - 2025-10-29 - Support Python's native `ConnectionRefusedError` exception to reject a connection [#1515](https://github.com/miguelgrinberg/python-socketio/issues/1515) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/f3b18bde3f16437b223491d4c3e440ea37105fe3)) diff --git a/pyproject.toml b/pyproject.toml index f444d74d..038fabb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.14.4.dev0" +version = "5.15.0" license = {text = "MIT"} authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, From 692dca4747798e81f4505d4c1cbfe3ab9490368f Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 22 Nov 2025 18:50:22 +0000 Subject: [PATCH 558/571] Version 5.15.1.dev0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 038fabb7..a1c13910 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.15.0" +version = "5.15.1.dev0" license = {text = "MIT"} authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, From 91553c1ac22b9a0cf7fe3d2205e0c011f8c8df12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 17:23:54 +0000 Subject: [PATCH 559/571] Bump django in /examples/server/wsgi/django_socketio (#1538) #nolog Bumps [django](https://github.com/django/django) from 4.2.26 to 4.2.27. - [Commits](https://github.com/django/django/compare/4.2.26...4.2.27) --- updated-dependencies: - dependency-name: django dependency-version: 4.2.27 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/wsgi/django_socketio/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/wsgi/django_socketio/requirements.txt b/examples/server/wsgi/django_socketio/requirements.txt index 065a4405..e2e8f50c 100644 --- a/examples/server/wsgi/django_socketio/requirements.txt +++ b/examples/server/wsgi/django_socketio/requirements.txt @@ -1,6 +1,6 @@ asgiref==3.6.0 bidict==0.22.1 -Django==4.2.26 +Django==4.2.27 gunicorn==23.0.0 h11==0.16.0 python-engineio From c279f26bb8c9887c4ca99d4d81ad331c4844438c Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 14 Dec 2025 19:35:44 +0000 Subject: [PATCH 560/571] Support multiple arguments via pubsub emits (Fixes #1539) (#1540) --- src/socketio/async_pubsub_manager.py | 9 ++ src/socketio/pubsub_manager.py | 9 ++ tests/async/test_pubsub_manager.py | 217 +++++++++++++++++++++++++-- tests/common/test_pubsub_manager.py | 207 +++++++++++++++++++++++-- 4 files changed, 422 insertions(+), 20 deletions(-) diff --git a/src/socketio/async_pubsub_manager.py b/src/socketio/async_pubsub_manager.py index 1bfcf9dc..6d631640 100644 --- a/src/socketio/async_pubsub_manager.py +++ b/src/socketio/async_pubsub_manager.py @@ -66,6 +66,10 @@ async def emit(self, event, data, namespace=None, room=None, skip_sid=None, callback = (room, namespace, id) else: callback = None + if isinstance(data, tuple): + data = list(data) + else: + data = [data] binary = Packet.data_is_binary(data) if binary: data, attachments = Packet.deconstruct_binary(data) @@ -155,6 +159,11 @@ async def _handle_emit(self, message): if message.get('binary'): attachments = [base64.b64decode(a) for a in data[1:]] data = Packet.reconstruct_binary(data[0], attachments) + if isinstance(data, list): + if len(data) == 1: + data = data[0] + else: + data = tuple(data) await super().emit(message['event'], data, namespace=message.get('namespace'), room=message.get('room'), diff --git a/src/socketio/pubsub_manager.py b/src/socketio/pubsub_manager.py index 3528d228..4254d4af 100644 --- a/src/socketio/pubsub_manager.py +++ b/src/socketio/pubsub_manager.py @@ -63,6 +63,10 @@ def emit(self, event, data, namespace=None, room=None, skip_sid=None, callback = (room, namespace, id) else: callback = None + if isinstance(data, tuple): + data = list(data) + else: + data = [data] binary = Packet.data_is_binary(data) if binary: data, attachments = Packet.deconstruct_binary(data) @@ -151,6 +155,11 @@ def _handle_emit(self, message): if message.get('binary'): attachments = [base64.b64decode(a) for a in data[1:]] data = Packet.reconstruct_binary(data[0], attachments) + if isinstance(data, list): + if len(data) == 1: + data = data[0] + else: + data = tuple(data) super().emit(message['event'], data, namespace=message.get('namespace'), room=message.get('room'), diff --git a/tests/async/test_pubsub_manager.py b/tests/async/test_pubsub_manager.py index f9cfb7eb..abf41a24 100644 --- a/tests/async/test_pubsub_manager.py +++ b/tests/async/test_pubsub_manager.py @@ -58,7 +58,7 @@ async def test_emit(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -74,7 +74,7 @@ async def test_emit_binary(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'], + 'data': [[{'_placeholder': True, 'num': 0}], 'YmFy'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -88,7 +88,7 @@ async def test_emit_binary(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'], + 'data': [[{'foo': {'_placeholder': True, 'num': 0}}], 'YmFy'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -104,7 +104,7 @@ async def test_emit_bytearray(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'], + 'data': [[{'_placeholder': True, 'num': 0}], 'YmFy'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -118,7 +118,87 @@ async def test_emit_bytearray(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'], + 'data': [[{'foo': {'_placeholder': True, 'num': 0}}], 'YmFy'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + + async def test_emit_list(self): + await self.pm.emit('foo', [1, 'two']) + self.pm._publish.assert_awaited_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': False, + 'data': [[1, 'two']], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + await self.pm.emit('foo', [1, b'two', 'three']) + self.pm._publish.assert_awaited_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [ + [[1, {'_placeholder': True, 'num': 0}, 'three']], 'dHdv', + ], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + + async def test_emit_no_arguments(self): + await self.pm.emit('foo', ()) + self.pm._publish.assert_awaited_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': False, + 'data': [], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + + async def test_emit_multiple_arguments(self): + await self.pm.emit('foo', (1, 'two')) + self.pm._publish.assert_awaited_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': False, + 'data': [1, 'two'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + await self.pm.emit('foo', (1, b'two', 'three')) + self.pm._publish.assert_awaited_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [ + [1, {'_placeholder': True, 'num': 0}, 'three'], 'dHdv', + ], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -135,7 +215,7 @@ async def test_emit_with_to(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': sid, 'skip_sid': None, @@ -151,7 +231,7 @@ async def test_emit_with_namespace(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/baz', 'room': None, 'skip_sid': None, @@ -167,7 +247,7 @@ async def test_emit_with_room(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': 'baz', 'skip_sid': None, @@ -183,7 +263,7 @@ async def test_emit_with_skip_sid(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': None, 'skip_sid': 'baz', @@ -202,7 +282,7 @@ async def test_emit_with_callback(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': 'baz', 'skip_sid': None, @@ -294,6 +374,20 @@ async def test_close_room_with_namespace(self): ) async def test_handle_emit(self): + with mock.patch.object( + async_manager.AsyncManager, 'emit' + ) as super_emit: + await self.pm._handle_emit({'event': 'foo', 'data': ['bar']}) + super_emit.assert_awaited_once_with( + 'foo', + 'bar', + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + + async def test_handle_legacy_emit(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: @@ -308,6 +402,37 @@ async def test_handle_emit(self): ) async def test_handle_emit_binary(self): + with mock.patch.object( + async_manager.AsyncManager, 'emit' + ) as super_emit: + await self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [[{'_placeholder': True, 'num': 0}], 'YmFy'], + }) + super_emit.assert_awaited_once_with( + 'foo', + b'bar', + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + await self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [[{'foo': {'_placeholder': True, 'num': 0}}], 'YmFy'], + }) + super_emit.assert_awaited_with( + 'foo', + {'foo': b'bar'}, + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + + async def test_handle_legacy_emit_binary(self): with mock.patch.object( async_manager.AsyncManager, 'emit' ) as super_emit: @@ -338,6 +463,78 @@ async def test_handle_emit_binary(self): callback=None, ) + async def test_handle_emit_list(self): + with mock.patch.object( + async_manager.AsyncManager, 'emit' + ) as super_emit: + await self.pm._handle_emit({'event': 'foo', 'data': [[1, 'two']]}) + super_emit.assert_awaited_once_with( + 'foo', + [1, 'two'], + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + await self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [ + [[1, {'_placeholder': True, 'num': 0}, 'three']], 'dHdv' + ] + }) + super_emit.assert_awaited_with( + 'foo', + [1, b'two', 'three'], + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + + async def test_handle_emit_no_arguments(self): + with mock.patch.object( + async_manager.AsyncManager, 'emit' + ) as super_emit: + await self.pm._handle_emit({'event': 'foo', 'data': []}) + super_emit.assert_awaited_once_with( + 'foo', + (), + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + + async def test_handle_emit_multiple_arguments(self): + with mock.patch.object( + async_manager.AsyncManager, 'emit' + ) as super_emit: + await self.pm._handle_emit({'event': 'foo', 'data': [1, 'two']}) + super_emit.assert_awaited_once_with( + 'foo', + (1, 'two'), + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + await self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [ + [1, {'_placeholder': True, 'num': 0}, 'three'], 'dHdv' + ] + }) + super_emit.assert_awaited_with( + 'foo', + (1, b'two', 'three'), + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + async def test_handle_emit_with_namespace(self): with mock.patch.object( async_manager.AsyncManager, 'emit' diff --git a/tests/common/test_pubsub_manager.py b/tests/common/test_pubsub_manager.py index abef2bf2..91b77854 100644 --- a/tests/common/test_pubsub_manager.py +++ b/tests/common/test_pubsub_manager.py @@ -70,7 +70,7 @@ def test_emit(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -86,7 +86,7 @@ def test_emit_binary(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'], + 'data': [[{'_placeholder': True, 'num': 0}], 'YmFy'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -100,7 +100,7 @@ def test_emit_binary(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'], + 'data': [[{'foo': {'_placeholder': True, 'num': 0}}], 'YmFy'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -116,7 +116,7 @@ def test_emit_bytearray(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'], + 'data': [[{'_placeholder': True, 'num': 0}], 'YmFy'], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -130,7 +130,87 @@ def test_emit_bytearray(self): 'method': 'emit', 'event': 'foo', 'binary': True, - 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'], + 'data': [[{'foo': {'_placeholder': True, 'num': 0}}], 'YmFy'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + + def test_emit_list(self): + self.pm.emit('foo', [1, 'two']) + self.pm._publish.assert_called_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': False, + 'data': [[1, 'two']], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + self.pm.emit('foo', [1, b'two', 'three']) + self.pm._publish.assert_called_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [ + [[1, {'_placeholder': True, 'num': 0}, 'three']], 'dHdv', + ], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + + def test_emit_no_arguments(self): + self.pm.emit('foo', ()) + self.pm._publish.assert_called_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': False, + 'data': [], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + + def test_emit_multiple_arguments(self): + self.pm.emit('foo', (1, 'two')) + self.pm._publish.assert_called_once_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': False, + 'data': [1, 'two'], + 'namespace': '/', + 'room': None, + 'skip_sid': None, + 'callback': None, + 'host_id': '123456', + } + ) + self.pm.emit('foo', (1, b'two', 'three')) + self.pm._publish.assert_called_with( + { + 'method': 'emit', + 'event': 'foo', + 'binary': True, + 'data': [ + [1, {'_placeholder': True, 'num': 0}, 'three'], 'dHdv', + ], 'namespace': '/', 'room': None, 'skip_sid': None, @@ -147,7 +227,7 @@ def test_emit_with_to(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': sid, 'skip_sid': None, @@ -163,7 +243,7 @@ def test_emit_with_namespace(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/baz', 'room': None, 'skip_sid': None, @@ -179,7 +259,7 @@ def test_emit_with_room(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': 'baz', 'skip_sid': None, @@ -195,7 +275,7 @@ def test_emit_with_skip_sid(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': None, 'skip_sid': 'baz', @@ -214,7 +294,7 @@ def test_emit_with_callback(self): 'method': 'emit', 'event': 'foo', 'binary': False, - 'data': 'bar', + 'data': ['bar'], 'namespace': '/', 'room': 'baz', 'skip_sid': None, @@ -305,6 +385,18 @@ def test_close_room_with_namespace(self): ) def test_handle_emit(self): + with mock.patch.object(manager.Manager, 'emit') as super_emit: + self.pm._handle_emit({'event': 'foo', 'data': ['bar']}) + super_emit.assert_called_once_with( + 'foo', + 'bar', + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + + def test_handle_legacy_emit(self): with mock.patch.object(manager.Manager, 'emit') as super_emit: self.pm._handle_emit({'event': 'foo', 'data': 'bar'}) super_emit.assert_called_once_with( @@ -317,6 +409,35 @@ def test_handle_emit(self): ) def test_handle_emit_binary(self): + with mock.patch.object(manager.Manager, 'emit') as super_emit: + self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [[{'_placeholder': True, 'num': 0}], 'YmFy'], + }) + super_emit.assert_called_once_with( + 'foo', + b'bar', + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [[{'foo': {'_placeholder': True, 'num': 0}}], 'YmFy'], + }) + super_emit.assert_called_with( + 'foo', + {'foo': b'bar'}, + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + + def test_handle_legacy_emit_binary(self): with mock.patch.object(manager.Manager, 'emit') as super_emit: self.pm._handle_emit({ 'event': 'foo', @@ -345,6 +466,72 @@ def test_handle_emit_binary(self): callback=None, ) + def test_handle_emit_list(self): + with mock.patch.object(manager.Manager, 'emit') as super_emit: + self.pm._handle_emit({'event': 'foo', 'data': [[1, 'two']]}) + super_emit.assert_called_once_with( + 'foo', + [1, 'two'], + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [ + [[1, {'_placeholder': True, 'num': 0}, 'three']], 'dHdv' + ], + }) + super_emit.assert_called_with( + 'foo', + [1, b'two', 'three'], + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + + def test_handle_emit_no_arguments(self): + with mock.patch.object(manager.Manager, 'emit') as super_emit: + self.pm._handle_emit({'event': 'foo', 'data': []}) + super_emit.assert_called_once_with( + 'foo', + (), + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + + def test_handle_emit_multiple_arguments(self): + with mock.patch.object(manager.Manager, 'emit') as super_emit: + self.pm._handle_emit({'event': 'foo', 'data': [1, 'two']}) + super_emit.assert_called_once_with( + 'foo', + (1, 'two'), + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + self.pm._handle_emit({ + 'event': 'foo', + 'binary': True, + 'data': [ + [1, {'_placeholder': True, 'num': 0}, 'three'], 'dHdv' + ], + }) + super_emit.assert_called_with( + 'foo', + (1, b'two', 'three'), + namespace=None, + room=None, + skip_sid=None, + callback=None, + ) + def test_handle_emit_with_namespace(self): with mock.patch.object(manager.Manager, 'emit') as super_emit: self.pm._handle_emit( From c6eaca07bb70b42dc6dea5f03628a8df3b5a9ff7 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 16 Dec 2025 23:42:01 +0000 Subject: [PATCH 561/571] Release 5.15.1 --- CHANGES.md | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index b384da0e..2ec98f1c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ # python-socketio change log +**Release 5.15.1** - 2025-12-16 + +- Restore support multiple arguments via pubsub emits [#1540](https://github.com/miguelgrinberg/python-socketio/issues/1540) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c279f26bb8c9887c4ca99d4d81ad331c4844438c)) + **Release 5.15.0** - 2025-11-22 - Retry initial Redis connection [#1534](https://github.com/miguelgrinberg/python-socketio/issues/1536) ([commit #1](https://github.com/miguelgrinberg/python-socketio/commit/1e903e173a2d7b04599c4f7f9630c1abbb531fad) [commit #2](https://github.com/miguelgrinberg/python-socketio/commit/5e898a9b93526e6e667767e54c60f4c84589989d)) diff --git a/pyproject.toml b/pyproject.toml index a1c13910..a8f1833e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.15.1.dev0" +version = "5.15.1" license = {text = "MIT"} authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, From 412a6cf84960649d26d11f4aa33176bd94dbc213 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 16 Dec 2025 23:48:42 +0000 Subject: [PATCH 562/571] Version 5.15.2.dev0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a8f1833e..ff9e27d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.15.1" +version = "5.15.2.dev0" license = {text = "MIT"} authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, From b235699d9b06564753c570b76055997e9d62a938 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 24 Dec 2025 23:40:33 +0000 Subject: [PATCH 563/571] Address deprecations --- src/socketio/async_admin.py | 3 ++- src/socketio/async_client.py | 7 ++++--- src/socketio/async_manager.py | 3 ++- src/socketio/async_namespace.py | 5 +++-- src/socketio/async_server.py | 3 ++- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/socketio/async_admin.py b/src/socketio/async_admin.py index b052d8fe..b426b71e 100644 --- a/src/socketio/async_admin.py +++ b/src/socketio/async_admin.py @@ -1,6 +1,7 @@ import asyncio from datetime import datetime, timezone import functools +import inspect import os import socket import time @@ -124,7 +125,7 @@ async def admin_connect(self, sid, environ, client_auth): elif isinstance(self.auth, list): authenticated = client_auth in self.auth else: - if asyncio.iscoroutinefunction(self.auth): + if inspect.iscoroutinefunction(self.auth): authenticated = await self.auth(client_auth) else: authenticated = self.auth(client_auth) diff --git a/src/socketio/async_client.py b/src/socketio/async_client.py index ead963d0..49241ed0 100644 --- a/src/socketio/async_client.py +++ b/src/socketio/async_client.py @@ -1,4 +1,5 @@ import asyncio +import inspect import logging import random @@ -375,7 +376,7 @@ async def _get_real_value(self, value): callables.""" if not callable(value): return value - if asyncio.iscoroutinefunction(value): + if inspect.iscoroutinefunction(value): return await value() return value() @@ -437,7 +438,7 @@ async def _handle_ack(self, namespace, id, data): else: del self.callbacks[namespace][id] if callback is not None: - if asyncio.iscoroutinefunction(callback): + if inspect.iscoroutinefunction(callback): await callback(*data) else: callback(*data) @@ -464,7 +465,7 @@ async def _trigger_event(self, event, namespace, *args): # first see if we have an explicit handler for the event handler, args = self._get_event_handler(event, namespace, args) if handler: - if asyncio.iscoroutinefunction(handler): + if inspect.iscoroutinefunction(handler): try: try: ret = await handler(*args) diff --git a/src/socketio/async_manager.py b/src/socketio/async_manager.py index 47e7a79f..19028b04 100644 --- a/src/socketio/async_manager.py +++ b/src/socketio/async_manager.py @@ -1,4 +1,5 @@ import asyncio +import inspect from engineio import packet as eio_packet from socketio import packet @@ -113,7 +114,7 @@ async def trigger_callback(self, sid, id, data): del self.callbacks[sid][id] if callback is not None: ret = callback(*data) - if asyncio.iscoroutine(ret): + if inspect.iscoroutine(ret): try: await ret except asyncio.CancelledError: # pragma: no cover diff --git a/src/socketio/async_namespace.py b/src/socketio/async_namespace.py index 42d65089..4035e336 100644 --- a/src/socketio/async_namespace.py +++ b/src/socketio/async_namespace.py @@ -1,4 +1,5 @@ import asyncio +import inspect from socketio import base_namespace @@ -32,7 +33,7 @@ async def trigger_event(self, event, *args): handler_name = 'on_' + (event or '') if hasattr(self, handler_name): handler = getattr(self, handler_name) - if asyncio.iscoroutinefunction(handler) is True: + if inspect.iscoroutinefunction(handler) is True: try: try: ret = await handler(*args) @@ -213,7 +214,7 @@ async def trigger_event(self, event, *args): handler_name = 'on_' + (event or '') if hasattr(self, handler_name): handler = getattr(self, handler_name) - if asyncio.iscoroutinefunction(handler) is True: + if inspect.iscoroutinefunction(handler) is True: try: try: ret = await handler(*args) diff --git a/src/socketio/async_server.py b/src/socketio/async_server.py index 6c9e3ca3..e13009ef 100644 --- a/src/socketio/async_server.py +++ b/src/socketio/async_server.py @@ -1,4 +1,5 @@ import asyncio +import inspect import engineio @@ -637,7 +638,7 @@ async def _trigger_event(self, event, namespace, *args): # first see if we have an explicit handler for the event handler, args = self._get_event_handler(event, namespace, args) if handler: - if asyncio.iscoroutinefunction(handler): + if inspect.iscoroutinefunction(handler): try: try: ret = await handler(*args) From d0728d2f74538762dd551fa9cd0cd1fd5aedfa37 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 24 Dec 2025 23:40:42 +0000 Subject: [PATCH 564/571] Drop Python 3.8 and 3.9 from CI --- .github/workflows/tests.yml | 2 +- tox.ini | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6a8433e4..99ad78e2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: [windows-latest, macos-latest, ubuntu-latest] - python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14', 'pypy-3.11'] + python: ['3.10', '3.11', '3.12', '3.13', '3.14', 'pypy-3.11'] fail-fast: false runs-on: ${{ matrix.os }} steps: diff --git a/tox.ini b/tox.ini index b73e4bbc..02297a2c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,9 @@ [tox] -envlist=flake8,py{38,39,310,311,312,313,314},docs +envlist=flake8,py{310,311,312,313,314},docs skip_missing_interpreters=True [gh-actions] python = - 3.8: py38 - 3.9: py39 3.10: py310 3.11: py311 3.12: py312 From 9e624b38c863efaacd104f2c0dd7125e12c8cac2 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 24 Dec 2025 23:51:21 +0000 Subject: [PATCH 565/571] Release 5.16.0 --- CHANGES.md | 5 +++++ pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 2ec98f1c..7e97e423 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ # python-socketio change log +**Release 5.16.0** - 2025-12-24 + +- Address deprecation warnings ([commit](https://github.com/miguelgrinberg/python-socketio/commit/b235699d9b06564753c570b76055997e9d62a938)) +- Drop Python 3.8 and 3.9 from CI builds ([commit](https://github.com/miguelgrinberg/python-socketio/commit/d0728d2f74538762dd551fa9cd0cd1fd5aedfa37)) + **Release 5.15.1** - 2025-12-16 - Restore support multiple arguments via pubsub emits [#1540](https://github.com/miguelgrinberg/python-socketio/issues/1540) ([commit](https://github.com/miguelgrinberg/python-socketio/commit/c279f26bb8c9887c4ca99d4d81ad331c4844438c)) diff --git a/pyproject.toml b/pyproject.toml index ff9e27d7..21f1813e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.15.2.dev0" +version = "5.16.0" license = {text = "MIT"} authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, From 2ce73481986061c54c702b89a511df32e16aa57f Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Wed, 24 Dec 2025 23:51:50 +0000 Subject: [PATCH 566/571] Version 5.16.1.dev0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 21f1813e..e1861764 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-socketio" -version = "5.16.0" +version = "5.16.1.dev0" license = {text = "MIT"} authors = [ { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, From e01550bda0d8963540a5bac28db71edb6e57e542 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 30 Dec 2025 19:47:12 +0000 Subject: [PATCH 567/571] Fix typos in documentation #nolog (Fixes #1541) --- docs/client.rst | 2 +- docs/server.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/client.rst b/docs/client.rst index 64cbec88..1e23c749 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -280,7 +280,7 @@ remaining arguments are the same as for a regular event handler. The ``connect`` and ``disconnect`` events have to be defined explicitly and are not invoked on a catch-all event handler. -Similarily, a "catch-all" namespace handler is invoked for any connected +Similarly, a "catch-all" namespace handler is invoked for any connected namespaces that do not have an explicitly defined event handler. As with catch-all events, ``'*'`` is used in place of a namespace:: diff --git a/docs/server.rst b/docs/server.rst index f31e6680..e73f14c5 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -492,7 +492,7 @@ information. Catch-All Namespaces ~~~~~~~~~~~~~~~~~~~~ -Similarily to catch-all event handlers, a "catch-all" namespace can be used +Similarly to catch-all event handlers, a "catch-all" namespace can be used when defining event handlers for any connected namespaces that do not have an explicitly defined event handler. As with catch-all events, ``'*'`` is used in place of a namespace:: From 7353b276137ec00723f747edf6d11829b85fb4e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 14:40:06 +0000 Subject: [PATCH 568/571] Bump qs and express in /examples/server/javascript (#1542) #nolog Bumps [qs](https://github.com/ljharb/qs) to 6.14.1 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `qs` from 6.13.0 to 6.14.1 - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.13.0...v6.14.1) Updates `express` from 4.21.2 to 4.22.1 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/v4.22.1/History.md) - [Commits](https://github.com/expressjs/express/compare/4.21.2...v4.22.1) --- updated-dependencies: - dependency-name: qs dependency-version: 6.14.1 dependency-type: indirect - dependency-name: express dependency-version: 4.22.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/javascript/package-lock.json | 482 ++++++++++++------- examples/server/javascript/package.json | 2 +- 2 files changed, 296 insertions(+), 188 deletions(-) diff --git a/examples/server/javascript/package-lock.json b/examples/server/javascript/package-lock.json index 4bae767a..7a3ed280 100644 --- a/examples/server/javascript/package-lock.json +++ b/examples/server/javascript/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@socket.io/admin-ui": "^0.5.1", - "express": "^4.21.2", + "express": "^4.22.1", "smoothie": "1.19.0", "socket.io": "^4.8.0", "socket.io-client": "^4.6.1" @@ -133,16 +133,25 @@ "node": ">= 0.8" } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dependencies": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -211,22 +220,6 @@ } } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -244,6 +237,19 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -306,12 +312,9 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -324,6 +327,17 @@ "node": ">= 0.4" } }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -338,38 +352,38 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -403,6 +417,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/express/node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -466,15 +494,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -483,32 +516,22 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gopd": { + "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dependencies": { - "es-define-property": "^1.0.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.4" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "engines": { "node": ">= 0.4" }, @@ -517,9 +540,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -577,6 +600,14 @@ "node": ">= 0.10" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -653,9 +684,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "engines": { "node": ">= 0.4" }, @@ -822,36 +853,71 @@ "node": ">= 0.8" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dependencies": { - "define-data-property": "^1.1.4", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -1108,16 +1174,22 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, - "call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "requires": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" } }, "content-disposition": { @@ -1160,16 +1232,6 @@ "ms": "2.1.2" } }, - "define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "requires": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - } - }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1180,6 +1242,16 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1232,18 +1304,23 @@ "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" }, "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "requires": { - "get-intrinsic": "^1.2.4" - } + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, "es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1255,38 +1332,38 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -1309,6 +1386,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "requires": { + "side-channel": "^1.1.0" + } } } }, @@ -1362,42 +1447,40 @@ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" } }, - "gopd": { + "get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "requires": { - "es-define-property": "^1.0.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" } }, - "has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "hasown": { "version": "2.0.2", @@ -1437,6 +1520,11 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1486,9 +1574,9 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" }, "on-finished": { "version": "2.4.1", @@ -1611,33 +1699,53 @@ } } }, - "set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "requires": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - } - }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "requires": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" } }, "smoothie": { diff --git a/examples/server/javascript/package.json b/examples/server/javascript/package.json index 1e0e150c..84715872 100644 --- a/examples/server/javascript/package.json +++ b/examples/server/javascript/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "dependencies": { "@socket.io/admin-ui": "^0.5.1", - "express": "^4.21.2", + "express": "^4.22.1", "smoothie": "1.19.0", "socket.io": "^4.8.0", "socket.io-client": "^4.6.1" From 174629eb8d49031eb579f842c9b9575eb09b22c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 00:44:41 +0000 Subject: [PATCH 569/571] Bump aiohttp from 3.12.14 to 3.13.3 in /examples/server/aiohttp (#1543) #nolog --- updated-dependencies: - dependency-name: aiohttp dependency-version: 3.13.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/server/aiohttp/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/aiohttp/requirements.txt b/examples/server/aiohttp/requirements.txt index aec8bf98..f1a60ed0 100644 --- a/examples/server/aiohttp/requirements.txt +++ b/examples/server/aiohttp/requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.12.14 +aiohttp==3.13.3 async-timeout==1.1.0 chardet==2.3.0 multidict==2.1.4 From 8469b69261eefc3c1cb2d08473fd255cd23fc8d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 10:00:25 +0000 Subject: [PATCH 570/571] Bump qs and express in /examples/client/javascript (#1544) #nolog Bumps [qs](https://github.com/ljharb/qs) to 6.14.1 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `qs` from 6.13.0 to 6.14.1 - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.13.0...v6.14.1) Updates `express` from 4.21.2 to 4.22.1 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/v4.22.1/History.md) - [Commits](https://github.com/expressjs/express/compare/4.21.2...v4.22.1) --- updated-dependencies: - dependency-name: qs dependency-version: 6.14.1 dependency-type: indirect - dependency-name: express dependency-version: 4.22.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/client/javascript/package-lock.json | 482 ++++++++++++------- examples/client/javascript/package.json | 2 +- 2 files changed, 296 insertions(+), 188 deletions(-) diff --git a/examples/client/javascript/package-lock.json b/examples/client/javascript/package-lock.json index 5a2ec0ca..fef1fa8f 100644 --- a/examples/client/javascript/package-lock.json +++ b/examples/client/javascript/package-lock.json @@ -8,7 +8,7 @@ "name": "socketio-examples", "version": "0.1.0", "dependencies": { - "express": "^4.21.2", + "express": "^4.22.1", "smoothie": "1.19.0", "socket.io": "^4.8.0", "socket.io-client": "^4.6.1" @@ -96,16 +96,25 @@ "node": ">= 0.8" } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dependencies": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -166,22 +175,6 @@ "ms": "2.0.0" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -199,6 +192,19 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -303,12 +309,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -321,6 +324,17 @@ "node": ">= 0.4" } }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -335,38 +349,38 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -387,6 +401,20 @@ "node": ">= 0.8" } }, + "node_modules/express/node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -437,15 +465,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -454,32 +487,22 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gopd": { + "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dependencies": { - "es-define-property": "^1.0.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.4" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "engines": { "node": ">= 0.4" }, @@ -488,9 +511,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -548,6 +571,14 @@ "node": ">= 0.10" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -624,9 +655,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "engines": { "node": ">= 0.4" }, @@ -780,36 +811,71 @@ "node": ">= 0.8" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dependencies": { - "define-data-property": "^1.1.4", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -1115,16 +1181,22 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, - "call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "requires": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" } }, "content-disposition": { @@ -1167,16 +1239,6 @@ "ms": "2.0.0" } }, - "define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "requires": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - } - }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1187,6 +1249,16 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1267,18 +1339,23 @@ "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" }, "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "requires": { - "get-intrinsic": "^1.2.4" - } + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, "es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1290,38 +1367,38 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -1331,6 +1408,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "requires": { + "side-channel": "^1.1.0" + } } } }, @@ -1371,42 +1456,40 @@ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" } }, - "gopd": { + "get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "requires": { - "es-define-property": "^1.0.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" } }, - "has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "hasown": { "version": "2.0.2", @@ -1446,6 +1529,11 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1495,9 +1583,9 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" }, "on-finished": { "version": "2.4.1", @@ -1605,33 +1693,53 @@ } } }, - "set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "requires": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - } - }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "requires": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" } }, "smoothie": { diff --git a/examples/client/javascript/package.json b/examples/client/javascript/package.json index 6bea0d87..8f1ec830 100644 --- a/examples/client/javascript/package.json +++ b/examples/client/javascript/package.json @@ -2,7 +2,7 @@ "name": "socketio-examples", "version": "0.1.0", "dependencies": { - "express": "^4.21.2", + "express": "^4.22.1", "smoothie": "1.19.0", "socket.io": "^4.8.0", "socket.io-client": "^4.6.1" From 1c2eab13a92fac9e43663eb0b5f099eb1c40ea5b Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Thu, 8 Jan 2026 19:52:30 +0000 Subject: [PATCH 571/571] Admin UI fixes: remove duplicate tasks, report transport upgrades --- src/socketio/admin.py | 6 ++++++ src/socketio/async_admin.py | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/socketio/admin.py b/src/socketio/admin.py index 12b905ea..e59da8ab 100644 --- a/src/socketio/admin.py +++ b/src/socketio/admin.py @@ -194,6 +194,8 @@ def shutdown(self): if self.stats_task: # pragma: no branch self.stop_stats_event.set() self.stats_task.join() + self.stop_stats_event.clear() + self.stats_task = None def _trigger_event(self, event, namespace, *args): t = time.time() @@ -206,6 +208,9 @@ def _trigger_event(self, event, namespace, *args): serialized_socket, datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) + if not self.sio.eio._get_socket(eio_sid).upgraded: + self.sio.start_background_task( + self._check_for_upgrade, eio_sid, sid, namespace) elif event == 'disconnect': del self.sio.manager._timestamps[sid] reason = args[1] @@ -283,6 +288,7 @@ def _emit(self, event, data, namespace, room=None, skip_sid=None, def _handle_eio_connect(self, eio_sid, environ): if self.stop_stats_event is None: self.stop_stats_event = self.sio.eio.create_event() + if self.stats_task is None: self.stats_task = self.sio.start_background_task( self._emit_server_stats) diff --git a/src/socketio/async_admin.py b/src/socketio/async_admin.py index b426b71e..9d0fc9f3 100644 --- a/src/socketio/async_admin.py +++ b/src/socketio/async_admin.py @@ -157,9 +157,6 @@ async def config(sid): namespace=self.admin_namespace) self.sio.start_background_task(config, sid) - self.stop_stats_event = self.sio.eio.create_event() - self.stats_task = self.sio.start_background_task( - self._emit_server_stats) async def admin_emit(self, _, namespace, room_filter, event, *data): await self.sio.emit(event, data, to=room_filter, namespace=namespace) @@ -183,6 +180,8 @@ async def shutdown(self): if self.stats_task: # pragma: no branch self.stop_stats_event.set() await asyncio.gather(self.stats_task) + self.stats_task = None + self.stop_stats_event.clear() async def _trigger_event(self, event, namespace, *args): t = time.time() @@ -195,6 +194,9 @@ async def _trigger_event(self, event, namespace, *args): serialized_socket, datetime.fromtimestamp(t, timezone.utc).isoformat(), ), namespace=self.admin_namespace) + if not self.sio.eio._get_socket(eio_sid).upgraded: + self.sio.start_background_task( + self._check_for_upgrade, eio_sid, sid, namespace) elif event == 'disconnect': del self.sio.manager._timestamps[sid] reason = args[1] @@ -273,6 +275,7 @@ async def _emit(self, event, data, namespace, room=None, skip_sid=None, async def _handle_eio_connect(self, eio_sid, environ): if self.stop_stats_event is None: self.stop_stats_event = self.sio.eio.create_event() + if self.stats_task is None: self.stats_task = self.sio.start_background_task( self._emit_server_stats)