python-socketio 5.14.1__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 (87) hide show
  1. {python_socketio-5.14.1/src/python_socketio.egg-info → python_socketio-5.14.2}/PKG-INFO +3 -1
  2. {python_socketio-5.14.1 → python_socketio-5.14.2}/pyproject.toml +4 -1
  3. {python_socketio-5.14.1 → python_socketio-5.14.2/src/python_socketio.egg-info}/PKG-INFO +3 -1
  4. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/python_socketio.egg-info/requires.txt +3 -0
  5. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/async_client.py +2 -2
  6. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/async_pubsub_manager.py +12 -2
  7. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/base_manager.py +7 -1
  8. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/client.py +1 -1
  9. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/packet.py +22 -18
  10. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/pubsub_manager.py +12 -2
  11. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/async/test_client.py +54 -0
  12. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/async/test_pubsub_manager.py +67 -0
  13. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/common/test_client.py +56 -0
  14. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/common/test_packet.py +16 -8
  15. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/common/test_pubsub_manager.py +65 -0
  16. {python_socketio-5.14.1 → python_socketio-5.14.2}/tox.ini +2 -1
  17. {python_socketio-5.14.1 → python_socketio-5.14.2}/LICENSE +0 -0
  18. {python_socketio-5.14.1 → python_socketio-5.14.2}/MANIFEST.in +0 -0
  19. {python_socketio-5.14.1 → python_socketio-5.14.2}/README.md +0 -0
  20. {python_socketio-5.14.1 → python_socketio-5.14.2}/docs/Makefile +0 -0
  21. {python_socketio-5.14.1 → python_socketio-5.14.2}/docs/_static/README.md +0 -0
  22. {python_socketio-5.14.1 → python_socketio-5.14.2}/docs/_static/custom.css +0 -0
  23. {python_socketio-5.14.1 → python_socketio-5.14.2}/docs/api.rst +0 -0
  24. {python_socketio-5.14.1 → python_socketio-5.14.2}/docs/client.rst +0 -0
  25. {python_socketio-5.14.1 → python_socketio-5.14.2}/docs/conf.py +0 -0
  26. {python_socketio-5.14.1 → python_socketio-5.14.2}/docs/index.rst +0 -0
  27. {python_socketio-5.14.1 → python_socketio-5.14.2}/docs/intro.rst +0 -0
  28. {python_socketio-5.14.1 → python_socketio-5.14.2}/docs/make.bat +0 -0
  29. {python_socketio-5.14.1 → python_socketio-5.14.2}/docs/server.rst +0 -0
  30. {python_socketio-5.14.1 → python_socketio-5.14.2}/setup.cfg +0 -0
  31. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/python_socketio.egg-info/SOURCES.txt +0 -0
  32. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/python_socketio.egg-info/dependency_links.txt +0 -0
  33. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/python_socketio.egg-info/not-zip-safe +0 -0
  34. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/python_socketio.egg-info/top_level.txt +0 -0
  35. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/__init__.py +0 -0
  36. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/admin.py +0 -0
  37. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/asgi.py +0 -0
  38. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/async_admin.py +0 -0
  39. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/async_aiopika_manager.py +0 -0
  40. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/async_manager.py +0 -0
  41. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/async_namespace.py +0 -0
  42. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/async_redis_manager.py +0 -0
  43. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/async_server.py +0 -0
  44. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/async_simple_client.py +0 -0
  45. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/base_client.py +0 -0
  46. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/base_namespace.py +0 -0
  47. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/base_server.py +0 -0
  48. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/exceptions.py +0 -0
  49. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/kafka_manager.py +0 -0
  50. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/kombu_manager.py +0 -0
  51. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/manager.py +0 -0
  52. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/middleware.py +0 -0
  53. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/msgpack_packet.py +0 -0
  54. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/namespace.py +0 -0
  55. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/redis_manager.py +0 -0
  56. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/server.py +0 -0
  57. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/simple_client.py +0 -0
  58. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/tornado.py +0 -0
  59. {python_socketio-5.14.1 → python_socketio-5.14.2}/src/socketio/zmq_manager.py +0 -0
  60. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/__init__.py +0 -0
  61. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/async/__init__.py +0 -0
  62. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/async/test_admin.py +0 -0
  63. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/async/test_manager.py +0 -0
  64. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/async/test_namespace.py +0 -0
  65. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/async/test_redis_manager.py +0 -0
  66. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/async/test_server.py +0 -0
  67. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/async/test_simple_client.py +0 -0
  68. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/asyncio_web_server.py +0 -0
  69. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/common/__init__.py +0 -0
  70. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/common/test_admin.py +0 -0
  71. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/common/test_manager.py +0 -0
  72. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/common/test_middleware.py +0 -0
  73. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/common/test_msgpack_packet.py +0 -0
  74. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/common/test_namespace.py +0 -0
  75. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/common/test_redis_manager.py +0 -0
  76. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/common/test_server.py +0 -0
  77. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/common/test_simple_client.py +0 -0
  78. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/performance/README.md +0 -0
  79. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/performance/binary_packet.py +0 -0
  80. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/performance/json_packet.py +0 -0
  81. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/performance/namespace_packet.py +0 -0
  82. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/performance/run.sh +0 -0
  83. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/performance/server_receive.py +0 -0
  84. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/performance/server_send.py +0 -0
  85. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/performance/server_send_broadcast.py +0 -0
  86. {python_socketio-5.14.1 → python_socketio-5.14.2}/tests/performance/text_packet.py +0 -0
  87. {python_socketio-5.14.1 → 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.1
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.1"
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.1
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
@@ -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'),
@@ -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)
@@ -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()
@@ -57,6 +57,7 @@ class TestAsyncPubSubManager:
57
57
  {
58
58
  'method': 'emit',
59
59
  'event': 'foo',
60
+ 'binary': False,
60
61
  'data': 'bar',
61
62
  'namespace': '/',
62
63
  'room': None,
@@ -66,6 +67,36 @@ class TestAsyncPubSubManager:
66
67
  }
