maxapi-python 2.4.1__py3-none-any.whl → 2.5.0__py3-none-any.whl

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 (74) hide show
  1. {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/METADATA +17 -9
  2. {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/RECORD +74 -45
  3. {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +5 -1
  5. pymax/_data/apk_fingerprints.json +272 -0
  6. pymax/api/auth/service.py +1 -0
  7. pymax/api/auth/types.py +3 -0
  8. pymax/api/bots/payloads.py +8 -0
  9. pymax/api/bots/service.py +13 -1
  10. pymax/api/chats/enums.py +1 -0
  11. pymax/api/chats/payloads.py +19 -0
  12. pymax/api/chats/service.py +34 -0
  13. pymax/api/facade.py +2 -0
  14. pymax/api/messages/enums.py +1 -0
  15. pymax/api/messages/payloads.py +47 -1
  16. pymax/api/messages/service.py +308 -24
  17. pymax/api/response.py +1 -1
  18. pymax/api/stories/__init__.py +2 -0
  19. pymax/api/stories/enums.py +19 -0
  20. pymax/api/stories/payloads.py +64 -0
  21. pymax/api/stories/service.py +198 -0
  22. pymax/api/uploads/payloads.py +10 -1
  23. pymax/api/uploads/service.py +94 -7
  24. pymax/app.py +16 -4
  25. pymax/auth/qr.py +11 -1
  26. pymax/base.py +70 -25
  27. pymax/client.py +0 -1
  28. pymax/config.py +38 -32
  29. pymax/di/__init__.py +2 -0
  30. pymax/di/context.py +18 -0
  31. pymax/di/resolver.py +38 -0
  32. pymax/dispatch/__init__.py +7 -0
  33. pymax/dispatch/dispatcher.py +143 -40
  34. pymax/dispatch/enums.py +1 -0
  35. pymax/dispatch/mapping.py +23 -3
  36. pymax/dispatch/middleware.py +32 -0
  37. pymax/dispatch/resolvers.py +4 -0
  38. pymax/dispatch/router.py +86 -34
  39. pymax/filters/__init__.py +15 -0
  40. pymax/filters/base.py +218 -0
  41. pymax/filters/containers/__init__.py +2 -0
  42. pymax/filters/containers/chat.py +240 -0
  43. pymax/filters/containers/message.py +147 -0
  44. pymax/filters/filters.py +211 -0
  45. pymax/filters/magic.py +13 -0
  46. pymax/filters/presets.py +88 -0
  47. pymax/filters/refs.py +56 -0
  48. pymax/formatting/markdown.py +55 -1
  49. pymax/fsm/__init__.py +3 -0
  50. pymax/fsm/context.py +30 -0
  51. pymax/fsm/protocol.py +14 -0
  52. pymax/fsm/state.py +70 -0
  53. pymax/fsm/storage.py +151 -0
  54. pymax/infra/base.py +2 -0
  55. pymax/infra/bots.py +20 -0
  56. pymax/infra/chat.py +28 -0
  57. pymax/infra/message.py +195 -6
  58. pymax/infra/stories.py +205 -0
  59. pymax/integrations/__init__.py +0 -0
  60. pymax/integrations/dishka.py +109 -0
  61. pymax/protocol/enums.py +2 -0
  62. pymax/protocol/tcp/payload.py +0 -2
  63. pymax/types/domain/__init__.py +23 -1
  64. pymax/types/domain/attachments/__init__.py +1 -1
  65. pymax/types/domain/attachments/keyboards/__init__.py +1 -1
  66. pymax/types/domain/attachments/keyboards/enums.py +12 -0
  67. pymax/types/domain/attachments/keyboards/inline.py +74 -3
  68. pymax/types/domain/callback.py +19 -0
  69. pymax/types/domain/comment.py +24 -0
  70. pymax/types/domain/message.py +91 -22
  71. pymax/types/domain/story.py +126 -0
  72. pymax/types/events/reaction.py +28 -4
  73. pymax/versions/catalog.py +1 -1
  74. {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/licenses/LICENSE +0 -0
pymax/base.py CHANGED
@@ -4,9 +4,10 @@ import asyncio
4
4
  from abc import ABC, abstractmethod
5
5
  from typing import TYPE_CHECKING, Any, Generic, TypeVar
6
6
 
7
- from pymax.dispatch import ErrorScope, Router
7
+ from pymax.dispatch import ErrorScope, Middleware, Router
8
8
  from pymax.dispatch.router import DisconnectDecorator, ErrorDecorator
9
9
  from pymax.exceptions import ApiError
10
+ from pymax.filters import Filter
10
11
  from pymax.fingerprint.models import ApkBuildFingerprint
11
12
  from pymax.infra import BaseMixin
12
13
  from pymax.logging import get_logger
@@ -19,7 +20,6 @@ if TYPE_CHECKING:
19
20
  from pymax.auth import AuthFlow
20
21
  from pymax.connection import ConnectionManager
21
22
  from pymax.dispatch.router import (
22
- FilterCallback,
23
23
  HandlerDecorator,
24
24
  StartDecorator,
25
25
  )
@@ -30,6 +30,7 @@ if TYPE_CHECKING:
30
30
  MessageReadEvent,
31
31
  PresenceEvent,
32
32
  ReactionUpdateEvent,
33
+ StoryPreview,
33
34
  TypingEvent,
34
35
  User,
35
36
  )
@@ -125,7 +126,6 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
125
126
 
126
127
  @abstractmethod
127
128
  def _build_connection(self) -> ConnectionManager:
128
-
129
129
  raise NotImplementedError
130
130
 
131
131
  @abstractmethod
@@ -140,7 +140,10 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
140
140
  return
141
141
 
142
142
  self._connection = self._build_connection()
143
- self._app = self._build_app()
143
+ if self.__app is not None:
144
+ self._app.bind_connection(self._connection)
145
+ else:
146
+ self._app = self._build_app()
144
147
 
145
148
  def _init_runtime(self: ClientT, *, auth_flow: AuthFlow) -> None: # noqa: PYI019
146
149
  self._connection = self._build_connection()
@@ -158,14 +161,16 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
158
161
  app.dispatcher.bind_client(self)
159
162
  return app
160
163
 
161
- def _reset_runtime(self: ClientT) -> None: # noqa: PYI019
164
+ def _reset_connection(self: ClientT) -> None: # noqa: PYI019
162
165
  self._connection = self._build_connection()
163
- self._app = self._build_app()
166
+ self._app.bind_connection(self._connection)
167
+ # self._app = self._build_app()
164
168
 
165
169
  async def connect(self: ClientT) -> None: # noqa: PYI019
166
- """Подключает клиента один раз и возвращается после ``on_start``.
170
+ """Подключает клиента один раз и запускает ``on_start``-обработчики.
167
171
 
168
- После возврата соединение остается открытым. В отличие от
172
+ Обработчики старта выполняются в отдельных задачах; метод не ждет
173
+ их завершения. После возврата соединение остается открытым. В отличие от
169
174
  :meth:`start`, метод не ожидает закрытия соединения и не запускает
170
175
  reconnect loop; завершите работу через :meth:`stop` или :meth:`close`.
171
176
  """
@@ -181,6 +186,9 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
181
186
  await self.close()
182
187
  raise
183
188
  except (ConnectionError, EOFError, OSError, TimeoutError, ApiError) as e:
189
+ if self.__app is None:
190
+ raise
191
+
184
192
  await self.close()
185
193
 
186
194
  await self._app.dispatcher.emit_disconnect(
@@ -189,13 +197,18 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
189
197
  self.extra_config.reconnect_delay,
190
198
  )
191
199
  raise
200
+ except Exception:
201
+ await self.close()
202
+ raise
192
203
 
193
204
  async def start(self: ClientT) -> None: # noqa: PYI019
194
205
  """Запускает клиента и слушает события до закрытия соединения.
195
206
 
196
207
  При сетевых ошибках повторяет подключение, если ``reconnect=True``.
197
208
  Отозванный login token автоматически сбрасывается при ``relogin=True``.
209
+ Если новый token тоже отклонен, ошибка передается вызывающему коду.
198
210
  """
211
+ relogin_attempted = False
199
212
  while True:
200
213
  try:
201
214
  await self._ensure_runtime()
@@ -204,26 +217,38 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
204
217
  await self.close()
205
218
  return
206
219
 
220
+ relogin_attempted = False
207
221
  await self._app.dispatcher.emit_start(self)
208
222
  await self._connection.wait_closed()
209
223
  except asyncio.CancelledError:
210
224
  await self.close()
211
225
  raise
212
226
  except ApiError as e:
213
- if not self._app._is_invalid_login_token_error(e):
227
+ if (
228
+ not self._app._is_invalid_login_token_error(e)
229
+ or not self.extra_config.relogin
230
+ or relogin_attempted
231
+ ):
214
232
  await self.close()
215
233
  raise
216
234
 
217
235
  logger.warning("login token was revoked; starting authentication again")
218
- if self.extra_config.relogin:
219
- await self.relogin(start=False)
236
+ relogin_attempted = True
237
+ await self.relogin(start=False)
220
238
  except (
221
239
  ConnectionError,
222
240
  EOFError,
223
241
  OSError,
224
242
  TimeoutError,
225
243
  ) as e:
226
- await self.close()
244
+ if self.__app is None:
245
+ raise
246
+
247
+ if self.extra_config.reconnect:
248
+ await self._app.disconnect()
249
+ else:
250
+ await self.close()
251
+
227
252
  await self._app.dispatcher.emit_disconnect(
228
253
  e,
229
254
  self.extra_config.reconnect,
@@ -239,7 +264,7 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
239
264
  exc_info=True,
240
265
  )
241
266
  await asyncio.sleep(self.extra_config.reconnect_delay)
242
- self._reset_runtime()
267
+ self._reset_connection()
243
268
  except Exception:
244
269
  await self.close()
245
270
  raise
@@ -270,63 +295,70 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
270
295
 
271
296
  def on_message(
272
297
  self,
273
- *filters: FilterCallback[Message],
298
+ *filters: Filter[Message, ClientT],
274
299
  ) -> HandlerDecorator[Message, ClientT]:
275
300
  """Регистрирует обработчик новых сообщений."""
276
301
  return self._router.on_message(*filters)
277
302
 
278
303
  def on_message_edit(
279
304
  self,
280
- *filters: FilterCallback[Message],
305
+ *filters: Filter[Message, ClientT],
281
306
  ) -> HandlerDecorator[Message, ClientT]:
282
307
  """Регистрирует обработчик редактирования сообщений."""
283
308
  return self._router.on_message_edit(*filters)
284
309
 
285
310
  def on_message_delete(
286
311
  self,
287
- *filters: FilterCallback[MessageDeleteEvent],
312
+ *filters: Filter[MessageDeleteEvent, ClientT],
288
313
  ) -> HandlerDecorator[MessageDeleteEvent, ClientT]:
289
314
  """Регистрирует обработчик удаления сообщений."""
290
315
  return self._router.on_message_delete(*filters)
291
316
 
292
317
  def on_message_read(
293
318
  self,
294
- *filters: FilterCallback[MessageReadEvent],
319
+ *filters: Filter[MessageReadEvent, ClientT],
295
320
  ) -> HandlerDecorator[MessageReadEvent, ClientT]:
296
321
  """Регистрирует обработчик изменения отметки прочтения."""
297
322
  return self._router.on_message_read(*filters)
298
323
 
299
324
  def on_typing(
300
325
  self,
301
- *filters: FilterCallback[TypingEvent],
326
+ *filters: Filter[TypingEvent, ClientT],
302
327
  ) -> HandlerDecorator[TypingEvent, ClientT]:
303
328
  """Регистрирует обработчик набора текста."""
304
329
  return self._router.on_typing(*filters)
305
330
 
306
331
  def on_presence(
307
332
  self,
308
- *filters: FilterCallback[PresenceEvent],
333
+ *filters: Filter[PresenceEvent, ClientT],
309
334
  ) -> HandlerDecorator[PresenceEvent, ClientT]:
310
335
  """Регистрирует обработчик изменения присутствия пользователя."""
311
336
  return self._router.on_presence(*filters)
312
337
 
313
338
  def on_reaction_update(
314
339
  self,
315
- *filters: FilterCallback[ReactionUpdateEvent],
340
+ *filters: Filter[ReactionUpdateEvent, ClientT],
316
341
  ) -> HandlerDecorator[ReactionUpdateEvent, ClientT]:
317
342
  """Регистрирует обработчик обновления реакций сообщения."""
318
343
  return self._router.on_reaction_update(*filters)
319
344
 
320
345
  def on_chat_update(
321
346
  self,
322
- *filters: FilterCallback[Chat],
347
+ *filters: Filter[Chat, ClientT],
323
348
  ) -> HandlerDecorator[Chat, ClientT]:
324
349
  """Регистрирует обработчик обновления чата."""
325
350
  return self._router.on_chat_update(*filters)
326
351
 
352
+ def on_story_update(
353
+ self,
354
+ *filters: Filter[StoryPreview, ClientT],
355
+ ) -> HandlerDecorator[StoryPreview, ClientT]:
356
+ """Регистрирует обработчик обновления превью сторис владельца."""
357
+ return self._router.on_story_update(*filters)
358
+
327
359
  def on_raw(
328
360
  self,
329
- *filters: FilterCallback[InboundFrame],
361
+ *filters: Filter[InboundFrame, ClientT],
330
362
  ) -> HandlerDecorator[InboundFrame, ClientT]:
331
363
  """Регистрирует обработчик исходных входящих frame-ов."""
332
364
  return self._router.on_raw(*filters)
@@ -343,12 +375,23 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
343
375
  """Подключает дочерний router к root router клиента."""
344
376
  self._router.include_router(router)
345
377
 
378
+ def add_middleware(self, middleware: Middleware[ClientT]) -> None:
379
+ """Добавляет middleware в цепочку обработчиков клиента.
380
+
381
+ Middleware вызываются в порядке регистрации после проверки фильтров.
382
+ Регистрация доступна до запуска клиента и сохраняется при reconnect.
383
+ """
384
+ if middleware not in self._router.middlewares:
385
+ self._router.middlewares.append(middleware)
386
+ else:
387
+ logger.warning(f"Middleware {middleware.__class__.__name__} is already registered")
388
+
346
389
  async def relogin(self: ClientT, drop_config_token: bool = True, start: bool = True) -> None: # noqa: PYI019
347
390
  """Удаляет текущую локальную сессию и запускает авторизацию заново.
348
391
 
349
392
  Args:
350
393
  drop_config_token: Сбросить token, переданный через ``ExtraConfig``.
351
- start: Сразу запустить клиента после сброса runtime.
394
+ start: Сразу запустить клиента после сброса сессии.
352
395
  """
353
396
  store = self._app.store
354
397
  session = self._app.session
@@ -357,13 +400,15 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
357
400
  raise RuntimeError("Cannot relogin before session is loaded")
358
401
 
359
402
  await store.delete_session(session.token)
360
- await self.close()
403
+ await self._app.disconnect()
361
404
 
362
405
  if drop_config_token:
363
406
  self.extra_config.token = None
364
407
  self._config.token = None
365
408
 
366
- self._reset_runtime()
409
+ self._reset_connection()
410
+
411
+ self._app.session = None
367
412
 
368
413
  if start:
369
414
  await self.start()
pymax/client.py CHANGED
@@ -63,7 +63,6 @@ class Client(BaseClient["Client"]):
63
63
  app_version: str = VersionCatalog.RECOMMENDED_APP_VERSION,
64
64
  catalog: VersionCatalog | None = None,
65
65
  ) -> None:
66
-
67
66
  self.phone: str = phone
68
67
  self.extra_config: ExtraConfig = extra_config or ExtraConfig()
69
68
  self.session_name: str = session_name
pymax/config.py CHANGED
@@ -57,42 +57,48 @@ APP_VERSIONS: tuple[tuple[str, int], ...] = (
57
57
  # ("26.9.1", 6643),
58
58
  )
59
59
  ANDROID_DEVICES: tuple[tuple[str, str, str, str], ...] = (
60
- ("Samsung SM-A525F", "Android 13", "405dpi 405dpi 1080x2400", "arm64-v8a"),
61
- ("Samsung SM-A536B", "Android 14", "405dpi 405dpi 1080x2400", "arm64-v8a"),
62
- ("Samsung SM-A546E", "Android 14", "405dpi 405dpi 1080x2340", "arm64-v8a"),
63
- ("Samsung SM-G991B", "Android 14", "421dpi 421dpi 1080x2400", "arm64-v8a"),
64
- ("Samsung SM-G998B", "Android 13", "515dpi 515dpi 1440x3200", "arm64-v8a"),
65
- ("Samsung SM-S901B", "Android 14", "425dpi 425dpi 1080x2340", "arm64-v8a"),
66
- ("Samsung SM-S911B", "Android 14", "425dpi 425dpi 1080x2340", "arm64-v8a"),
67
- ("Xiaomi 2109119DG", "Android 13", "395dpi 395dpi 1080x2400", "arm64-v8a"),
68
- ("Xiaomi 2201117TG", "Android 13", "395dpi 395dpi 1080x2400", "arm64-v8a"),
69
- ("Xiaomi 2201123G", "Android 14", "526dpi 526dpi 1440x3200", "arm64-v8a"),
70
- ("Xiaomi 2210132G", "Android 14", "446dpi 446dpi 1220x2712", "arm64-v8a"),
71
60
  (
72
- "Xiaomi 23049PCD8G",
61
+ "Google Pixel 6a",
62
+ "Android 13",
63
+ "420dpi 420dpi 1080x2400",
64
+ "arm64-v8a",
65
+ ),
66
+ (
67
+ "Google Pixel 7",
68
+ "Android 13",
69
+ "420dpi 420dpi 1080x2400",
70
+ "arm64-v8a",
71
+ ),
72
+ (
73
+ "Google Pixel 7 Pro",
74
+ "Android 13",
75
+ "560dpi 560dpi 1440x3120",
76
+ "arm64-v8a",
77
+ ),
78
+ (
79
+ "Google Pixel 8",
73
80
  "Android 14",
74
- "446dpi 446dpi 1220x2712",
81
+ "420dpi 420dpi 1080x2400",
82
+ "arm64-v8a",
83
+ ),
84
+ (
85
+ "samsung SM-G991B",
86
+ "Android 14",
87
+ "xxhdpi 480dpi 1080x2400",
88
+ "arm64-v8a",
89
+ ),
90
+ (
91
+ "samsung SM-S911B",
92
+ "Android 15",
93
+ "xxhdpi 480dpi 1080x2340",
94
+ "arm64-v8a",
95
+ ),
96
+ (
97
+ "Xiaomi 2201117TG",
98
+ "Android 13",
99
+ "440dpi 440dpi 1080x2400",
75
100
  "arm64-v8a",
76
101
  ),
77
- ("Redmi 2201116TG", "Android 13", "395dpi 395dpi 1080x2400", "arm64-v8a"),
78
- ("Redmi 22101316G", "Android 13", "395dpi 395dpi 1080x2400", "arm64-v8a"),
79
- ("Redmi 23021RAA2Y", "Android 14", "395dpi 395dpi 1080x2400", "arm64-v8a"),
80
- ("POCO 22011211G", "Android 13", "395dpi 395dpi 1080x2400", "arm64-v8a"),
81
- ("POCO 23049PCD8G", "Android 14", "446dpi 446dpi 1220x2712", "arm64-v8a"),
82
- ("Pixel 6", "Android 14", "411dpi 411dpi 1080x2400", "arm64-v8a"),
83
- ("Pixel 6a", "Android 14", "429dpi 429dpi 1080x2400", "arm64-v8a"),
84
- ("Pixel 7", "Android 14", "416dpi 416dpi 1080x2400", "arm64-v8a"),
85
- ("Pixel 7 Pro", "Android 14", "512dpi 512dpi 1440x3120", "arm64-v8a"),
86
- ("Pixel 8", "Android 14", "428dpi 428dpi 1080x2400", "arm64-v8a"),
87
- ("OnePlus NE2213", "Android 14", "525dpi 525dpi 1440x3216", "arm64-v8a"),
88
- ("OnePlus CPH2449", "Android 14", "451dpi 451dpi 1240x2772", "arm64-v8a"),
89
- ("realme RMX3085", "Android 13", "409dpi 409dpi 1080x2400", "arm64-v8a"),
90
- ("realme RMX3370", "Android 13", "409dpi 409dpi 1080x2400", "arm64-v8a"),
91
- ("realme RMX3630", "Android 13", "400dpi 400dpi 1080x2412", "arm64-v8a"),
92
- ("HUAWEI ELS-NX9", "Android 12", "441dpi 441dpi 1080x2340", "arm64-v8a"),
93
- ("HUAWEI VOG-L29", "Android 12", "398dpi 398dpi 1080x2340", "arm64-v8a"),
94
- ("HONOR RMO-NX1", "Android 13", "391dpi 391dpi 1080x2388", "arm64-v8a"),
95
- ("HONOR REA-NX9", "Android 13", "435dpi 435dpi 1200x2664", "arm64-v8a"),
96
102
  )
97
103
  LOCALE_TIMEZONES: tuple[tuple[str, str], ...] = (
98
104
  ("ru", "Europe/Moscow"),
pymax/di/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .context import DiContext
2
+ from .resolver import DiResolver
pymax/di/context.py ADDED
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Generic, TypeVar
5
+
6
+ if TYPE_CHECKING:
7
+ from pymax.app import App
8
+ from pymax.base import BaseClient
9
+ from pymax.dispatch.mapping import DispatchEvent
10
+
11
+ ClientT = TypeVar("ClientT", bound="BaseClient")
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class DiContext(Generic[ClientT]):
16
+ app: App[ClientT]
17
+ event: DispatchEvent
18
+ client: ClientT
pymax/di/resolver.py ADDED
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from logging import getLogger
5
+ from typing import Any, TypeAlias
6
+
7
+ from pymax.fsm import FSMContext
8
+ from pymax.types import Message
9
+
10
+ from .context import DiContext
11
+
12
+ logger = getLogger()
13
+
14
+
15
+ async def resolve_fsm(ctx: DiContext) -> FSMContext | None:
16
+ if not isinstance(ctx.event, Message):
17
+ raise ValueError("Fsm is not currently working for non-message handlers.")
18
+
19
+ if not ctx.event.chat_id or not ctx.event.sender:
20
+ logger.warning("Not enough data in message event. Skipping fsm")
21
+ return None
22
+
23
+ return ctx.app.fsm_storage.get_context(ctx.client, ctx.event)
24
+
25
+
26
+ Resolver: TypeAlias = Callable[[DiContext], Awaitable[Any]]
27
+
28
+ DI_MAP: dict[type, Resolver] = {FSMContext: resolve_fsm}
29
+
30
+
31
+ class DiResolver:
32
+ async def resolve(self, annotation: Any, ctx: DiContext) -> None | Any:
33
+ resolver = DI_MAP.get(annotation)
34
+
35
+ if resolver is None:
36
+ return None
37
+
38
+ return await resolver(ctx)
@@ -1,11 +1,14 @@
1
1
  from .dispatcher import Dispatcher
2
2
  from .enums import EventType
3
+ from .mapping import DispatchEvent
4
+ from .middleware import Middleware, MiddlewareNext
3
5
  from .router import (
4
6
  ClientRouter,
5
7
  DisconnectCallback,
6
8
  DisconnectDecorator,
7
9
  ErrorContext,
8
10
  ErrorScope,
11
+ HandlerCallback,
9
12
  Router,
10
13
  )
11
14
 
@@ -13,9 +16,13 @@ __all__ = (
13
16
  "ClientRouter",
14
17
  "DisconnectCallback",
15
18
  "DisconnectDecorator",
19
+ "DispatchEvent",
16
20
  "Dispatcher",
17
21
  "ErrorContext",
18
22
  "ErrorScope",
19
23
  "EventType",
24
+ "HandlerCallback",
25
+ "Middleware",
26
+ "MiddlewareNext",
20
27
  "Router",
21
28
  )