python-socketio 5.14.0__tar.gz → 5.14.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_socketio-5.14.0/src/python_socketio.egg-info → python_socketio-5.14.2}/PKG-INFO +3 -1
- {python_socketio-5.14.0 → python_socketio-5.14.2}/pyproject.toml +4 -1
- {python_socketio-5.14.0 → python_socketio-5.14.2/src/python_socketio.egg-info}/PKG-INFO +3 -1
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/python_socketio.egg-info/SOURCES.txt +1 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/python_socketio.egg-info/requires.txt +3 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_client.py +2 -2
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_pubsub_manager.py +12 -2
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_redis_manager.py +27 -20
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/base_manager.py +7 -1
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/client.py +1 -1
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/packet.py +22 -18
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/pubsub_manager.py +12 -2
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/redis_manager.py +24 -18
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/test_client.py +54 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/test_pubsub_manager.py +67 -0
- python_socketio-5.14.2/tests/async/test_redis_manager.py +107 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_client.py +56 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_packet.py +16 -8
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_pubsub_manager.py +65 -0
- python_socketio-5.14.2/tests/common/test_redis_manager.py +142 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tox.ini +4 -1
- python_socketio-5.14.0/tests/common/test_redis_manager.py +0 -40
- {python_socketio-5.14.0 → python_socketio-5.14.2}/LICENSE +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/MANIFEST.in +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/README.md +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/Makefile +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/_static/README.md +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/_static/custom.css +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/api.rst +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/client.rst +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/conf.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/index.rst +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/intro.rst +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/make.bat +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/server.rst +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/setup.cfg +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/python_socketio.egg-info/dependency_links.txt +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/python_socketio.egg-info/not-zip-safe +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/python_socketio.egg-info/top_level.txt +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/__init__.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/admin.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/asgi.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_admin.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_aiopika_manager.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_manager.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_namespace.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_server.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_simple_client.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/base_client.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/base_namespace.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/base_server.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/exceptions.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/kafka_manager.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/kombu_manager.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/manager.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/middleware.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/msgpack_packet.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/namespace.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/server.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/simple_client.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/tornado.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/zmq_manager.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/__init__.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/__init__.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/test_admin.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/test_manager.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/test_namespace.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/test_server.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/test_simple_client.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/asyncio_web_server.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/__init__.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_admin.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_manager.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_middleware.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_msgpack_packet.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_namespace.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_server.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_simple_client.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/README.md +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/binary_packet.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/json_packet.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/namespace_packet.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/run.sh +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/server_receive.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/server_send.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/server_send_broadcast.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/text_packet.py +0 -0
- {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/web_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-socketio
|
|
3
|
-
Version: 5.14.
|
|
3
|
+
Version: 5.14.2
|
|
4
4
|
Summary: Socket.IO server and client for Python
|
|
5
5
|
Author-email: Miguel Grinberg <miguel.grinberg@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -20,6 +20,8 @@ Requires-Dist: requests>=2.21.0; extra == "client"
|
|
|
20
20
|
Requires-Dist: websocket-client>=0.54.0; extra == "client"
|
|
21
21
|
Provides-Extra: asyncio-client
|
|
22
22
|
Requires-Dist: aiohttp>=3.4; extra == "asyncio-client"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: tox; extra == "dev"
|
|
23
25
|
Provides-Extra: docs
|
|
24
26
|
Requires-Dist: sphinx; extra == "docs"
|
|
25
27
|
Dynamic: license-file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-socketio"
|
|
3
|
-
version = "5.14.
|
|
3
|
+
version = "5.14.2"
|
|
4
4
|
license = {text = "MIT"}
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
|
|
@@ -34,6 +34,9 @@ client = [
|
|
|
34
34
|
asyncio_client = [
|
|
35
35
|
"aiohttp >= 3.4",
|
|
36
36
|
]
|
|
37
|
+
dev = [
|
|
38
|
+
"tox",
|
|
39
|
+
]
|
|
37
40
|
docs = [
|
|
38
41
|
"sphinx",
|
|
39
42
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-socketio
|
|
3
|
-
Version: 5.14.
|
|
3
|
+
Version: 5.14.2
|
|
4
4
|
Summary: Socket.IO server and client for Python
|
|
5
5
|
Author-email: Miguel Grinberg <miguel.grinberg@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -20,6 +20,8 @@ Requires-Dist: requests>=2.21.0; extra == "client"
|
|
|
20
20
|
Requires-Dist: websocket-client>=0.54.0; extra == "client"
|
|
21
21
|
Provides-Extra: asyncio-client
|
|
22
22
|
Requires-Dist: aiohttp>=3.4; extra == "asyncio-client"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: tox; extra == "dev"
|
|
23
25
|
Provides-Extra: docs
|
|
24
26
|
Requires-Dist: sphinx; extra == "docs"
|
|
25
27
|
Dynamic: license-file
|
|
@@ -175,8 +175,8 @@ class AsyncClient(base_client.BaseClient):
|
|
|
175
175
|
if set(self.namespaces) != set(self.connection_namespaces):
|
|
176
176
|
await self.disconnect()
|
|
177
177
|
raise exceptions.ConnectionError(
|
|
178
|
-
'One or more namespaces failed to connect'
|
|
179
|
-
', '.join(self.failed_namespaces))
|
|
178
|
+
'One or more namespaces failed to connect: '
|
|
179
|
+
+ ', '.join(self.failed_namespaces))
|
|
180
180
|
|
|
181
181
|
self.connected = True
|
|
182
182
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import base64
|
|
2
3
|
from functools import partial
|
|
3
4
|
import uuid
|
|
4
5
|
|
|
5
6
|
from engineio import json
|
|
6
7
|
|
|
7
8
|
from .async_manager import AsyncManager
|
|
9
|
+
from .packet import Packet
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class AsyncPubSubManager(AsyncManager):
|
|
@@ -64,8 +66,12 @@ class AsyncPubSubManager(AsyncManager):
|
|
|
64
66
|
callback = (room, namespace, id)
|
|
65
67
|
else:
|
|
66
68
|
callback = None
|
|
69
|
+
binary = Packet.data_is_binary(data)
|
|
70
|
+
if binary:
|
|
71
|
+
data, attachments = Packet.deconstruct_binary(data)
|
|
72
|
+
data = [data, *[base64.b64encode(a).decode() for a in attachments]]
|
|
67
73
|
message = {'method': 'emit', 'event': event, 'data': data,
|
|
68
|
-
'namespace': namespace, 'room': room,
|
|
74
|
+
'binary': binary, 'namespace': namespace, 'room': room,
|
|
69
75
|
'skip_sid': skip_sid, 'callback': callback,
|
|
70
76
|
'host_id': self.host_id}
|
|
71
77
|
await self._handle_emit(message) # handle in this host
|
|
@@ -145,7 +151,11 @@ class AsyncPubSubManager(AsyncManager):
|
|
|
145
151
|
*remote_callback)
|
|
146
152
|
else:
|
|
147
153
|
callback = None
|
|
148
|
-
|
|
154
|
+
data = message['data']
|
|
155
|
+
if message.get('binary'):
|
|
156
|
+
attachments = [base64.b64decode(a) for a in data[1:]]
|
|
157
|
+
data = Packet.reconstruct_binary(data[0], attachments)
|
|
158
|
+
await super().emit(message['event'], data,
|
|
149
159
|
namespace=message.get('namespace'),
|
|
150
160
|
room=message.get('room'),
|
|
151
161
|
skip_sid=message.get('skip_sid'),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from urllib.parse import urlparse
|
|
3
3
|
|
|
4
|
-
try:
|
|
4
|
+
try:
|
|
5
5
|
from redis import asyncio as aioredis
|
|
6
6
|
from redis.exceptions import RedisError
|
|
7
7
|
except ImportError: # pragma: no cover
|
|
@@ -12,11 +12,11 @@ except ImportError: # pragma: no cover
|
|
|
12
12
|
aioredis = None
|
|
13
13
|
RedisError = None
|
|
14
14
|
|
|
15
|
-
try:
|
|
16
|
-
from valkey import asyncio as
|
|
15
|
+
try:
|
|
16
|
+
from valkey import asyncio as aiovalkey
|
|
17
17
|
from valkey.exceptions import ValkeyError
|
|
18
18
|
except ImportError: # pragma: no cover
|
|
19
|
-
|
|
19
|
+
aiovalkey = None
|
|
20
20
|
ValkeyError = None
|
|
21
21
|
|
|
22
22
|
from engineio import json
|
|
@@ -24,7 +24,7 @@ from .async_pubsub_manager import AsyncPubSubManager
|
|
|
24
24
|
from .redis_manager import parse_redis_sentinel_url
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
class AsyncRedisManager(AsyncPubSubManager):
|
|
27
|
+
class AsyncRedisManager(AsyncPubSubManager):
|
|
28
28
|
"""Redis based client manager for asyncio servers.
|
|
29
29
|
|
|
30
30
|
This class implements a Redis backend for event sharing across multiple
|
|
@@ -55,12 +55,8 @@ class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover
|
|
|
55
55
|
|
|
56
56
|
def __init__(self, url='redis://localhost:6379/0', channel='socketio',
|
|
57
57
|
write_only=False, logger=None, redis_options=None):
|
|
58
|
-
if aioredis
|
|
59
|
-
|
|
60
|
-
'(Run "pip install redis" or '
|
|
61
|
-
'"pip install valkey" '
|
|
62
|
-
'in your virtualenv).')
|
|
63
|
-
if aioredis and not hasattr(aioredis.Redis, 'from_url'):
|
|
58
|
+
if aioredis and \
|
|
59
|
+
not hasattr(aioredis.Redis, 'from_url'): # pragma: no cover
|
|
64
60
|
raise RuntimeError('Version 2 of aioredis package is required.')
|
|
65
61
|
super().__init__(channel=channel, write_only=write_only, logger=logger)
|
|
66
62
|
self.redis_url = url
|
|
@@ -69,20 +65,31 @@ class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover
|
|
|
69
65
|
|
|
70
66
|
def _get_redis_module_and_error(self):
|
|
71
67
|
parsed_url = urlparse(self.redis_url)
|
|
72
|
-
|
|
73
|
-
if
|
|
68
|
+
scheme = parsed_url.scheme.split('+', 1)[0].lower()
|
|
69
|
+
if scheme in ['redis', 'rediss']:
|
|
74
70
|
if aioredis is None or RedisError is None:
|
|
75
71
|
raise RuntimeError('Redis package is not installed '
|
|
76
72
|
'(Run "pip install redis" '
|
|
77
73
|
'in your virtualenv).')
|
|
78
74
|
return aioredis, RedisError
|
|
79
|
-
if
|
|
80
|
-
if
|
|
75
|
+
if scheme in ['valkey', 'valkeys']:
|
|
76
|
+
if aiovalkey is None or ValkeyError is None:
|
|
81
77
|
raise RuntimeError('Valkey package is not installed '
|
|
82
78
|
'(Run "pip install valkey" '
|
|
83
79
|
'in your virtualenv).')
|
|
84
|
-
return
|
|
85
|
-
|
|
80
|
+
return aiovalkey, ValkeyError
|
|
81
|
+
if scheme == 'unix':
|
|
82
|
+
if aioredis is None or RedisError is None:
|
|
83
|
+
if aiovalkey is None or ValkeyError is None:
|
|
84
|
+
raise RuntimeError('Redis package is not installed '
|
|
85
|
+
'(Run "pip install redis" '
|
|
86
|
+
'or "pip install valkey" '
|
|
87
|
+
'in your virtualenv).')
|
|
88
|
+
else:
|
|
89
|
+
return aiovalkey, ValkeyError
|
|
90
|
+
else:
|
|
91
|
+
return aioredis, RedisError
|
|
92
|
+
error_msg = f'Unsupported Redis URL scheme: {scheme}'
|
|
86
93
|
raise ValueError(error_msg)
|
|
87
94
|
|
|
88
95
|
def _redis_connect(self):
|
|
@@ -100,7 +107,7 @@ class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover
|
|
|
100
107
|
**self.redis_options)
|
|
101
108
|
self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True)
|
|
102
109
|
|
|
103
|
-
async def _publish(self, data):
|
|
110
|
+
async def _publish(self, data): # pragma: no cover
|
|
104
111
|
retry = True
|
|
105
112
|
_, error = self._get_redis_module_and_error()
|
|
106
113
|
while True:
|
|
@@ -124,7 +131,7 @@ class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover
|
|
|
124
131
|
|
|
125
132
|
break
|
|
126
133
|
|
|
127
|
-
async def _redis_listen_with_retries(self):
|
|
134
|
+
async def _redis_listen_with_retries(self): # pragma: no cover
|
|
128
135
|
retry_sleep = 1
|
|
129
136
|
connect = False
|
|
130
137
|
_, error = self._get_redis_module_and_error()
|
|
@@ -147,7 +154,7 @@ class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover
|
|
|
147
154
|
if retry_sleep > 60:
|
|
148
155
|
retry_sleep = 60
|
|
149
156
|
|
|
150
|
-
async def _listen(self):
|
|
157
|
+
async def _listen(self): # pragma: no cover
|
|
151
158
|
channel = self.channel.encode('utf-8')
|
|
152
159
|
await self.pubsub.subscribe(self.channel)
|
|
153
160
|
async for message in self._redis_listen_with_retries():
|
|
@@ -29,7 +29,13 @@ class BaseManager:
|
|
|
29
29
|
return self.rooms.keys()
|
|
30
30
|
|
|
31
31
|
def get_participants(self, namespace, room):
|
|
32
|
-
"""Return an iterable with the active participants in a room.
|
|
32
|
+
"""Return an iterable with the active participants in a room.
|
|
33
|
+
|
|
34
|
+
Note that in a multi-server scenario this method only returns the
|
|
35
|
+
participants connect to the server in which the method is called. There
|
|
36
|
+
is currently no functionality to assemble a complete list of users
|
|
37
|
+
across multiple servers.
|
|
38
|
+
"""
|
|
33
39
|
ns = self.rooms.get(namespace, {})
|
|
34
40
|
if hasattr(room, '__len__') and not isinstance(room, str):
|
|
35
41
|
participants = ns[room[0]]._fwdm.copy() if room[0] in ns else {}
|
|
@@ -169,7 +169,7 @@ class Client(base_client.BaseClient):
|
|
|
169
169
|
self.disconnect()
|
|
170
170
|
raise exceptions.ConnectionError(
|
|
171
171
|
'One or more namespaces failed to connect: '
|
|
172
|
-
', '.join(self.failed_namespaces))
|
|
172
|
+
+ ', '.join(self.failed_namespaces))
|
|
173
173
|
|
|
174
174
|
self.connected = True
|
|
175
175
|
|
|
@@ -29,7 +29,7 @@ class Packet:
|
|
|
29
29
|
self.namespace = namespace
|
|
30
30
|
self.id = id
|
|
31
31
|
if self.uses_binary_events and \
|
|
32
|
-
(binary or (binary is None and self.
|
|
32
|
+
(binary or (binary is None and self.data_is_binary(
|
|
33
33
|
self.data))):
|
|
34
34
|
if self.packet_type == EVENT:
|
|
35
35
|
self.packet_type = BINARY_EVENT
|
|
@@ -51,7 +51,7 @@ class Packet:
|
|
|
51
51
|
"""
|
|
52
52
|
encoded_packet = str(self.packet_type)
|
|
53
53
|
if self.packet_type == BINARY_EVENT or self.packet_type == BINARY_ACK:
|
|
54
|
-
data, attachments = self.
|
|
54
|
+
data, attachments = self.deconstruct_binary(self.data)
|
|
55
55
|
encoded_packet += str(len(attachments)) + '-'
|
|
56
56
|
else:
|
|
57
57
|
data = self.data
|
|
@@ -119,61 +119,65 @@ class Packet:
|
|
|
119
119
|
raise ValueError('Unexpected binary attachment')
|
|
120
120
|
self.attachments.append(attachment)
|
|
121
121
|
if self.attachment_count == len(self.attachments):
|
|
122
|
-
self.reconstruct_binary(self.attachments)
|
|
122
|
+
self.data = self.reconstruct_binary(self.data, self.attachments)
|
|
123
123
|
return True
|
|
124
124
|
return False
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
@classmethod
|
|
127
|
+
def reconstruct_binary(cls, data, attachments):
|
|
127
128
|
"""Reconstruct a decoded packet using the given list of binary
|
|
128
129
|
attachments.
|
|
129
130
|
"""
|
|
130
|
-
|
|
131
|
-
self.attachments)
|
|
131
|
+
return cls._reconstruct_binary_internal(data, attachments)
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
@classmethod
|
|
134
|
+
def _reconstruct_binary_internal(cls, data, attachments):
|
|
134
135
|
if isinstance(data, list):
|
|
135
|
-
return [
|
|
136
|
+
return [cls._reconstruct_binary_internal(item, attachments)
|
|
136
137
|
for item in data]
|
|
137
138
|
elif isinstance(data, dict):
|
|
138
139
|
if data.get('_placeholder') and 'num' in data:
|
|
139
140
|
return attachments[data['num']]
|
|
140
141
|
else:
|
|
141
|
-
return {key:
|
|
142
|
-
|
|
142
|
+
return {key: cls._reconstruct_binary_internal(value,
|
|
143
|
+
attachments)
|
|
143
144
|
for key, value in data.items()}
|
|
144
145
|
else:
|
|
145
146
|
return data
|
|
146
147
|
|
|
147
|
-
|
|
148
|
+
@classmethod
|
|
149
|
+
def deconstruct_binary(cls, data):
|
|
148
150
|
"""Extract binary components in the packet."""
|
|
149
151
|
attachments = []
|
|
150
|
-
data =
|
|
152
|
+
data = cls._deconstruct_binary_internal(data, attachments)
|
|
151
153
|
return data, attachments
|
|
152
154
|
|
|
153
|
-
|
|
155
|
+
@classmethod
|
|
156
|
+
def _deconstruct_binary_internal(cls, data, attachments):
|
|
154
157
|
if isinstance(data, bytes):
|
|
155
158
|
attachments.append(data)
|
|
156
159
|
return {'_placeholder': True, 'num': len(attachments) - 1}
|
|
157
160
|
elif isinstance(data, list):
|
|
158
|
-
return [
|
|
161
|
+
return [cls._deconstruct_binary_internal(item, attachments)
|
|
159
162
|
for item in data]
|
|
160
163
|
elif isinstance(data, dict):
|
|
161
|
-
return {key:
|
|
164
|
+
return {key: cls._deconstruct_binary_internal(value, attachments)
|
|
162
165
|
for key, value in data.items()}
|
|
163
166
|
else:
|
|
164
167
|
return data
|
|
165
168
|
|
|
166
|
-
|
|
169
|
+
@classmethod
|
|
170
|
+
def data_is_binary(cls, data):
|
|
167
171
|
"""Check if the data contains binary components."""
|
|
168
172
|
if isinstance(data, bytes):
|
|
169
173
|
return True
|
|
170
174
|
elif isinstance(data, list):
|
|
171
175
|
return functools.reduce(
|
|
172
|
-
lambda a, b: a or b, [
|
|
176
|
+
lambda a, b: a or b, [cls.data_is_binary(item)
|
|
173
177
|
for item in data], False)
|
|
174
178
|
elif isinstance(data, dict):
|
|
175
179
|
return functools.reduce(
|
|
176
|
-
lambda a, b: a or b, [
|
|
180
|
+
lambda a, b: a or b, [cls.data_is_binary(item)
|
|
177
181
|
for item in data.values()],
|
|
178
182
|
False)
|
|
179
183
|
else:
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
from functools import partial
|
|
2
3
|
import uuid
|
|
3
4
|
|
|
4
5
|
from engineio import json
|
|
5
6
|
|
|
6
7
|
from .manager import Manager
|
|
8
|
+
from .packet import Packet
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class PubSubManager(Manager):
|
|
@@ -61,8 +63,12 @@ class PubSubManager(Manager):
|
|
|
61
63
|
callback = (room, namespace, id)
|
|
62
64
|
else:
|
|
63
65
|
callback = None
|
|
66
|
+
binary = Packet.data_is_binary(data)
|
|
67
|
+
if binary:
|
|
68
|
+
data, attachments = Packet.deconstruct_binary(data)
|
|
69
|
+
data = [data, *[base64.b64encode(a).decode() for a in attachments]]
|
|
64
70
|
message = {'method': 'emit', 'event': event, 'data': data,
|
|
65
|
-
'namespace': namespace, 'room': room,
|
|
71
|
+
'binary': binary, 'namespace': namespace, 'room': room,
|
|
66
72
|
'skip_sid': skip_sid, 'callback': callback,
|
|
67
73
|
'host_id': self.host_id}
|
|
68
74
|
self._handle_emit(message) # handle in this host
|
|
@@ -141,7 +147,11 @@ class PubSubManager(Manager):
|
|
|
141
147
|
*remote_callback)
|
|
142
148
|
else:
|
|
143
149
|
callback = None
|
|
144
|
-
|
|
150
|
+
data = message['data']
|
|
151
|
+
if message.get('binary'):
|
|
152
|
+
attachments = [base64.b64decode(a) for a in data[1:]]
|
|
153
|
+
data = Packet.reconstruct_binary(data[0], attachments)
|
|
154
|
+
super().emit(message['event'], data,
|
|
145
155
|
namespace=message.get('namespace'),
|
|
146
156
|
room=message.get('room'),
|
|
147
157
|
skip_sid=message.get('skip_sid'), callback=callback)
|
|
@@ -2,17 +2,17 @@ import logging
|
|
|
2
2
|
import time
|
|
3
3
|
from urllib.parse import urlparse
|
|
4
4
|
|
|
5
|
-
try:
|
|
5
|
+
try:
|
|
6
6
|
import redis
|
|
7
7
|
from redis.exceptions import RedisError
|
|
8
|
-
except ImportError:
|
|
8
|
+
except ImportError: # pragma: no cover
|
|
9
9
|
redis = None
|
|
10
10
|
RedisError = None
|
|
11
11
|
|
|
12
|
-
try:
|
|
12
|
+
try:
|
|
13
13
|
import valkey
|
|
14
14
|
from valkey.exceptions import ValkeyError
|
|
15
|
-
except ImportError:
|
|
15
|
+
except ImportError: # pragma: no cover
|
|
16
16
|
valkey = None
|
|
17
17
|
ValkeyError = None
|
|
18
18
|
|
|
@@ -48,7 +48,7 @@ def parse_redis_sentinel_url(url):
|
|
|
48
48
|
return sentinels, service_name, kwargs
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
class RedisManager(PubSubManager):
|
|
51
|
+
class RedisManager(PubSubManager):
|
|
52
52
|
"""Redis based client manager.
|
|
53
53
|
|
|
54
54
|
This class implements a Redis backend for event sharing across multiple
|
|
@@ -80,17 +80,12 @@ class RedisManager(PubSubManager): # pragma: no cover
|
|
|
80
80
|
|
|
81
81
|
def __init__(self, url='redis://localhost:6379/0', channel='socketio',
|
|
82
82
|
write_only=False, logger=None, redis_options=None):
|
|
83
|
-
if redis is None and valkey is None:
|
|
84
|
-
raise RuntimeError('Redis package is not installed '
|
|
85
|
-
'(Run "pip install redis" '
|
|
86
|
-
'or "pip install valkey" '
|
|
87
|
-
'in your virtualenv).')
|
|
88
83
|
super().__init__(channel=channel, write_only=write_only, logger=logger)
|
|
89
84
|
self.redis_url = url
|
|
90
85
|
self.redis_options = redis_options or {}
|
|
91
86
|
self._redis_connect()
|
|
92
87
|
|
|
93
|
-
def initialize(self):
|
|
88
|
+
def initialize(self): # pragma: no cover
|
|
94
89
|
super().initialize()
|
|
95
90
|
|
|
96
91
|
monkey_patched = True
|
|
@@ -107,20 +102,31 @@ class RedisManager(PubSubManager): # pragma: no cover
|
|
|
107
102
|
|
|
108
103
|
def _get_redis_module_and_error(self):
|
|
109
104
|
parsed_url = urlparse(self.redis_url)
|
|
110
|
-
|
|
111
|
-
if
|
|
105
|
+
scheme = parsed_url.scheme.split('+', 1)[0].lower()
|
|
106
|
+
if scheme in ['redis', 'rediss']:
|
|
112
107
|
if redis is None or RedisError is None:
|
|
113
108
|
raise RuntimeError('Redis package is not installed '
|
|
114
109
|
'(Run "pip install redis" '
|
|
115
110
|
'in your virtualenv).')
|
|
116
111
|
return redis, RedisError
|
|
117
|
-
if
|
|
112
|
+
if scheme in ['valkey', 'valkeys']:
|
|
118
113
|
if valkey is None or ValkeyError is None:
|
|
119
114
|
raise RuntimeError('Valkey package is not installed '
|
|
120
115
|
'(Run "pip install valkey" '
|
|
121
116
|
'in your virtualenv).')
|
|
122
117
|
return valkey, ValkeyError
|
|
123
|
-
|
|
118
|
+
if scheme == 'unix':
|
|
119
|
+
if redis is None or RedisError is None:
|
|
120
|
+
if valkey is None or ValkeyError is None:
|
|
121
|
+
raise RuntimeError('Redis package is not installed '
|
|
122
|
+
'(Run "pip install redis" '
|
|
123
|
+
'or "pip install valkey" '
|
|
124
|
+
'in your virtualenv).')
|
|
125
|
+
else:
|
|
126
|
+
return valkey, ValkeyError
|
|
127
|
+
else:
|
|
128
|
+
return redis, RedisError
|
|
129
|
+
error_msg = f'Unsupported Redis URL scheme: {scheme}'
|
|
124
130
|
raise ValueError(error_msg)
|
|
125
131
|
|
|
126
132
|
def _redis_connect(self):
|
|
@@ -138,7 +144,7 @@ class RedisManager(PubSubManager): # pragma: no cover
|
|
|
138
144
|
**self.redis_options)
|
|
139
145
|
self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True)
|
|
140
146
|
|
|
141
|
-
def _publish(self, data):
|
|
147
|
+
def _publish(self, data): # pragma: no cover
|
|
142
148
|
retry = True
|
|
143
149
|
_, error = self._get_redis_module_and_error()
|
|
144
150
|
while True:
|
|
@@ -160,7 +166,7 @@ class RedisManager(PubSubManager): # pragma: no cover
|
|
|
160
166
|
)
|
|
161
167
|
break
|
|
162
168
|
|
|
163
|
-
def _redis_listen_with_retries(self):
|
|
169
|
+
def _redis_listen_with_retries(self): # pragma: no cover
|
|
164
170
|
retry_sleep = 1
|
|
165
171
|
connect = False
|
|
166
172
|
_, error = self._get_redis_module_and_error()
|
|
@@ -181,7 +187,7 @@ class RedisManager(PubSubManager): # pragma: no cover
|
|
|
181
187
|
if retry_sleep > 60:
|
|
182
188
|
retry_sleep = 60
|
|
183
189
|
|
|
184
|
-
def _listen(self):
|
|
190
|
+
def _listen(self): # pragma: no cover
|
|
185
191
|
channel = self.channel.encode('utf-8')
|
|
186
192
|
self.pubsub.subscribe(self.channel)
|
|
187
193
|
for message in self._redis_listen_with_retries():
|
|
@@ -203,6 +203,60 @@ class TestAsyncClient:
|
|
|
203
203
|
assert c.connected is True
|
|
204
204
|
assert c.namespaces == {'/bar': '123', '/foo': '456'}
|
|
205
205
|
|
|
206
|
+
async def test_connect_wait_one_namespaces_error(self):
|
|
207
|
+
c = async_client.AsyncClient()
|
|
208
|
+
c.eio.connect = mock.AsyncMock()
|
|
209
|
+
c._connect_event = mock.MagicMock()
|
|
210
|
+
|
|
211
|
+
async def mock_connect():
|
|
212
|
+
if c.failed_namespaces == []:
|
|
213
|
+
c.failed_namespaces = ['/foo']
|
|
214
|
+
return True
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
c._connect_event.wait = mock_connect
|
|
218
|
+
with pytest.raises(exceptions.ConnectionError,
|
|
219
|
+
match='failed to connect: /foo'):
|
|
220
|
+
await c.connect(
|
|
221
|
+
'url',
|
|
222
|
+
namespaces=['/foo'],
|
|
223
|
+
wait=True,
|
|
224
|
+
wait_timeout=0.01,
|
|
225
|
+
)
|
|
226
|
+
assert c.connected is False
|
|
227
|
+
assert c.namespaces == {}
|
|
228
|
+
assert c.failed_namespaces == ['/foo']
|
|
229
|
+
|
|
230
|
+
async def test_connect_wait_three_namespaces_error(self):
|
|
231
|
+
c = async_client.AsyncClient()
|
|
232
|
+
c.eio.connect = mock.AsyncMock()
|
|
233
|
+
c._connect_event = mock.MagicMock()
|
|
234
|
+
|
|
235
|
+
async def mock_connect():
|
|
236
|
+
if c.namespaces == {}:
|
|
237
|
+
c.namespaces = {'/bar': '123'}
|
|
238
|
+
return True
|
|
239
|
+
elif c.namespaces == {'/bar': '123'} and c.failed_namespaces == []:
|
|
240
|
+
c.failed_namespaces = ['/baz']
|
|
241
|
+
return True
|
|
242
|
+
elif c.failed_namespaces == ['/baz']:
|
|
243
|
+
c.failed_namespaces = ['/baz', '/foo']
|
|
244
|
+
return True
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
c._connect_event.wait = mock_connect
|
|
248
|
+
with pytest.raises(exceptions.ConnectionError,
|
|
249
|
+
match='failed to connect: /baz, /foo'):
|
|
250
|
+
await c.connect(
|
|
251
|
+
'url',
|
|
252
|
+
namespaces=['/foo', '/bar', '/baz'],
|
|
253
|
+
wait=True,
|
|
254
|
+
wait_timeout=0.01,
|
|
255
|
+
)
|
|
256
|
+
assert c.connected is False
|
|
257
|
+
assert c.namespaces == {'/bar': '123'}
|
|
258
|
+
assert c.failed_namespaces == ['/baz', '/foo']
|
|
259
|
+
|
|
206
260
|
async def test_connect_timeout(self):
|
|
207
261
|
c = async_client.AsyncClient()
|
|
208
262
|
c.eio.connect = mock.AsyncMock()
|