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/dispatch/router.py CHANGED
@@ -4,17 +4,28 @@ from collections import defaultdict
4
4
  from collections.abc import Awaitable, Callable
5
5
  from dataclasses import dataclass
6
6
  from enum import Enum
7
- from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar
8
-
7
+ from typing import (
8
+ TYPE_CHECKING,
9
+ Any,
10
+ Concatenate,
11
+ Generic,
12
+ ParamSpec,
13
+ Protocol,
14
+ TypeAlias,
15
+ TypeVar,
16
+ )
17
+
18
+ from pymax.filters import Filter
9
19
  from pymax.types import MessageDeleteEvent
10
20
 
11
21
  from .enums import EventType
12
22
 
13
23
  if TYPE_CHECKING:
14
- from pymax import Client
15
24
  from pymax.base import BaseClient
25
+ from pymax.client import Client
26
+ from pymax.dispatch.middleware import Middleware
16
27
  from pymax.protocol import InboundFrame
17
- from pymax.types import Chat
28
+ from pymax.types import Chat, StoryPreview
18
29
  from pymax.types.domain import Message
19
30
  from pymax.types.events import (
20
31
  MessageReadEvent,
@@ -33,19 +44,33 @@ class ErrorScope(str, Enum):
33
44
 
34
45
  _EventT = TypeVar("_EventT")
35
46
  ClientT = TypeVar("ClientT", bound="BaseClient")
47
+ _P = ParamSpec("_P")
36
48
 
37
49
  HandlerCallback: TypeAlias = Callable[
38
50
  [_EventT, ClientT],
39
51
  Awaitable[Any] | Any,
40
52
  ]
41
- HandlerDecorator: TypeAlias = Callable[
42
- [HandlerCallback[_EventT, ClientT]],
43
- HandlerCallback[_EventT, ClientT],
44
- ]
45
- FilterCallback: TypeAlias = Callable[
46
- [_EventT],
47
- Awaitable[bool] | bool,
48
- ]
53
+ # HandlerDecorator: TypeAlias = Callable[
54
+ # [HandlerCallback[_EventT, ClientT]],
55
+ # HandlerCallback[_EventT, ClientT],
56
+ # ]
57
+
58
+ HandlerResult: TypeAlias = Awaitable[Any] | Any
59
+
60
+
61
+ class HandlerDecorator(Protocol, Generic[_EventT, ClientT]):
62
+ def __call__(
63
+ self,
64
+ handler: Callable[
65
+ Concatenate[_EventT, ClientT, _P],
66
+ HandlerResult,
67
+ ],
68
+ ) -> Callable[
69
+ Concatenate[_EventT, ClientT, _P],
70
+ HandlerResult,
71
+ ]: ...
72
+
73
+
49
74
  StartCallback: TypeAlias = Callable[
50
75
  [ClientT],
51
76
  Awaitable[Any] | Any,
@@ -91,7 +116,7 @@ DisconnectDecorator: TypeAlias = Callable[
91
116
  @dataclass(slots=True)
92
117
  class HandlerEntry(Generic[_EventT, ClientT]):
93
118
  callback: HandlerCallback[_EventT, ClientT]
94
- filters: tuple[FilterCallback[_EventT], ...] = ()
119
+ filters: tuple[Filter, ...] = ()
95
120
 
96
121
 
97
122
  @dataclass(slots=True)
@@ -107,20 +132,20 @@ class Router(Generic[ClientT]):
107
132
  """Контейнер обработчиков событий PyMax.
108
133
 
109
134
  Роутер хранит обработчики и фильтры. Когда приходит событие, dispatcher
110
- проходит по root router и его дочерним роутерам, проверяет фильтры и
111
- вызывает подходящие callbacks как ``handler(event, client)``.
135
+ проверяет обработчики в порядке регистрации и вызывает первый подходящий
136
+ в каждом роутере, затем обходит его дочерние роутеры. Первые два аргумента
137
+ обработчика — ``event`` и ``client``; дополнительные обязательные параметры
138
+ разрешаются по аннотациям типов через DI.
112
139
 
113
140
  Example:
114
141
  .. code-block:: python
115
142
 
116
143
  from pymax import Client, Message, Router
144
+ from pymax.filters import Command
117
145
 
118
146
  router = Router[Client]()
119
147
 
120
- def is_start(message: Message) -> bool:
121
- return message.text == "/start"
122
-
123
- @router.on_message(is_start)
148
+ @router.on_message(Command("start"))
124
149
  async def start(message: Message, client: Client) -> None:
125
150
  await message.answer("Привет")
126
151
 
@@ -135,6 +160,7 @@ class Router(Generic[ClientT]):
135
160
  ] = defaultdict(list)
136
161
 
137
162
  self.children: list[Router[ClientT]] = []
163
+ self.middlewares: list[Middleware[ClientT]] = []
138
164
  self.on_start_handlers: list[StartCallback[ClientT]] = []
139
165
  self.error_handlers: list[ErrorEntry[ClientT]] = []
140
166
  self.disconnect_handlers: list[DisconnectCallback] = []
@@ -147,6 +173,8 @@ class Router(Generic[ClientT]):
147
173
 
148
174
  ``GLOBAL``-handler видит ошибки всего дерева подключенных router-ов.
149
175
  ``LOCAL``-handler видит только ошибки своего router-а.
176
+ Callback получает ``handler(exception, context)``. При обработке
177
+ событий сюда передаются ошибки фильтров, middleware и handler-ов.
150
178
  """
151
179
  scope = ErrorScope(scope)
152
180
 
@@ -173,13 +201,15 @@ class Router(Generic[ClientT]):
173
201
  self,
174
202
  event: EventType,
175
203
  /,
176
- *filters: FilterCallback[_EventT],
204
+ *filters: Filter[_EventT, ClientT],
177
205
  ) -> HandlerDecorator[_EventT, ClientT]:
178
206
  """Регистрирует обработчик события по ``EventType``.
179
207
 
180
208
  Args:
181
209
  event: Тип события.
182
- *filters: Фильтры, которые получают событие и возвращают bool.
210
+ *filters: Экземпляры ``Filter``, вызываемые как
211
+ ``filter(event, client)``. Все фильтры должны вернуть ``True``;
212
+ async-результаты ожидаются автоматически.
183
213
 
184
214
  Returns:
185
215
  Декоратор для ``handler(event, client)``.
@@ -187,7 +217,7 @@ class Router(Generic[ClientT]):
187
217
  Example:
188
218
  .. code-block:: python
189
219
 
190
- from pymax import Client
220
+ from pymax import Client, EventType
191
221
  from pymax.protocol import InboundFrame
192
222
 
193
223
  @router.on(EventType.RAW)
@@ -196,8 +226,14 @@ class Router(Generic[ClientT]):
196
226
  """
197
227
 
198
228
  def decorator(
199
- handler: HandlerCallback[_EventT, ClientT],
200
- ) -> HandlerCallback[_EventT, ClientT]:
229
+ handler: Callable[
230
+ Concatenate[_EventT, ClientT, _P],
231
+ HandlerResult,
232
+ ],
233
+ ) -> Callable[
234
+ Concatenate[_EventT, ClientT, _P],
235
+ HandlerResult,
236
+ ]:
201
237
  self.handlers[event].append(
202
238
  HandlerEntry(
203
239
  callback=handler,
@@ -234,7 +270,7 @@ class Router(Generic[ClientT]):
234
270
 
235
271
  def on_message(
236
272
  self,
237
- *filters: FilterCallback[Message],
273
+ *filters: Filter[Message, ClientT],
238
274
  ) -> HandlerDecorator[Message, ClientT]:
239
275
  """Регистрирует обработчик новых сообщений.
240
276
 
@@ -248,7 +284,7 @@ class Router(Generic[ClientT]):
248
284
 
249
285
  def on_message_edit(
250
286
  self,
251
- *filters: FilterCallback[Message],
287
+ *filters: Filter[Message, ClientT],
252
288
  ) -> HandlerDecorator[Message, ClientT]:
253
289
  """Регистрирует обработчик редактирования сообщений.
254
290
 
@@ -262,7 +298,7 @@ class Router(Generic[ClientT]):
262
298
 
263
299
  def on_message_delete(
264
300
  self,
265
- *filters: FilterCallback[MessageDeleteEvent],
301
+ *filters: Filter[MessageDeleteEvent, ClientT],
266
302
  ) -> HandlerDecorator[MessageDeleteEvent, ClientT]:
267
303
  """Регистрирует обработчик удаления сообщений.
268
304
 
@@ -276,35 +312,38 @@ class Router(Generic[ClientT]):
276
312
 
277
313
  def on_message_read(
278
314
  self,
279
- *filters: FilterCallback[MessageReadEvent],
315
+ *filters: Filter[MessageReadEvent, ClientT],
280
316
  ) -> HandlerDecorator[MessageReadEvent, ClientT]:
281
317
  """Регистрирует обработчик изменения отметки прочтения."""
282
318
  return self.on(EventType.MESSAGE_READ, *filters)
283
319
 
284
320
  def on_typing(
285
321
  self,
286
- *filters: FilterCallback[TypingEvent],
322
+ *filters: Filter[TypingEvent, ClientT],
287
323
  ) -> HandlerDecorator[TypingEvent, ClientT]:
288
324
  """Регистрирует обработчик набора текста."""
289
325
  return self.on(EventType.TYPING, *filters)
290
326
 
291
327
  def on_presence(
292
328
  self,
293
- *filters: FilterCallback[PresenceEvent],
329
+ *filters: Filter[PresenceEvent, ClientT],
294
330
  ) -> HandlerDecorator[PresenceEvent, ClientT]:
295
331
  """Регистрирует обработчик изменения присутствия пользователя."""
296
332
  return self.on(EventType.PRESENCE, *filters)
297
333
 
298
334
  def on_reaction_update(
299
335
  self,
300
- *filters: FilterCallback[ReactionUpdateEvent],
336
+ *filters: Filter[ReactionUpdateEvent, ClientT],
301
337
  ) -> HandlerDecorator[ReactionUpdateEvent, ClientT]:
302
- """Регистрирует обработчик обновления реакций сообщения."""
338
+ """Регистрирует обработчик ``ReactionUpdateEvent``.
339
+
340
+ Получает обновления счетчиков реакций и реакции текущего пользователя.
341
+ """
303
342
  return self.on(EventType.REACTION_UPDATE, *filters)
304
343
 
305
344
  def on_chat_update(
306
345
  self,
307
- *filters: FilterCallback[Chat],
346
+ *filters: Filter[Chat, ClientT],
308
347
  ) -> HandlerDecorator[Chat, ClientT]:
309
348
  """Регистрирует обработчик обновления чата.
310
349
 
@@ -316,12 +355,25 @@ class Router(Generic[ClientT]):
316
355
  """
317
356
  return self.on(EventType.CHAT_UPDATE, *filters)
318
357
 
358
+ def on_story_update(
359
+ self,
360
+ *filters: Filter[StoryPreview, ClientT],
361
+ ) -> HandlerDecorator[StoryPreview, ClientT]:
362
+ """Регистрирует обработчик обновления ``StoryPreview`` владельца.
363
+
364
+ Первый аргумент handler-а содержит сводное превью, а не ``Story``.
365
+ """
366
+ return self.on(EventType.STORY_UPDATE, *filters)
367
+
319
368
  def on_raw(
320
369
  self,
321
- *filters: FilterCallback[InboundFrame],
370
+ *filters: Filter[InboundFrame, ClientT],
322
371
  ) -> HandlerDecorator[InboundFrame, ClientT]:
323
372
  """Регистрирует обработчик исходных frame-ов.
324
373
 
374
+ Вызывается после типизированной обработки, даже если тип события
375
+ неизвестен или его обработка завершилась ошибкой.
376
+
325
377
  Args:
326
378
  *filters: Фильтры для ``InboundFrame``.
327
379
 
@@ -0,0 +1,15 @@
1
+ from .base import Filter
2
+ from .magic import MagicFilter
3
+ from .presets import Command, Forwarded, FromMe, HasAttachment, Reply
4
+
5
+ F = MagicFilter()
6
+
7
+ __all__ = (
8
+ "Command",
9
+ "Filter",
10
+ "Forwarded",
11
+ "FromMe",
12
+ "HasAttachment",
13
+ "MagicFilter",
14
+ "Reply",
15
+ )
pymax/filters/base.py ADDED
@@ -0,0 +1,218 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from inspect import isawaitable
5
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Awaitable
9
+
10
+ from pymax.base import BaseClient
11
+
12
+
13
+ MISSING = object()
14
+
15
+ EventT = TypeVar("EventT")
16
+ ClientT = TypeVar("ClientT", bound="BaseClient")
17
+
18
+
19
+ class Filter(ABC, Generic[EventT, ClientT]):
20
+ """Базовый класс фильтра событий.
21
+
22
+ Реализуйте ``__call__(event, client)`` с результатом ``bool`` или
23
+ ``Awaitable[bool]`` и передайте экземпляр в декоратор роутера. Фильтры
24
+ объединяются через ``&``, ``|`` и ``~``; составной фильтр при ручном вызове
25
+ нужно ожидать через ``await``.
26
+ """
27
+
28
+ @abstractmethod
29
+ def __call__(
30
+ self,
31
+ event: EventT,
32
+ client: ClientT,
33
+ /,
34
+ ) -> bool | Awaitable[bool]: ...
35
+ def __and__(self, other: Filter[EventT, ClientT]) -> AndFilter:
36
+ return AndFilter(self, other)
37
+
38
+ def __or__(self, value: Filter[EventT, ClientT]) -> OrFilter:
39
+ return OrFilter(self, value)
40
+
41
+ def __invert__(self) -> NotFilter:
42
+ return NotFilter(self)
43
+
44
+ async def resolve_filter_result(
45
+ self,
46
+ result: bool | Awaitable[bool],
47
+ ) -> bool:
48
+ if isawaitable(result):
49
+ return await result
50
+
51
+ return result
52
+
53
+
54
+ class PathResolver:
55
+ def __init__(self, path: list[str]) -> None:
56
+ self.path = path
57
+
58
+ def resolve_path(self, event: Any) -> Any | None:
59
+ value = event
60
+
61
+ for attr in self.path:
62
+ value = getattr(value, attr, MISSING)
63
+
64
+ if value is MISSING:
65
+ return MISSING
66
+
67
+ return value
68
+
69
+
70
+ class IsNoneFilter(Filter[EventT, ClientT], PathResolver):
71
+ def __init__(
72
+ self,
73
+ path: list[str],
74
+ expected_type: type[EventT],
75
+ ) -> None:
76
+ super().__init__(path)
77
+ self.expected_type = expected_type
78
+ self.path = path
79
+
80
+ def __call__(self, event: EventT, _: ClientT) -> bool:
81
+ if not isinstance(event, self.expected_type):
82
+ return False
83
+
84
+ value = self.resolve_path(event)
85
+
86
+ if value is MISSING:
87
+ return False
88
+
89
+ return value is None
90
+
91
+
92
+ class IsEmptyFilter(Filter[EventT, ClientT], PathResolver):
93
+ def __init__(
94
+ self,
95
+ path: list[str],
96
+ expected_type: type[EventT],
97
+ ) -> None:
98
+ super().__init__(path)
99
+ self.expected_type = expected_type
100
+ self.path = path
101
+
102
+ def __call__(self, event: EventT, _: ClientT) -> bool:
103
+ if not isinstance(event, self.expected_type):
104
+ return False
105
+
106
+ value = self.resolve_path(event)
107
+
108
+ return value == []
109
+
110
+
111
+ class FieldRef(Filter[EventT, ClientT], PathResolver):
112
+ def __init__(self, path: list[str], expected_type: type[EventT]) -> None:
113
+ super().__init__(path)
114
+ self.expected_type: type[EventT] = expected_type
115
+ self.path = path
116
+
117
+ def __eq__(self, value: object) -> EqFilter: # pyright: ignore[reportIncompatibleMethodOverride] # ty: ignore[invalid-method-override]
118
+ return EqFilter(self.path, self.expected_type, value)
119
+
120
+ def __ne__(self, value: object) -> NeFilter: # pyright: ignore[reportIncompatibleMethodOverride] # ty: ignore[invalid-method-override]
121
+ return NeFilter(self.path, self.expected_type, value)
122
+
123
+ def is_none(self) -> IsNoneFilter:
124
+ return IsNoneFilter(self.path, expected_type=self.expected_type)
125
+
126
+ def is_empty(self) -> IsEmptyFilter:
127
+ return IsEmptyFilter(self.path, expected_type=self.expected_type)
128
+
129
+ def __call__(self, event: EventT, _: ClientT, /) -> bool:
130
+ if not isinstance(event, self.expected_type):
131
+ return False
132
+
133
+ value = self.resolve_path(event)
134
+
135
+ if value is MISSING:
136
+ return False
137
+
138
+ return bool(value)
139
+
140
+
141
+ class BinaryFilter(Filter[EventT, ClientT]):
142
+ def __init__(self, left: Filter[EventT, ClientT], right: Filter[EventT, ClientT]) -> None:
143
+ self.left = left
144
+ self.right = right
145
+
146
+
147
+ class NotFilter(Filter[EventT, ClientT]):
148
+ def __init__(self, filter_instance: Filter[EventT, ClientT]) -> None:
149
+ self.filter = filter_instance
150
+
151
+ async def __call__(self, event: EventT, client: ClientT) -> bool:
152
+ return not await self.resolve_filter_result(self.filter(event, client))
153
+
154
+
155
+ class NeFilter(Filter[EventT, ClientT], PathResolver):
156
+ def __init__(
157
+ self,
158
+ path: list[str],
159
+ expected_type: type[EventT],
160
+ expected: object,
161
+ ) -> None:
162
+ self.path: list[str] = path
163
+ self.expected_type = expected_type
164
+ self.expected: object = expected
165
+
166
+ def __call__(self, event: EventT, _: ClientT) -> bool:
167
+ if not isinstance(event, self.expected_type):
168
+ return False
169
+
170
+ value = self.resolve_path(event)
171
+
172
+ if value is MISSING:
173
+ return False
174
+
175
+ return value != self.expected
176
+
177
+
178
+ class AndFilter(BinaryFilter[EventT, ClientT]):
179
+ async def __call__(self, event: EventT, client: ClientT) -> bool:
180
+ left = await self.resolve_filter_result(self.left(event, client))
181
+
182
+ if not left:
183
+ return False
184
+
185
+ return await self.resolve_filter_result(self.right(event, client))
186
+
187
+
188
+ class OrFilter(BinaryFilter[EventT, ClientT]):
189
+ async def __call__(self, event: EventT, client: ClientT) -> bool:
190
+ left = await self.resolve_filter_result(self.left(event, client))
191
+
192
+ if left:
193
+ return True
194
+
195
+ return await self.resolve_filter_result(self.right(event, client))
196
+
197
+
198
+ class EqFilter(Filter[EventT, ClientT], PathResolver):
199
+ def __init__(
200
+ self,
201
+ path: list[str],
202
+ expected_type: type[EventT],
203
+ expected: object,
204
+ ) -> None:
205
+ self.path: list[str] = path
206
+ self.expected_type = expected_type
207
+ self.expected: object = expected
208
+
209
+ def __call__(self, event: EventT, _: ClientT) -> bool:
210
+ if not isinstance(event, self.expected_type):
211
+ return False
212
+
213
+ value = self.resolve_path(event)
214
+
215
+ if value is MISSING:
216
+ return False
217
+
218
+ return value == self.expected
@@ -0,0 +1,2 @@
1
+ from .chat import ChatContainer
2
+ from .message import MessageContainer