67
68
  )
68
69
 
70
+ async def test_emit_binary(self):
71
+ await self.pm.emit('foo', b'bar')
72
+ self.pm._publish.assert_awaited_once_with(
73
+ {
74
+ 'method': 'emit',
75
+ 'event': 'foo',
76
+ 'binary': True,
77
+ 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'],
78
+ 'namespace': '/',
79
+ 'room': None,
80
+ 'skip_sid': None,
81
+ 'callback': None,
82
+ 'host_id': '123456',
83
+ }
84
+ )
85
+ await self.pm.emit('foo', {'foo': b'bar'})
86
+ self.pm._publish.assert_awaited_with(
87
+ {
88
+ 'method': 'emit',
89
+ 'event': 'foo',
90
+ 'binary': True,
91
+ 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'],
92
+ 'namespace': '/',
93
+ 'room': None,
94
+ 'skip_sid': None,
95
+ 'callback': None,
96
+ 'host_id': '123456',
97
+ }
98
+ )
99
+
69
100
  async def test_emit_with_to(self):
70
101
  sid = 'room-mate'
71
102
  await self.pm.emit('foo', 'bar', to=sid)
@@ -73,6 +104,7 @@ class TestAsyncPubSubManager:
73
104
  {
74
105
  'method': 'emit',
75
106
  'event': 'foo',
107
+ 'binary': False,
76
108
  'data': 'bar',
77
109
  'namespace': '/',
78
110
  'room': sid,
@@ -88,6 +120,7 @@ class TestAsyncPubSubManager:
88
120
  {
89
121
  'method': 'emit',
90
122
  'event': 'foo',
123
+ 'binary': False,
91
124
  'data': 'bar',
92
125
  'namespace': '/baz',
93
126
  'room': None,
@@ -103,6 +136,7 @@ class TestAsyncPubSubManager:
103
136
  {
104
137
  'method': 'emit',
105
138
  'event': 'foo',
139
+ 'binary': False,
106
140
  'data': 'bar',
107
141
  'namespace': '/',
108
142
  'room': 'baz',
@@ -118,6 +152,7 @@ class TestAsyncPubSubManager:
118
152
  {
119
153
  'method': 'emit',
120
154
  'event': 'foo',
155
+ 'binary': False,
121
156
  'data': 'bar',
122
157
  'namespace': '/',
123
158
  'room': None,
@@ -136,6 +171,7 @@ class TestAsyncPubSubManager:
136
171
  {
137
172
  'method': 'emit',
138
173
  'event': 'foo',
174
+ 'binary': False,
139
175
  'data': 'bar',
140
176
  'namespace': '/',
141
177
  'room': 'baz',
@@ -241,6 +277,37 @@ class TestAsyncPubSubManager:
241
277
  callback=None,
242
278
  )
243
279
 
280
+ async def test_handle_emit_binary(self):
281
+ with mock.patch.object(
282
+ async_manager.AsyncManager, 'emit'
283
+ ) as super_emit:
284
+ await self.pm._handle_emit({
285
+ 'event': 'foo',
286
+ 'binary': True,
287
+ 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'],
288
+ })
289
+ super_emit.assert_awaited_once_with(
290
+ 'foo',
291
+ b'bar',
292
+ namespace=None,
293
+ room=None,
294
+ skip_sid=None,
295
+ callback=None,
296
+ )
297
+ await self.pm._handle_emit({
298
+ 'event': 'foo',
299
+ 'binary': True,
300
+ 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'],
301
+ })
302
+ super_emit.assert_awaited_with(
303
+ 'foo',
304
+ {'foo': b'bar'},
305
+ namespace=None,
306
+ room=None,
307
+ skip_sid=None,
308
+ callback=None,
309
+ )
310
+
244
311
  async def test_handle_emit_with_namespace(self):
245
312
  with mock.patch.object(
246
313
  async_manager.AsyncManager, 'emit'
@@ -350,6 +350,62 @@ class TestClient:
350
350
  assert c.connected is True
351
351
  assert c.namespaces == {'/bar': '123', '/foo': '456'}
352
352
 
353
+ def test_connect_wait_one_namespaces_error(self):
354
+ c = client.Client()
355
+ c.eio.connect = mock.MagicMock()
356
+ c._connect_event = mock.MagicMock()
357
+
358
+ def mock_connect(timeout):
359
+ assert timeout == 0.01
360
+ if c.failed_namespaces == []:
361
+ c.failed_namespaces = ['/foo']
362
+ return True
363
+ return False
364
+
365
+ c._connect_event.wait = mock_connect
366
+ with pytest.raises(exceptions.ConnectionError,
367
+ match='failed to connect: /foo'):
368
+ c.connect(
369
+ 'url',
370
+ namespaces=['/foo'],
371
+ wait=True,
372
+ wait_timeout=0.01,
373
+ )
374
+ assert c.connected is False
375
+ assert c.namespaces == {}
376
+ assert c.failed_namespaces == ['/foo']
377
+
378
+ def test_connect_wait_three_namespaces_error(self):
379
+ c = client.Client()
380
+ c.eio.connect = mock.MagicMock()
381
+ c._connect_event = mock.MagicMock()
382
+
383
+ def mock_connect(timeout):
384
+ assert timeout == 0.01
385
+ if c.namespaces == {}:
386
+ c.namespaces = {'/bar': '123'}
387
+ return True
388
+ elif c.namespaces == {'/bar': '123'} and c.failed_namespaces == []:
389
+ c.failed_namespaces = ['/baz']
390
+ return True
391
+ elif c.failed_namespaces == ['/baz']:
392
+ c.failed_namespaces = ['/baz', '/foo']
393
+ return True
394
+ return False
395
+
396
+ c._connect_event.wait = mock_connect
397
+ with pytest.raises(exceptions.ConnectionError,
398
+ match='failed to connect: /baz, /foo'):
399
+ c.connect(
400
+ 'url',
401
+ namespaces=['/foo', '/bar', '/baz'],
402
+ wait=True,
403
+ wait_timeout=0.01,
404
+ )
405
+ assert c.connected is False
406
+ assert c.namespaces == {'/bar': '123'}
407
+ assert c.failed_namespaces == ['/baz', '/foo']
408
+
353
409
  def test_connect_timeout(self):
354
410
  c = client.Client()
355
411
  c.eio.connect = mock.MagicMock()
@@ -266,16 +266,24 @@ class TestPacket:
266
266
  assert pkt.data["a"] == "0123456789-"
267
267
  assert pkt.attachment_count == 0
268
268
 
269
+ def test_deconstruct_binary(self):
270
+ datas = [b'foo', [b'foo', b'bar'], ['foo', b'bar'], {'foo': b'bar'},
271
+ {'foo': 'bar', 'baz': b'qux'}, {'foo': [b'bar']}]
272
+ for data in datas:
273
+ bdata, attachments = packet.Packet.deconstruct_binary(data)
274
+ rdata = packet.Packet.reconstruct_binary(bdata, attachments)
275
+ assert data == rdata
276
+
269
277
  def test_data_is_binary_list(self):
270
278
  pkt = packet.Packet()
271
- assert not pkt._data_is_binary(['foo'])
272
- assert not pkt._data_is_binary([])
273
- assert pkt._data_is_binary([b'foo'])
274
- assert pkt._data_is_binary(['foo', b'bar'])
279
+ assert not pkt.data_is_binary(['foo'])
280
+ assert not pkt.data_is_binary([])
281
+ assert pkt.data_is_binary([b'foo'])
282
+ assert pkt.data_is_binary(['foo', b'bar'])
275
283
 
276
284
  def test_data_is_binary_dict(self):
277
285
  pkt = packet.Packet()
278
- assert not pkt._data_is_binary({'a': 'foo'})
279
- assert not pkt._data_is_binary({})
280
- assert pkt._data_is_binary({'a': b'foo'})
281
- assert pkt._data_is_binary({'a': 'foo', 'b': b'bar'})
286
+ assert not pkt.data_is_binary({'a': 'foo'})
287
+ assert not pkt.data_is_binary({})
288
+ assert pkt.data_is_binary({'a': b'foo'})
289
+ assert pkt.data_is_binary({'a': 'foo', 'b': b'bar'})
@@ -69,6 +69,7 @@ class TestPubSubManager:
69
69
  {
70
70
  'method': 'emit',
71
71
  'event': 'foo',
72
+ 'binary': False,
72
73
  'data': 'bar',
73
74
  'namespace': '/',
74
75
  'room': None,
@@ -78,6 +79,36 @@ class TestPubSubManager:
78
79
  }
79
80
  )
80
81
 
82
+ def test_emit_binary(self):
83
+ self.pm.emit('foo', b'bar')
84
+ self.pm._publish.assert_called_once_with(
85
+ {
86
+ 'method': 'emit',
87
+ 'event': 'foo',
88
+ 'binary': True,
89
+ 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'],
90
+ 'namespace': '/',
91
+ 'room': None,
92
+ 'skip_sid': None,
93
+ 'callback': None,
94
+ 'host_id': '123456',
95
+ }
96
+ )
97
+ self.pm.emit('foo', {'foo': b'bar'})
98
+ self.pm._publish.assert_called_with(
99
+ {
100
+ 'method': 'emit',
101
+ 'event': 'foo',
102
+ 'binary': True,
103
+ 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'],
104
+ 'namespace': '/',
105
+ 'room': None,
106
+ 'skip_sid': None,
107
+ 'callback': None,
108
+ 'host_id': '123456',
109
+ }
110
+ )
111
+
81
112
  def test_emit_with_to(self):
82
113
  sid = "ferris"
83
114
  self.pm.emit('foo', 'bar', to=sid)
@@ -85,6 +116,7 @@ class TestPubSubManager:
85
116
  {
86
117
  'method': 'emit',
87
118
  'event': 'foo',
119
+ 'binary': False,
88
120
  'data': 'bar',
89
121
  'namespace': '/',
90
122
  'room': sid,
@@ -100,6 +132,7 @@ class TestPubSubManager:
100
132
  {
101
133
  'method': 'emit',
102
134
  'event': 'foo',
135
+ 'binary': False,
103
136
  'data': 'bar',
104
137
  'namespace': '/baz',
105
138
  'room': None,
@@ -115,6 +148,7 @@ class TestPubSubManager:
115
148
  {
116
149
  'method': 'emit',
117
150
  'event': 'foo',
151
+ 'binary': False,
118
152
  'data': 'bar',
119
153
  'namespace': '/',
120
154
  'room': 'baz',
@@ -130,6 +164,7 @@ class TestPubSubManager:
130
164
  {
131
165
  'method': 'emit',
132
166
  'event': 'foo',
167
+ 'binary': False,
133
168
  'data': 'bar',
134
169
  'namespace': '/',
135
170
  'room': None,
@@ -148,6 +183,7 @@ class TestPubSubManager:
148
183
  {
149
184
  'method': 'emit',
150
185
  'event': 'foo',
186
+ 'binary': False,
151
187
  'data': 'bar',
152
188
  'namespace': '/',
153
189
  'room': 'baz',
@@ -250,6 +286,35 @@ class TestPubSubManager:
250
286
  callback=None,
251
287
  )
252
288
 
289
+ def test_handle_emit_binary(self):
290
+ with mock.patch.object(manager.Manager, 'emit') as super_emit:
291
+ self.pm._handle_emit({
292
+ 'event': 'foo',
293
+ 'binary': True,
294
+ 'data': [{'_placeholder': True, 'num': 0}, 'YmFy'],
295
+ })
296
+ super_emit.assert_called_once_with(
297
+ 'foo',
298
+ b'bar',
299
+ namespace=None,
300
+ room=None,
301
+ skip_sid=None,
302
+ callback=None,
303
+ )
304
+ self.pm._handle_emit({
305
+ 'event': 'foo',
306
+ 'binary': True,
307
+ 'data': [{'foo': {'_placeholder': True, 'num': 0}}, 'YmFy'],
308
+ })
309
+ super_emit.assert_called_with(
310
+ 'foo',
311
+ {'foo': b'bar'},
312
+ namespace=None,
313
+ room=None,
314
+ skip_sid=None,
315
+ callback=None,
316
+ )
317
+
253
318
  def test_handle_emit_with_namespace(self):
254
319
  with mock.patch.object(manager.Manager, 'emit') as super_emit:
255
320
  self.pm._handle_emit(
@@ -1,5 +1,5 @@
1
1
  [tox]
2
- envlist=flake8,py{38,39,310,311,312,313},docs
2
+ envlist=flake8,py{38,39,310,311,312,313,314},docs
3
3
  skip_missing_interpreters=True
4
4
 
5
5
  [gh-actions]
@@ -10,6 +10,7 @@ python =
10
10
  3.11: py311
11
11
  3.12: py312
12
12
  3.13: py313
13
+ 3.14: py314
13
14
  pypy-3: pypy3
14
15
 
15
16
  [testenv]