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.
Files changed (86) hide show
  1. {python_socketio-5.13.0/src/python_socketio.egg-info → python_socketio-5.14.0}/PKG-INFO +2 -2
  2. {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/server.rst +19 -0
  3. {python_socketio-5.13.0 → python_socketio-5.14.0}/pyproject.toml +2 -2
  4. {python_socketio-5.13.0 → python_socketio-5.14.0/src/python_socketio.egg-info}/PKG-INFO +2 -2
  5. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_aiopika_manager.py +4 -4
  6. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_client.py +7 -4
  7. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_pubsub_manager.py +4 -11
  8. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_redis_manager.py +56 -18
  9. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_server.py +3 -3
  10. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_simple_client.py +3 -1
  11. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/base_client.py +1 -0
  12. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/client.py +8 -4
  13. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/kafka_manager.py +3 -3
  14. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/kombu_manager.py +2 -2
  15. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/pubsub_manager.py +4 -11
  16. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/redis_manager.py +57 -18
  17. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/simple_client.py +3 -1
  18. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/zmq_manager.py +6 -6
  19. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/test_admin.py +24 -24
  20. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/test_pubsub_manager.py +6 -7
  21. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/test_simple_client.py +12 -1
  22. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_admin.py +21 -21
  23. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_pubsub_manager.py +6 -7
  24. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_redis_manager.py +8 -6
  25. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_simple_client.py +11 -1
  26. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/web_server.py +1 -1
  27. {python_socketio-5.13.0 → python_socketio-5.14.0}/LICENSE +0 -0
  28. {python_socketio-5.13.0 → python_socketio-5.14.0}/MANIFEST.in +0 -0
  29. {python_socketio-5.13.0 → python_socketio-5.14.0}/README.md +0 -0
  30. {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/Makefile +0 -0
  31. {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/_static/README.md +0 -0
  32. {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/_static/custom.css +0 -0
  33. {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/api.rst +0 -0
  34. {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/client.rst +0 -0
  35. {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/conf.py +0 -0
  36. {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/index.rst +0 -0
  37. {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/intro.rst +0 -0
  38. {python_socketio-5.13.0 → python_socketio-5.14.0}/docs/make.bat +0 -0
  39. {python_socketio-5.13.0 → python_socketio-5.14.0}/setup.cfg +0 -0
  40. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/python_socketio.egg-info/SOURCES.txt +0 -0
  41. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/python_socketio.egg-info/dependency_links.txt +0 -0
  42. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/python_socketio.egg-info/not-zip-safe +0 -0
  43. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/python_socketio.egg-info/requires.txt +0 -0
  44. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/python_socketio.egg-info/top_level.txt +0 -0
  45. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/__init__.py +0 -0
  46. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/admin.py +0 -0
  47. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/asgi.py +0 -0
  48. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_admin.py +0 -0
  49. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_manager.py +0 -0
  50. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/async_namespace.py +0 -0
  51. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/base_manager.py +0 -0
  52. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/base_namespace.py +0 -0
  53. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/base_server.py +0 -0
  54. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/exceptions.py +0 -0
  55. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/manager.py +0 -0
  56. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/middleware.py +0 -0
  57. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/msgpack_packet.py +0 -0
  58. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/namespace.py +0 -0
  59. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/packet.py +0 -0
  60. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/server.py +0 -0
  61. {python_socketio-5.13.0 → python_socketio-5.14.0}/src/socketio/tornado.py +0 -0
  62. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/__init__.py +0 -0
  63. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/__init__.py +0 -0
  64. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/test_client.py +0 -0
  65. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/test_manager.py +0 -0
  66. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/test_namespace.py +0 -0
  67. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/async/test_server.py +0 -0
  68. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/asyncio_web_server.py +0 -0
  69. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/__init__.py +0 -0
  70. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_client.py +0 -0
  71. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_manager.py +0 -0
  72. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_middleware.py +0 -0
  73. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_msgpack_packet.py +0 -0
  74. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_namespace.py +0 -0
  75. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_packet.py +0 -0
  76. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/common/test_server.py +0 -0
  77. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/README.md +0 -0
  78. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/binary_packet.py +0 -0
  79. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/json_packet.py +0 -0
  80. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/namespace_packet.py +0 -0
  81. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/run.sh +0 -0
  82. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/server_receive.py +0 -0
  83. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/server_send.py +0 -0
  84. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/server_send_broadcast.py +0 -0
  85. {python_socketio-5.13.0 → python_socketio-5.14.0}/tests/performance/text_packet.py +0 -0
  86. {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.13.0
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.13.0"
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.13.0
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=pickle.dumps(data),
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 pickle.loads(message.body)
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 set(self.namespaces) == set(self.connection_namespaces):
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(abort=True)
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
- if isinstance(message, bytes): # pragma: no cover
206
- try:
207
- data = pickle.loads(message)
208
- except:
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 pickle
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" in your virtualenv).')
53
- if not hasattr(aioredis.Redis, 'from_url'):
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
- super().__init__(channel=channel, write_only=write_only, logger=logger)
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
- if not self.redis_url.startswith('redis+sentinel://'):
62
- self.redis = aioredis.Redis.from_url(self.redis_url,
63
- **self.redis_options)
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 = aioredis.sentinel.Sentinel(sentinels, **kwargs)
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, pickle.dumps(data))
81
- except RedisError:
111
+ self.channel, json.dumps(data))
112
+ except error as exc:
82
113
  if retry:
83
- self._get_logger().error('Cannot publish to redis... '
84
- 'retrying')
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('Cannot publish to redis... '
88
- 'giving up')
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 RedisError:
139
+ except error as exc:
103
140
  self._get_logger().error('Cannot receive from redis... '
104
141
  'retrying in '
105
- '{} secs'.format(retry_sleep))
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
 
@@ -93,6 +93,7 @@ class BaseClient:
93
93
 
94
94
  self.connected = False #: Indicates if the client is connected or not.
95
95
  self.namespaces = {} #: set of connected namespaces.
96
+ self.failed_namespaces = []
96
97
  self.handlers = {}
97
98
  self.namespace_handlers = {}
98
99
  self.callbacks = {}
@@ -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 set(self.namespaces) == set(self.connection_namespaces):
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(abort=True)
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=pickle.dumps(data))
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 pickle.loads(message.value)
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(pickle.dumps(data))
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
- if isinstance(message, bytes): # pragma: no cover
200
- try:
201
- data = pickle.loads(message)
202
- except:
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 != 'redis+sentinel':
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" in your '
77
- 'virtualenv).')
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
- if not self.redis_url.startswith('redis+sentinel://'):
100
- self.redis = redis.Redis.from_url(self.redis_url,
101
- **self.redis_options)
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 = redis.sentinel.Sentinel(sentinels, **kwargs)
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, pickle.dumps(data))
118
- except redis.exceptions.RedisError:
148
+ return self.redis.publish(self.channel, json.dumps(data))
149
+ except error as exc:
119
150
  if retry:
120
- logger.error('Cannot publish to redis... retrying')
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('Cannot publish to redis... giving up')
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 redis.exceptions.RedisError:
174
+ except error as exc:
137
175
  logger.error('Cannot receive from redis... '
138
- 'retrying in {} secs'.format(retry_sleep))
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