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.
Files changed (88) hide show
  1. {python_socketio-5.14.0/src/python_socketio.egg-info → python_socketio-5.14.2}/PKG-INFO +3 -1
  2. {python_socketio-5.14.0 → python_socketio-5.14.2}/pyproject.toml +4 -1
  3. {python_socketio-5.14.0 → python_socketio-5.14.2/src/python_socketio.egg-info}/PKG-INFO +3 -1
  4. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/python_socketio.egg-info/SOURCES.txt +1 -0
  5. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/python_socketio.egg-info/requires.txt +3 -0
  6. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_client.py +2 -2
  7. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_pubsub_manager.py +12 -2
  8. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_redis_manager.py +27 -20
  9. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/base_manager.py +7 -1
  10. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/client.py +1 -1
  11. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/packet.py +22 -18
  12. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/pubsub_manager.py +12 -2
  13. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/redis_manager.py +24 -18
  14. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/test_client.py +54 -0
  15. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/test_pubsub_manager.py +67 -0
  16. python_socketio-5.14.2/tests/async/test_redis_manager.py +107 -0
  17. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_client.py +56 -0
  18. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_packet.py +16 -8
  19. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_pubsub_manager.py +65 -0
  20. python_socketio-5.14.2/tests/common/test_redis_manager.py +142 -0
  21. {python_socketio-5.14.0 → python_socketio-5.14.2}/tox.ini +4 -1
  22. python_socketio-5.14.0/tests/common/test_redis_manager.py +0 -40
  23. {python_socketio-5.14.0 → python_socketio-5.14.2}/LICENSE +0 -0
  24. {python_socketio-5.14.0 → python_socketio-5.14.2}/MANIFEST.in +0 -0
  25. {python_socketio-5.14.0 → python_socketio-5.14.2}/README.md +0 -0
  26. {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/Makefile +0 -0
  27. {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/_static/README.md +0 -0
  28. {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/_static/custom.css +0 -0
  29. {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/api.rst +0 -0
  30. {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/client.rst +0 -0
  31. {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/conf.py +0 -0
  32. {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/index.rst +0 -0
  33. {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/intro.rst +0 -0
  34. {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/make.bat +0 -0
  35. {python_socketio-5.14.0 → python_socketio-5.14.2}/docs/server.rst +0 -0
  36. {python_socketio-5.14.0 → python_socketio-5.14.2}/setup.cfg +0 -0
  37. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/python_socketio.egg-info/dependency_links.txt +0 -0
  38. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/python_socketio.egg-info/not-zip-safe +0 -0
  39. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/python_socketio.egg-info/top_level.txt +0 -0
  40. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/__init__.py +0 -0
  41. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/admin.py +0 -0
  42. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/asgi.py +0 -0
  43. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_admin.py +0 -0
  44. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_aiopika_manager.py +0 -0
  45. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_manager.py +0 -0
  46. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_namespace.py +0 -0
  47. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_server.py +0 -0
  48. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/async_simple_client.py +0 -0
  49. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/base_client.py +0 -0
  50. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/base_namespace.py +0 -0
  51. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/base_server.py +0 -0
  52. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/exceptions.py +0 -0
  53. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/kafka_manager.py +0 -0
  54. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/kombu_manager.py +0 -0
  55. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/manager.py +0 -0
  56. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/middleware.py +0 -0
  57. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/msgpack_packet.py +0 -0
  58. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/namespace.py +0 -0
  59. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/server.py +0 -0
  60. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/simple_client.py +0 -0
  61. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/tornado.py +0 -0
  62. {python_socketio-5.14.0 → python_socketio-5.14.2}/src/socketio/zmq_manager.py +0 -0
  63. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/__init__.py +0 -0
  64. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/__init__.py +0 -0
  65. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/test_admin.py +0 -0
  66. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/test_manager.py +0 -0
  67. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/test_namespace.py +0 -0
  68. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/test_server.py +0 -0
  69. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/async/test_simple_client.py +0 -0
  70. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/asyncio_web_server.py +0 -0
  71. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/__init__.py +0 -0
  72. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_admin.py +0 -0
  73. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_manager.py +0 -0
  74. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_middleware.py +0 -0
  75. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_msgpack_packet.py +0 -0
  76. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_namespace.py +0 -0
  77. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_server.py +0 -0
  78. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/common/test_simple_client.py +0 -0
  79. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/README.md +0 -0
  80. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/binary_packet.py +0 -0
  81. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/json_packet.py +0 -0
  82. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/namespace_packet.py +0 -0
  83. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/run.sh +0 -0
  84. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/server_receive.py +0 -0
  85. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/server_send.py +0 -0
  86. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/server_send_broadcast.py +0 -0
  87. {python_socketio-5.14.0 → python_socketio-5.14.2}/tests/performance/text_packet.py +0 -0
  88. {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.0
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.0"
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.0
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
@@ -59,6 +59,7 @@ tests/async/test_client.py
59
59
  tests/async/test_manager.py
60
60
  tests/async/test_namespace.py
61
61
  tests/async/test_pubsub_manager.py
62
+ tests/async/test_redis_manager.py
62
63
  tests/async/test_server.py
63
64
  tests/async/test_simple_client.py
64
65
  tests/common/__init__.py
@@ -8,5 +8,8 @@ aiohttp>=3.4
8
8
  requests>=2.21.0
9
9
  websocket-client>=0.54.0
10
10
 
11
+ [dev]
12
+ tox
13
+
11
14
  [docs]
12
15
  sphinx
@@ -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
- await super().emit(message['event'], message['data'],
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: # pragma: no cover
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: # pragma: no cover
16
- from valkey import asyncio as valkey
15
+ try:
16
+ from valkey import asyncio as aiovalkey
17
17
  from valkey.exceptions import ValkeyError
18
18
  except ImportError: # pragma: no cover
19
- valkey = None
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): # pragma: no cover
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 is None and valkey is None:
59
- raise RuntimeError('Redis package is not installed '
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
- schema = parsed_url.scheme.split('+', 1)[0].lower()
73
- if schema == 'redis':
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 schema == 'valkey':
80
- if valkey is None or ValkeyError is None:
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 valkey, ValkeyError
85
- error_msg = f'Unsupported Redis URL schema: {schema}'
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._data_is_binary(
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._deconstruct_binary(self.data)
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
- def reconstruct_binary(self, attachments):
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
- self.data = self._reconstruct_binary_internal(self.data,
131
- self.attachments)
131
+ return cls._reconstruct_binary_internal(data, attachments)
132
132
 
133
- def _reconstruct_binary_internal(self, data, attachments):
133
+ @classmethod
134
+ def _reconstruct_binary_internal(cls, data, attachments):
134
135
  if isinstance(data, list):
135
- return [self._reconstruct_binary_internal(item, attachments)
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: self._reconstruct_binary_internal(value,
142
- attachments)
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
- def _deconstruct_binary(self, data):
148
+ @classmethod
149
+ def deconstruct_binary(cls, data):
148
150
  """Extract binary components in the packet."""
149
151
  attachments = []
150
- data = self._deconstruct_binary_internal(data, attachments)
152
+ data = cls._deconstruct_binary_internal(data, attachments)
151
153
  return data, attachments
152
154
 
153
- def _deconstruct_binary_internal(self, data, attachments):
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 [self._deconstruct_binary_internal(item, attachments)
161
+ return [cls._deconstruct_binary_internal(item, attachments)
159
162
  for item in data]
160
163
  elif isinstance(data, dict):
161
- return {key: self._deconstruct_binary_internal(value, attachments)
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
- def _data_is_binary(self, data):
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, [self._data_is_binary(item)
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, [self._data_is_binary(item)
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
- super().emit(message['event'], message['data'],
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: # pragma: no cover
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: # pragma: no cover
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): # pragma: no cover
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
- schema = parsed_url.scheme.split('+', 1)[0].lower()
111
- if schema == 'redis':
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 schema == 'valkey':
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
- error_msg = f'Unsupported Redis URL schema: {schema}'
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()