python-socketio 5.13.0__tar.gz → 5.14.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_socketio-5.13.0/src/python_socketio.egg-info → python_socketio-5.14.0}/PKG-INFO +2 -2
- {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/server.rst +19 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/pyproject.toml +2 -2
- {python_socketio-5.13.0 → python_socketio-5.14.0/src/python_socketio.egg-info}/PKG-INFO +2 -2
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_aiopika_manager.py +4 -4
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_client.py +7 -4
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_pubsub_manager.py +4 -11
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_redis_manager.py +56 -18
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_server.py +3 -3
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_simple_client.py +3 -1
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/base_client.py +1 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/client.py +8 -4
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/kafka_manager.py +3 -3
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/kombu_manager.py +2 -2
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/pubsub_manager.py +4 -11
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/redis_manager.py +57 -18
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/simple_client.py +3 -1
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/zmq_manager.py +6 -6
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/test_admin.py +24 -24
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/test_pubsub_manager.py +6 -7
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/test_simple_client.py +12 -1
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_admin.py +21 -21
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_pubsub_manager.py +6 -7
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_redis_manager.py +8 -6
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_simple_client.py +11 -1
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/web_server.py +1 -1
- {python_socketio-5.13.0 → python_socketio-5.14.0}/LICENSE +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/MANIFEST.in +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/README.md +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/Makefile +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/_static/README.md +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/_static/custom.css +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/api.rst +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/client.rst +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/conf.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/index.rst +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/intro.rst +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/make.bat +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/setup.cfg +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/python_socketio.egg-info/SOURCES.txt +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/python_socketio.egg-info/dependency_links.txt +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/python_socketio.egg-info/not-zip-safe +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/python_socketio.egg-info/requires.txt +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/python_socketio.egg-info/top_level.txt +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/__init__.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/admin.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/asgi.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_admin.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_manager.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_namespace.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/base_manager.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/base_namespace.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/base_server.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/exceptions.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/manager.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/middleware.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/msgpack_packet.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/namespace.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/packet.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/server.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/tornado.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/__init__.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/__init__.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/test_client.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/test_manager.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/test_namespace.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/test_server.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/asyncio_web_server.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/__init__.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_client.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_manager.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_middleware.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_msgpack_packet.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_namespace.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_packet.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_server.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/README.md +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/binary_packet.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/json_packet.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/namespace_packet.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/run.sh +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/server_receive.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/server_send.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/server_send_broadcast.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/text_packet.py +0 -0
- {python_socketio-5.13.0 → python_socketio-5.14.0}/tox.ini +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-socketio
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.14.0
|
|
4
4
|
Summary: Socket.IO server and client for Python
|
|
5
5
|
Author-email: Miguel Grinberg <miguel.grinberg@gmail.com>
|
|
6
|
+
License: MIT
|
|
6
7
|
Project-URL: Homepage, https://github.com/miguelgrinberg/python-socketio
|
|
7
8
|
Project-URL: Bug Tracker, https://github.com/miguelgrinberg/python-socketio/issues
|
|
8
9
|
Classifier: Environment :: Web Environment
|
|
9
10
|
Classifier: Intended Audience :: Developers
|
|
10
11
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Requires-Python: >=3.8
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
@@ -1089,6 +1089,25 @@ The RabbitMQ queue is configured through the
|
|
|
1089
1089
|
mgr = socketio.AsyncAioPikaManager('amqp://')
|
|
1090
1090
|
sio = socketio.AsyncServer(client_manager=mgr)
|
|
1091
1091
|
|
|
1092
|
+
Deploying the Message Queue for Production
|
|
1093
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1094
|
+
|
|
1095
|
+
For a production deployment there are a few recommendations to keep your
|
|
1096
|
+
application secure.
|
|
1097
|
+
|
|
1098
|
+
First of all, the message queue should never be listening on a public network
|
|
1099
|
+
interface, to ensure that external clients never connect to it. For a single
|
|
1100
|
+
node deployment, the queue should only listen on `localhost`. For a multi-node
|
|
1101
|
+
system the use of a private network (VPC), where the communication between
|
|
1102
|
+
servers can happen privately is highly recommended.
|
|
1103
|
+
|
|
1104
|
+
In addition, all message queues support authentication and encryption, which
|
|
1105
|
+
can strenthen the security of the deployment. Authentication ensures that only
|
|
1106
|
+
the Socket.IO servers and related processes have access, while encryption
|
|
1107
|
+
prevents data from being collected by a third-party that is listening on the
|
|
1108
|
+
network. Access credentials can be included in the connection URLs that are
|
|
1109
|
+
passed to the client managers.
|
|
1110
|
+
|
|
1092
1111
|
Horizontal Scaling
|
|
1093
1112
|
~~~~~~~~~~~~~~~~~~
|
|
1094
1113
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-socketio"
|
|
3
|
-
version = "5.
|
|
3
|
+
version = "5.14.0"
|
|
4
|
+
license = {text = "MIT"}
|
|
4
5
|
authors = [
|
|
5
6
|
{ name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
|
|
6
7
|
]
|
|
@@ -9,7 +10,6 @@ classifiers = [
|
|
|
9
10
|
"Environment :: Web Environment",
|
|
10
11
|
"Intended Audience :: Developers",
|
|
11
12
|
"Programming Language :: Python :: 3",
|
|
12
|
-
"License :: OSI Approved :: MIT License",
|
|
13
13
|
"Operating System :: OS Independent",
|
|
14
14
|
]
|
|
15
15
|
requires-python = ">=3.8"
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-socketio
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.14.0
|
|
4
4
|
Summary: Socket.IO server and client for Python
|
|
5
5
|
Author-email: Miguel Grinberg <miguel.grinberg@gmail.com>
|
|
6
|
+
License: MIT
|
|
6
7
|
Project-URL: Homepage, https://github.com/miguelgrinberg/python-socketio
|
|
7
8
|
Project-URL: Bug Tracker, https://github.com/miguelgrinberg/python-socketio/issues
|
|
8
9
|
Classifier: Environment :: Web Environment
|
|
9
10
|
Classifier: Intended Audience :: Developers
|
|
10
11
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Requires-Python: >=3.8
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import pickle
|
|
3
2
|
|
|
3
|
+
from engineio import json
|
|
4
4
|
from .async_pubsub_manager import AsyncPubSubManager
|
|
5
5
|
|
|
6
6
|
try:
|
|
@@ -43,12 +43,12 @@ class AsyncAioPikaManager(AsyncPubSubManager): # pragma: no cover
|
|
|
43
43
|
raise RuntimeError('aio_pika package is not installed '
|
|
44
44
|
'(Run "pip install aio_pika" in your '
|
|
45
45
|
'virtualenv).')
|
|
46
|
+
super().__init__(channel=channel, write_only=write_only, logger=logger)
|
|
46
47
|
self.url = url
|
|
47
48
|
self._lock = asyncio.Lock()
|
|
48
49
|
self.publisher_connection = None
|
|
49
50
|
self.publisher_channel = None
|
|
50
51
|
self.publisher_exchange = None
|
|
51
|
-
super().__init__(channel=channel, write_only=write_only, logger=logger)
|
|
52
52
|
|
|
53
53
|
async def _connection(self):
|
|
54
54
|
return await aio_pika.connect_robust(self.url)
|
|
@@ -82,7 +82,7 @@ class AsyncAioPikaManager(AsyncPubSubManager): # pragma: no cover
|
|
|
82
82
|
try:
|
|
83
83
|
await self.publisher_exchange.publish(
|
|
84
84
|
aio_pika.Message(
|
|
85
|
-
body=
|
|
85
|
+
body=json.dumps(data),
|
|
86
86
|
delivery_mode=aio_pika.DeliveryMode.PERSISTENT
|
|
87
87
|
), routing_key='*',
|
|
88
88
|
)
|
|
@@ -113,7 +113,7 @@ class AsyncAioPikaManager(AsyncPubSubManager): # pragma: no cover
|
|
|
113
113
|
async with queue.iterator() as queue_iter:
|
|
114
114
|
async for message in queue_iter:
|
|
115
115
|
async with message.process():
|
|
116
|
-
yield
|
|
116
|
+
yield message.body
|
|
117
117
|
retry_sleep = 1
|
|
118
118
|
except aio_pika.AMQPException:
|
|
119
119
|
self._get_logger().error(
|
|
@@ -139,6 +139,7 @@ class AsyncClient(base_client.BaseClient):
|
|
|
139
139
|
namespaces = [namespaces]
|
|
140
140
|
self.connection_namespaces = namespaces
|
|
141
141
|
self.namespaces = {}
|
|
142
|
+
self.failed_namespaces = []
|
|
142
143
|
if self._connect_event is None:
|
|
143
144
|
self._connect_event = self.eio.create_event()
|
|
144
145
|
else:
|
|
@@ -166,14 +167,16 @@ class AsyncClient(base_client.BaseClient):
|
|
|
166
167
|
await asyncio.wait_for(self._connect_event.wait(),
|
|
167
168
|
wait_timeout)
|
|
168
169
|
self._connect_event.clear()
|
|
169
|
-
if
|
|
170
|
+
if len(self.namespaces) + len(self.failed_namespaces) == \
|
|
171
|
+
len(self.connection_namespaces):
|
|
170
172
|
break
|
|
171
173
|
except asyncio.TimeoutError:
|
|
172
174
|
pass
|
|
173
175
|
if set(self.namespaces) != set(self.connection_namespaces):
|
|
174
176
|
await self.disconnect()
|
|
175
177
|
raise exceptions.ConnectionError(
|
|
176
|
-
'One or more namespaces failed to connect'
|
|
178
|
+
'One or more namespaces failed to connect'
|
|
179
|
+
', '.join(self.failed_namespaces))
|
|
177
180
|
|
|
178
181
|
self.connected = True
|
|
179
182
|
|
|
@@ -191,7 +194,6 @@ class AsyncClient(base_client.BaseClient):
|
|
|
191
194
|
if not self._reconnect_task:
|
|
192
195
|
if self.eio.state == 'connected': # pragma: no cover
|
|
193
196
|
# connected while sleeping above
|
|
194
|
-
print('oops')
|
|
195
197
|
continue
|
|
196
198
|
break
|
|
197
199
|
await self._reconnect_task
|
|
@@ -405,7 +407,7 @@ class AsyncClient(base_client.BaseClient):
|
|
|
405
407
|
del self.namespaces[namespace]
|
|
406
408
|
if not self.namespaces:
|
|
407
409
|
self.connected = False
|
|
408
|
-
await self.eio.disconnect(
|
|
410
|
+
await self.eio.disconnect()
|
|
409
411
|
|
|
410
412
|
async def _handle_event(self, namespace, id, data):
|
|
411
413
|
namespace = namespace or '/'
|
|
@@ -449,6 +451,7 @@ class AsyncClient(base_client.BaseClient):
|
|
|
449
451
|
elif not isinstance(data, (tuple, list)):
|
|
450
452
|
data = (data,)
|
|
451
453
|
await self._trigger_event('connect_error', namespace, *data)
|
|
454
|
+
self.failed_namespaces.append(namespace)
|
|
452
455
|
self._connect_event.set()
|
|
453
456
|
if namespace in self.namespaces:
|
|
454
457
|
del self.namespaces[namespace]
|
|
@@ -3,7 +3,6 @@ from functools import partial
|
|
|
3
3
|
import uuid
|
|
4
4
|
|
|
5
5
|
from engineio import json
|
|
6
|
-
import pickle
|
|
7
6
|
|
|
8
7
|
from .async_manager import AsyncManager
|
|
9
8
|
|
|
@@ -202,16 +201,10 @@ class AsyncPubSubManager(AsyncManager):
|
|
|
202
201
|
if isinstance(message, dict):
|
|
203
202
|
data = message
|
|
204
203
|
else:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
pass
|
|
210
|
-
if data is None:
|
|
211
|
-
try:
|
|
212
|
-
data = json.loads(message)
|
|
213
|
-
except:
|
|
214
|
-
pass
|
|
204
|
+
try:
|
|
205
|
+
data = json.loads(message)
|
|
206
|
+
except:
|
|
207
|
+
pass
|
|
215
208
|
if data and 'method' in data:
|
|
216
209
|
self._get_logger().debug('pubsub message: {}'.format(
|
|
217
210
|
data['method']))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
3
|
|
|
4
4
|
try: # pragma: no cover
|
|
5
5
|
from redis import asyncio as aioredis
|
|
@@ -12,6 +12,14 @@ except ImportError: # pragma: no cover
|
|
|
12
12
|
aioredis = None
|
|
13
13
|
RedisError = None
|
|
14
14
|
|
|
15
|
+
try: # pragma: no cover
|
|
16
|
+
from valkey import asyncio as valkey
|
|
17
|
+
from valkey.exceptions import ValkeyError
|
|
18
|
+
except ImportError: # pragma: no cover
|
|
19
|
+
valkey = None
|
|
20
|
+
ValkeyError = None
|
|
21
|
+
|
|
22
|
+
from engineio import json
|
|
15
23
|
from .async_pubsub_manager import AsyncPubSubManager
|
|
16
24
|
from .redis_manager import parse_redis_sentinel_url
|
|
17
25
|
|
|
@@ -47,50 +55,79 @@ class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover
|
|
|
47
55
|
|
|
48
56
|
def __init__(self, url='redis://localhost:6379/0', channel='socketio',
|
|
49
57
|
write_only=False, logger=None, redis_options=None):
|
|
50
|
-
if aioredis is None:
|
|
58
|
+
if aioredis is None and valkey is None:
|
|
51
59
|
raise RuntimeError('Redis package is not installed '
|
|
52
|
-
'(Run "pip install redis"
|
|
53
|
-
|
|
60
|
+
'(Run "pip install redis" or '
|
|
61
|
+
'"pip install valkey" '
|
|
62
|
+
'in your virtualenv).')
|
|
63
|
+
if aioredis and not hasattr(aioredis.Redis, 'from_url'):
|
|
54
64
|
raise RuntimeError('Version 2 of aioredis package is required.')
|
|
65
|
+
super().__init__(channel=channel, write_only=write_only, logger=logger)
|
|
55
66
|
self.redis_url = url
|
|
56
67
|
self.redis_options = redis_options or {}
|
|
57
68
|
self._redis_connect()
|
|
58
|
-
|
|
69
|
+
|
|
70
|
+
def _get_redis_module_and_error(self):
|
|
71
|
+
parsed_url = urlparse(self.redis_url)
|
|
72
|
+
schema = parsed_url.scheme.split('+', 1)[0].lower()
|
|
73
|
+
if schema == 'redis':
|
|
74
|
+
if aioredis is None or RedisError is None:
|
|
75
|
+
raise RuntimeError('Redis package is not installed '
|
|
76
|
+
'(Run "pip install redis" '
|
|
77
|
+
'in your virtualenv).')
|
|
78
|
+
return aioredis, RedisError
|
|
79
|
+
if schema == 'valkey':
|
|
80
|
+
if valkey is None or ValkeyError is None:
|
|
81
|
+
raise RuntimeError('Valkey package is not installed '
|
|
82
|
+
'(Run "pip install valkey" '
|
|
83
|
+
'in your virtualenv).')
|
|
84
|
+
return valkey, ValkeyError
|
|
85
|
+
error_msg = f'Unsupported Redis URL schema: {schema}'
|
|
86
|
+
raise ValueError(error_msg)
|
|
59
87
|
|
|
60
88
|
def _redis_connect(self):
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
else:
|
|
89
|
+
module, _ = self._get_redis_module_and_error()
|
|
90
|
+
parsed_url = urlparse(self.redis_url)
|
|
91
|
+
if parsed_url.scheme in {"redis+sentinel", "valkey+sentinel"}:
|
|
65
92
|
sentinels, service_name, connection_kwargs = \
|
|
66
93
|
parse_redis_sentinel_url(self.redis_url)
|
|
67
94
|
kwargs = self.redis_options
|
|
68
95
|
kwargs.update(connection_kwargs)
|
|
69
|
-
sentinel =
|
|
96
|
+
sentinel = module.sentinel.Sentinel(sentinels, **kwargs)
|
|
70
97
|
self.redis = sentinel.master_for(service_name or self.channel)
|
|
98
|
+
else:
|
|
99
|
+
self.redis = module.Redis.from_url(self.redis_url,
|
|
100
|
+
**self.redis_options)
|
|
71
101
|
self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True)
|
|
72
102
|
|
|
73
103
|
async def _publish(self, data):
|
|
74
104
|
retry = True
|
|
105
|
+
_, error = self._get_redis_module_and_error()
|
|
75
106
|
while True:
|
|
76
107
|
try:
|
|
77
108
|
if not retry:
|
|
78
109
|
self._redis_connect()
|
|
79
110
|
return await self.redis.publish(
|
|
80
|
-
self.channel,
|
|
81
|
-
except
|
|
111
|
+
self.channel, json.dumps(data))
|
|
112
|
+
except error as exc:
|
|
82
113
|
if retry:
|
|
83
|
-
self._get_logger().error(
|
|
84
|
-
|
|
114
|
+
self._get_logger().error(
|
|
115
|
+
'Cannot publish to redis... '
|
|
116
|
+
'retrying',
|
|
117
|
+
extra={"redis_exception": str(exc)})
|
|
85
118
|
retry = False
|
|
86
119
|
else:
|
|
87
|
-
self._get_logger().error(
|
|
88
|
-
|
|
120
|
+
self._get_logger().error(
|
|
121
|
+
'Cannot publish to redis... '
|
|
122
|
+
'giving up',
|
|
123
|
+
extra={"redis_exception": str(exc)})
|
|
124
|
+
|
|
89
125
|
break
|
|
90
126
|
|
|
91
127
|
async def _redis_listen_with_retries(self):
|
|
92
128
|
retry_sleep = 1
|
|
93
129
|
connect = False
|
|
130
|
+
_, error = self._get_redis_module_and_error()
|
|
94
131
|
while True:
|
|
95
132
|
try:
|
|
96
133
|
if connect:
|
|
@@ -99,10 +136,11 @@ class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover
|
|
|
99
136
|
retry_sleep = 1
|
|
100
137
|
async for message in self.pubsub.listen():
|
|
101
138
|
yield message
|
|
102
|
-
except
|
|
139
|
+
except error as exc:
|
|
103
140
|
self._get_logger().error('Cannot receive from redis... '
|
|
104
141
|
'retrying in '
|
|
105
|
-
'{} secs'
|
|
142
|
+
f'{retry_sleep} secs',
|
|
143
|
+
extra={"redis_exception": str(exc)})
|
|
106
144
|
connect = True
|
|
107
145
|
await asyncio.sleep(retry_sleep)
|
|
108
146
|
retry_sleep *= 2
|
|
@@ -373,15 +373,15 @@ class AsyncServer(base_server.BaseServer):
|
|
|
373
373
|
context manager block are saved back to the session. Example usage::
|
|
374
374
|
|
|
375
375
|
@eio.on('connect')
|
|
376
|
-
def on_connect(sid, environ):
|
|
376
|
+
async def on_connect(sid, environ):
|
|
377
377
|
username = authenticate_user(environ)
|
|
378
378
|
if not username:
|
|
379
379
|
return False
|
|
380
|
-
with eio.session(sid) as session:
|
|
380
|
+
async with eio.session(sid) as session:
|
|
381
381
|
session['username'] = username
|
|
382
382
|
|
|
383
383
|
@eio.on('message')
|
|
384
|
-
def on_message(sid, msg):
|
|
384
|
+
async def on_message(sid, msg):
|
|
385
385
|
async with eio.session(sid) as session:
|
|
386
386
|
print('received message from ', session['username'])
|
|
387
387
|
"""
|
|
@@ -105,7 +105,7 @@ class AsyncSimpleClient:
|
|
|
105
105
|
The transport is returned as a string and can be one of ``polling``
|
|
106
106
|
and ``websocket``.
|
|
107
107
|
"""
|
|
108
|
-
return self.client.transport if self.client else ''
|
|
108
|
+
return self.client.transport() if self.client else ''
|
|
109
109
|
|
|
110
110
|
async def emit(self, event, data=None):
|
|
111
111
|
"""Emit an event to the server.
|
|
@@ -163,6 +163,8 @@ class AsyncSimpleClient:
|
|
|
163
163
|
return await self.client.call(event, data,
|
|
164
164
|
namespace=self.namespace,
|
|
165
165
|
timeout=timeout)
|
|
166
|
+
except TimeoutError:
|
|
167
|
+
raise
|
|
166
168
|
except SocketIOError:
|
|
167
169
|
pass
|
|
168
170
|
|
|
@@ -137,6 +137,7 @@ class Client(base_client.BaseClient):
|
|
|
137
137
|
namespaces = [namespaces]
|
|
138
138
|
self.connection_namespaces = namespaces
|
|
139
139
|
self.namespaces = {}
|
|
140
|
+
self.failed_namespaces = []
|
|
140
141
|
if self._connect_event is None:
|
|
141
142
|
self._connect_event = self.eio.create_event()
|
|
142
143
|
else:
|
|
@@ -161,12 +162,14 @@ class Client(base_client.BaseClient):
|
|
|
161
162
|
if wait:
|
|
162
163
|
while self._connect_event.wait(timeout=wait_timeout):
|
|
163
164
|
self._connect_event.clear()
|
|
164
|
-
if
|
|
165
|
+
if len(self.namespaces) + len(self.failed_namespaces) == \
|
|
166
|
+
len(self.connection_namespaces):
|
|
165
167
|
break
|
|
166
168
|
if set(self.namespaces) != set(self.connection_namespaces):
|
|
167
169
|
self.disconnect()
|
|
168
170
|
raise exceptions.ConnectionError(
|
|
169
|
-
'One or more namespaces failed to connect'
|
|
171
|
+
'One or more namespaces failed to connect: '
|
|
172
|
+
', '.join(self.failed_namespaces))
|
|
170
173
|
|
|
171
174
|
self.connected = True
|
|
172
175
|
|
|
@@ -384,7 +387,7 @@ class Client(base_client.BaseClient):
|
|
|
384
387
|
del self.namespaces[namespace]
|
|
385
388
|
if not self.namespaces:
|
|
386
389
|
self.connected = False
|
|
387
|
-
self.eio.disconnect(
|
|
390
|
+
self.eio.disconnect()
|
|
388
391
|
|
|
389
392
|
def _handle_event(self, namespace, id, data):
|
|
390
393
|
namespace = namespace or '/'
|
|
@@ -425,6 +428,7 @@ class Client(base_client.BaseClient):
|
|
|
425
428
|
elif not isinstance(data, (tuple, list)):
|
|
426
429
|
data = (data,)
|
|
427
430
|
self._trigger_event('connect_error', namespace, *data)
|
|
431
|
+
self.failed_namespaces.append(namespace)
|
|
428
432
|
self._connect_event.set()
|
|
429
433
|
if namespace in self.namespaces:
|
|
430
434
|
del self.namespaces[namespace]
|
|
@@ -439,7 +443,7 @@ class Client(base_client.BaseClient):
|
|
|
439
443
|
if handler:
|
|
440
444
|
try:
|
|
441
445
|
return handler(*args)
|
|
442
|
-
except TypeError:
|
|
446
|
+
except TypeError: # pragma: no cover
|
|
443
447
|
# the legacy disconnect event does not take a reason argument
|
|
444
448
|
if event == 'disconnect':
|
|
445
449
|
return handler(*args[:-1])
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import pickle
|
|
3
2
|
|
|
4
3
|
try:
|
|
5
4
|
import kafka
|
|
6
5
|
except ImportError:
|
|
7
6
|
kafka = None
|
|
8
7
|
|
|
8
|
+
from engineio import json
|
|
9
9
|
from .pubsub_manager import PubSubManager
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger('socketio')
|
|
@@ -53,7 +53,7 @@ class KafkaManager(PubSubManager): # pragma: no cover
|
|
|
53
53
|
bootstrap_servers=self.kafka_urls)
|
|
54
54
|
|
|
55
55
|
def _publish(self, data):
|
|
56
|
-
self.producer.send(self.channel, value=
|
|
56
|
+
self.producer.send(self.channel, value=json.dumps(data))
|
|
57
57
|
self.producer.flush()
|
|
58
58
|
|
|
59
59
|
def _kafka_listen(self):
|
|
@@ -62,4 +62,4 @@ class KafkaManager(PubSubManager): # pragma: no cover
|
|
|
62
62
|
def _listen(self):
|
|
63
63
|
for message in self._kafka_listen():
|
|
64
64
|
if message.topic == self.channel:
|
|
65
|
-
yield
|
|
65
|
+
yield message.value
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import pickle
|
|
2
1
|
import time
|
|
3
2
|
import uuid
|
|
4
3
|
|
|
@@ -7,6 +6,7 @@ try:
|
|
|
7
6
|
except ImportError:
|
|
8
7
|
kombu = None
|
|
9
8
|
|
|
9
|
+
from engineio import json
|
|
10
10
|
from .pubsub_manager import PubSubManager
|
|
11
11
|
|
|
12
12
|
|
|
@@ -102,7 +102,7 @@ class KombuManager(PubSubManager): # pragma: no cover
|
|
|
102
102
|
try:
|
|
103
103
|
producer_publish = self._producer_publish(
|
|
104
104
|
self.publisher_connection)
|
|
105
|
-
producer_publish(
|
|
105
|
+
producer_publish(json.dumps(data))
|
|
106
106
|
break
|
|
107
107
|
except (OSError, kombu.exceptions.KombuError):
|
|
108
108
|
if retry:
|
|
@@ -2,7 +2,6 @@ from functools import partial
|
|
|
2
2
|
import uuid
|
|
3
3
|
|
|
4
4
|
from engineio import json
|
|
5
|
-
import pickle
|
|
6
5
|
|
|
7
6
|
from .manager import Manager
|
|
8
7
|
|
|
@@ -196,16 +195,10 @@ class PubSubManager(Manager):
|
|
|
196
195
|
if isinstance(message, dict):
|
|
197
196
|
data = message
|
|
198
197
|
else:
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
pass
|
|
204
|
-
if data is None:
|
|
205
|
-
try:
|
|
206
|
-
data = json.loads(message)
|
|
207
|
-
except:
|
|
208
|
-
pass
|
|
198
|
+
try:
|
|
199
|
+
data = json.loads(message)
|
|
200
|
+
except:
|
|
201
|
+
pass
|
|
209
202
|
if data and 'method' in data:
|
|
210
203
|
self._get_logger().debug('pubsub message: {}'.format(
|
|
211
204
|
data['method']))
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import pickle
|
|
3
2
|
import time
|
|
4
3
|
from urllib.parse import urlparse
|
|
5
4
|
|
|
6
|
-
try:
|
|
5
|
+
try: # pragma: no cover
|
|
7
6
|
import redis
|
|
7
|
+
from redis.exceptions import RedisError
|
|
8
8
|
except ImportError:
|
|
9
9
|
redis = None
|
|
10
|
+
RedisError = None
|
|
10
11
|
|
|
12
|
+
try: # pragma: no cover
|
|
13
|
+
import valkey
|
|
14
|
+
from valkey.exceptions import ValkeyError
|
|
15
|
+
except ImportError:
|
|
16
|
+
valkey = None
|
|
17
|
+
ValkeyError = None
|
|
18
|
+
|
|
19
|
+
from engineio import json
|
|
11
20
|
from .pubsub_manager import PubSubManager
|
|
12
21
|
|
|
13
22
|
logger = logging.getLogger('socketio')
|
|
@@ -18,7 +27,7 @@ def parse_redis_sentinel_url(url):
|
|
|
18
27
|
redis+sentinel://[:password]@host1:port1,host2:port2,.../db/service_name
|
|
19
28
|
"""
|
|
20
29
|
parsed_url = urlparse(url)
|
|
21
|
-
if parsed_url.scheme
|
|
30
|
+
if parsed_url.scheme not in {'redis+sentinel', 'valkey+sentinel'}:
|
|
22
31
|
raise ValueError('Invalid Redis Sentinel URL')
|
|
23
32
|
sentinels = []
|
|
24
33
|
for host_port in parsed_url.netloc.split('@')[-1].split(','):
|
|
@@ -71,14 +80,15 @@ class RedisManager(PubSubManager): # pragma: no cover
|
|
|
71
80
|
|
|
72
81
|
def __init__(self, url='redis://localhost:6379/0', channel='socketio',
|
|
73
82
|
write_only=False, logger=None, redis_options=None):
|
|
74
|
-
if redis is None:
|
|
83
|
+
if redis is None and valkey is None:
|
|
75
84
|
raise RuntimeError('Redis package is not installed '
|
|
76
|
-
'(Run "pip install redis"
|
|
77
|
-
'
|
|
85
|
+
'(Run "pip install redis" '
|
|
86
|
+
'or "pip install valkey" '
|
|
87
|
+
'in your virtualenv).')
|
|
88
|
+
super().__init__(channel=channel, write_only=write_only, logger=logger)
|
|
78
89
|
self.redis_url = url
|
|
79
90
|
self.redis_options = redis_options or {}
|
|
80
91
|
self._redis_connect()
|
|
81
|
-
super().__init__(channel=channel, write_only=write_only, logger=logger)
|
|
82
92
|
|
|
83
93
|
def initialize(self):
|
|
84
94
|
super().initialize()
|
|
@@ -95,37 +105,65 @@ class RedisManager(PubSubManager): # pragma: no cover
|
|
|
95
105
|
'Redis requires a monkey patched socket library to work '
|
|
96
106
|
'with ' + self.server.async_mode)
|
|
97
107
|
|
|
108
|
+
def _get_redis_module_and_error(self):
|
|
109
|
+
parsed_url = urlparse(self.redis_url)
|
|
110
|
+
schema = parsed_url.scheme.split('+', 1)[0].lower()
|
|
111
|
+
if schema == 'redis':
|
|
112
|
+
if redis is None or RedisError is None:
|
|
113
|
+
raise RuntimeError('Redis package is not installed '
|
|
114
|
+
'(Run "pip install redis" '
|
|
115
|
+
'in your virtualenv).')
|
|
116
|
+
return redis, RedisError
|
|
117
|
+
if schema == 'valkey':
|
|
118
|
+
if valkey is None or ValkeyError is None:
|
|
119
|
+
raise RuntimeError('Valkey package is not installed '
|
|
120
|
+
'(Run "pip install valkey" '
|
|
121
|
+
'in your virtualenv).')
|
|
122
|
+
return valkey, ValkeyError
|
|
123
|
+
error_msg = f'Unsupported Redis URL schema: {schema}'
|
|
124
|
+
raise ValueError(error_msg)
|
|
125
|
+
|
|
98
126
|
def _redis_connect(self):
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
else:
|
|
127
|
+
module, _ = self._get_redis_module_and_error()
|
|
128
|
+
parsed_url = urlparse(self.redis_url)
|
|
129
|
+
if parsed_url.scheme in {"redis+sentinel", "valkey+sentinel"}:
|
|
103
130
|
sentinels, service_name, connection_kwargs = \
|
|
104
131
|
parse_redis_sentinel_url(self.redis_url)
|
|
105
132
|
kwargs = self.redis_options
|
|
106
133
|
kwargs.update(connection_kwargs)
|
|
107
|
-
sentinel =
|
|
134
|
+
sentinel = module.sentinel.Sentinel(sentinels, **kwargs)
|
|
108
135
|
self.redis = sentinel.master_for(service_name or self.channel)
|
|
136
|
+
else:
|
|
137
|
+
self.redis = module.Redis.from_url(self.redis_url,
|
|
138
|
+
**self.redis_options)
|
|
109
139
|
self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True)
|
|
110
140
|
|
|
111
141
|
def _publish(self, data):
|
|
112
142
|
retry = True
|
|
143
|
+
_, error = self._get_redis_module_and_error()
|
|
113
144
|
while True:
|
|
114
145
|
try:
|
|
115
146
|
if not retry:
|
|
116
147
|
self._redis_connect()
|
|
117
|
-
return self.redis.publish(self.channel,
|
|
118
|
-
except
|
|
148
|
+
return self.redis.publish(self.channel, json.dumps(data))
|
|
149
|
+
except error as exc:
|
|
119
150
|
if retry:
|
|
120
|
-
logger.error(
|
|
151
|
+
logger.error(
|
|
152
|
+
'Cannot publish to redis... retrying',
|
|
153
|
+
extra={"redis_exception": str(exc)}
|
|
154
|
+
)
|
|
121
155
|
retry = False
|
|
122
156
|
else:
|
|
123
|
-
logger.error(
|
|
157
|
+
logger.error(
|
|
158
|
+
'Cannot publish to redis... giving up',
|
|
159
|
+
extra={"redis_exception": str(exc)}
|
|
160
|
+
)
|
|
124
161
|
break
|
|
125
162
|
|
|
126
163
|
def _redis_listen_with_retries(self):
|
|
127
164
|
retry_sleep = 1
|
|
128
165
|
connect = False
|
|
166
|
+
_, error = self._get_redis_module_and_error()
|
|
129
167
|
while True:
|
|
130
168
|
try:
|
|
131
169
|
if connect:
|
|
@@ -133,9 +171,10 @@ class RedisManager(PubSubManager): # pragma: no cover
|
|
|
133
171
|
self.pubsub.subscribe(self.channel)
|
|
134
172
|
retry_sleep = 1
|
|
135
173
|
yield from self.pubsub.listen()
|
|
136
|
-
except
|
|
174
|
+
except error as exc:
|
|
137
175
|
logger.error('Cannot receive from redis... '
|
|
138
|
-
'retrying in {} secs'
|
|
176
|
+
f'retrying in {retry_sleep} secs',
|
|
177
|
+
extra={"redis_exception": str(exc)})
|
|
139
178
|
connect = True
|
|
140
179
|
time.sleep(retry_sleep)
|
|
141
180
|
retry_sleep *= 2
|