maxapi-python 1.2.4__py3-none-any.whl → 2.0.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 (168) hide show
  1. maxapi_python-2.0.0.dist-info/METADATA +217 -0
  2. maxapi_python-2.0.0.dist-info/RECORD +140 -0
  3. {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +50 -105
  5. pymax/api/__init__.py +17 -0
  6. pymax/api/auth/__init__.py +1 -0
  7. pymax/api/auth/enums.py +17 -0
  8. pymax/api/auth/payloads.py +129 -0
  9. pymax/api/auth/service.py +313 -0
  10. pymax/api/auth/types.py +13 -0
  11. pymax/api/chats/__init__.py +8 -0
  12. pymax/api/chats/enums.py +27 -0
  13. pymax/api/chats/payloads.py +103 -0
  14. pymax/api/chats/service.py +277 -0
  15. pymax/api/facade.py +32 -0
  16. pymax/api/messages/__init__.py +1 -0
  17. pymax/api/messages/enums.py +17 -0
  18. pymax/api/messages/payloads.py +92 -0
  19. pymax/api/messages/service.py +337 -0
  20. pymax/api/models.py +13 -0
  21. pymax/api/response.py +123 -0
  22. pymax/api/self/__init__.py +2 -0
  23. pymax/api/self/enums.py +11 -0
  24. pymax/api/self/payloads.py +41 -0
  25. pymax/api/self/service.py +142 -0
  26. pymax/api/session/__init__.py +1 -0
  27. pymax/api/session/enums.py +10 -0
  28. pymax/api/session/payloads.py +76 -0
  29. pymax/api/session/service.py +72 -0
  30. pymax/api/uploads/__init__.py +1 -0
  31. pymax/api/uploads/models.py +49 -0
  32. pymax/api/uploads/payloads.py +25 -0
  33. pymax/api/uploads/service.py +458 -0
  34. pymax/api/users/__init__.py +2 -0
  35. pymax/api/users/enums.py +12 -0
  36. pymax/api/users/payloads.py +16 -0
  37. pymax/api/users/service.py +124 -0
  38. pymax/app.py +273 -0
  39. pymax/auth/__init__.py +25 -0
  40. pymax/auth/base.py +37 -0
  41. pymax/auth/email.py +0 -0
  42. pymax/auth/models.py +5 -0
  43. pymax/auth/providers.py +127 -0
  44. pymax/auth/qr.py +135 -0
  45. pymax/auth/service.py +25 -0
  46. pymax/auth/sms.py +122 -0
  47. pymax/base.py +204 -0
  48. pymax/client.py +106 -0
  49. pymax/client_web.py +83 -0
  50. pymax/config.py +215 -0
  51. pymax/connection/__init__.py +1 -0
  52. pymax/connection/connection.py +205 -0
  53. pymax/connection/pending.py +46 -0
  54. pymax/connection/readers/__init__.py +2 -0
  55. pymax/connection/readers/base.py +6 -0
  56. pymax/connection/readers/tcp.py +29 -0
  57. pymax/connection/readers/ws.py +14 -0
  58. pymax/dispatch/__init__.py +10 -0
  59. pymax/dispatch/dispatcher.py +222 -0
  60. pymax/dispatch/enums.py +12 -0
  61. pymax/dispatch/mapping.py +73 -0
  62. pymax/dispatch/resolvers.py +52 -0
  63. pymax/dispatch/router.py +216 -0
  64. pymax/exceptions.py +22 -89
  65. pymax/files/__init__.py +9 -0
  66. pymax/files/base.py +82 -0
  67. pymax/files/file.py +76 -0
  68. pymax/files/photo.py +108 -0
  69. pymax/files/static.py +10 -0
  70. pymax/files/video.py +74 -0
  71. pymax/formatting/__init__.py +0 -0
  72. pymax/formatting/markdown.py +217 -0
  73. pymax/infra/__init__.py +1 -0
  74. pymax/infra/auth.py +55 -0
  75. pymax/infra/base.py +15 -0
  76. pymax/infra/chat.py +240 -0
  77. pymax/infra/message.py +252 -0
  78. pymax/infra/protocol.py +9 -0
  79. pymax/infra/self.py +139 -0
  80. pymax/infra/user.py +107 -0
  81. pymax/logging.py +129 -0
  82. pymax/protocol/__init__.py +11 -0
  83. pymax/protocol/base.py +13 -0
  84. pymax/{static/enum.py → protocol/enums.py} +36 -79
  85. pymax/protocol/models.py +33 -0
  86. pymax/protocol/tcp/__init__.py +1 -0
  87. pymax/protocol/tcp/compression.py +97 -0
  88. pymax/protocol/tcp/framing.py +68 -0
  89. pymax/protocol/tcp/payload.py +127 -0
  90. pymax/protocol/tcp/protocol.py +68 -0
  91. pymax/protocol/ws/__init__.py +1 -0
  92. pymax/protocol/ws/protocol.py +27 -0
  93. pymax/py.typed +0 -0
  94. pymax/routers.py +8 -0
  95. pymax/session/__init__.py +3 -0
  96. pymax/session/models.py +11 -0
  97. pymax/session/protocol.py +14 -0
  98. pymax/session/store.py +232 -0
  99. pymax/telemetry/__init__.py +3 -0
  100. pymax/telemetry/navigation.py +181 -0
  101. pymax/telemetry/payloads.py +142 -0
  102. pymax/telemetry/service.py +225 -0
  103. pymax/transport/__init__.py +0 -0
  104. pymax/transport/base.py +14 -0
  105. pymax/transport/tcp.py +93 -0
  106. pymax/transport/websocket.py +50 -0
  107. pymax/types/__init__.py +2 -0
  108. pymax/types/domain/__init__.py +11 -0
  109. pymax/types/domain/attachments/__init__.py +11 -0
  110. pymax/types/domain/attachments/audio.py +35 -0
  111. pymax/types/domain/attachments/call.py +26 -0
  112. pymax/types/domain/attachments/contact.py +32 -0
  113. pymax/types/domain/attachments/control.py +20 -0
  114. pymax/types/domain/attachments/enums.py +27 -0
  115. pymax/types/domain/attachments/file.py +56 -0
  116. pymax/types/domain/attachments/keyboards/__init__.py +1 -0
  117. pymax/types/domain/attachments/keyboards/inline.py +19 -0
  118. pymax/types/domain/attachments/photo.py +45 -0
  119. pymax/types/domain/attachments/share.py +29 -0
  120. pymax/types/domain/attachments/sticker.py +50 -0
  121. pymax/types/domain/attachments/video.py +90 -0
  122. pymax/types/domain/auth.py +161 -0
  123. pymax/types/domain/base.py +17 -0
  124. pymax/types/domain/chat.py +426 -0
  125. pymax/types/domain/element.py +24 -0
  126. pymax/types/domain/enums.py +24 -0
  127. pymax/types/domain/error.py +20 -0
  128. pymax/types/domain/folder.py +74 -0
  129. pymax/types/domain/login.py +35 -0
  130. pymax/types/domain/message.py +378 -0
  131. pymax/types/domain/name.py +20 -0
  132. pymax/types/domain/profile.py +15 -0
  133. pymax/types/domain/session.py +52 -0
  134. pymax/types/domain/sync.py +80 -0
  135. pymax/types/domain/user.py +117 -0
  136. pymax/types/events/__init__.py +3 -0
  137. pymax/types/events/file.py +5 -0
  138. pymax/types/events/message.py +37 -0
  139. pymax/types/events/video.py +5 -0
  140. maxapi_python-1.2.4.dist-info/METADATA +0 -205
  141. maxapi_python-1.2.4.dist-info/RECORD +0 -33
  142. pymax/core.py +0 -390
  143. pymax/crud.py +0 -96
  144. pymax/files.py +0 -138
  145. pymax/filters.py +0 -164
  146. pymax/formatter.py +0 -31
  147. pymax/formatting.py +0 -74
  148. pymax/interfaces.py +0 -552
  149. pymax/mixins/__init__.py +0 -40
  150. pymax/mixins/auth.py +0 -368
  151. pymax/mixins/channel.py +0 -130
  152. pymax/mixins/group.py +0 -458
  153. pymax/mixins/handler.py +0 -285
  154. pymax/mixins/message.py +0 -879
  155. pymax/mixins/scheduler.py +0 -28
  156. pymax/mixins/self.py +0 -259
  157. pymax/mixins/socket.py +0 -297
  158. pymax/mixins/telemetry.py +0 -112
  159. pymax/mixins/user.py +0 -219
  160. pymax/mixins/websocket.py +0 -142
  161. pymax/models.py +0 -8
  162. pymax/navigation.py +0 -187
  163. pymax/payloads.py +0 -367
  164. pymax/protocols.py +0 -123
  165. pymax/static/constant.py +0 -89
  166. pymax/types.py +0 -1220
  167. pymax/utils.py +0 -90
  168. {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,222 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ from contextlib import suppress
6
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
7
+
8
+ from pymax.logging import get_logger
9
+ from pymax.protocol import InboundFrame
10
+ from pymax.types import Chat, MessageDeleteEvent
11
+ from pymax.types.domain import Message
12
+ from pymax.types.events import FileUploadSignal, VideoUploadSignal
13
+
14
+ from .enums import EventType
15
+ from .mapping import EventMapper, EventResolver
16
+ from .router import (
17
+ FilterCallback,
18
+ HandlerCallback,
19
+ HandlerDecorator,
20
+ HandlerEntry,
21
+ Router,
22
+ StartDecorator,
23
+ )
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Generator
27
+
28
+ from pymax.app import App
29
+ from pymax.client import Client
30
+
31
+
32
+ logger = get_logger(__name__)
33
+
34
+ ClientT = TypeVar("ClientT")
35
+
36
+
37
+ class Dispatcher(Generic[ClientT]):
38
+ def __init__(self, app: App, root_router: Router[ClientT] | None = None) -> None:
39
+ self.root_router: Router[ClientT] = root_router or Router()
40
+ self.internal_router: Router[ClientT] = Router()
41
+ self.resolver = EventResolver()
42
+ self.mapper = EventMapper(app)
43
+ self.startup_tasks: list[asyncio.Task[Any]] = []
44
+ self.client: ClientT | None = None
45
+
46
+ def bind_client(self, client: ClientT) -> None:
47
+ self.client = client
48
+
49
+ def include_router(self, router: Router[ClientT]) -> None:
50
+ self.root_router.include_router(router)
51
+ logger.debug(
52
+ "router included handlers=%s children=%s",
53
+ len(router.handlers),
54
+ len(router.children),
55
+ )
56
+
57
+ def on_internal(
58
+ self,
59
+ event: EventType,
60
+ *filters: FilterCallback[Any],
61
+ ) -> HandlerDecorator[Any, ClientT]:
62
+ logger.debug("registering internal handler event=%s filters=%s", event, len(filters))
63
+ return self.internal_router.on(event, *filters)
64
+
65
+ def on(
66
+ self,
67
+ event: EventType,
68
+ *filters: FilterCallback[Any],
69
+ ) -> HandlerDecorator[Any, ClientT]:
70
+ logger.debug("registering handler event=%s filters=%s", event, len(filters))
71
+ return self.root_router.on(event, *filters)
72
+
73
+ def on_message(
74
+ self,
75
+ *filters: FilterCallback[Message],
76
+ ) -> HandlerDecorator[Message, ClientT]:
77
+ logger.debug("registering message handler filters=%s", len(filters))
78
+ return self.root_router.on_message(*filters)
79
+
80
+ def on_message_edit(
81
+ self,
82
+ *filters: FilterCallback[Message],
83
+ ) -> HandlerDecorator[Message, ClientT]:
84
+ logger.debug("registering message edit handler filters=%s", len(filters))
85
+ return self.root_router.on_message_edit(*filters)
86
+
87
+ def on_message_delete(
88
+ self,
89
+ *filters: FilterCallback[MessageDeleteEvent],
90
+ ) -> HandlerDecorator[MessageDeleteEvent, ClientT]:
91
+ return self.root_router.on_message_delete(*filters)
92
+
93
+ def on_chat_update(
94
+ self,
95
+ *filters: FilterCallback[Chat],
96
+ ) -> HandlerDecorator[Chat, ClientT]:
97
+ return self.root_router.on_chat_update(*filters)
98
+
99
+ def on_raw(
100
+ self,
101
+ *filters: FilterCallback[InboundFrame],
102
+ ) -> HandlerDecorator[InboundFrame, ClientT]:
103
+ return self.root_router.on_raw(*filters)
104
+
105
+ def on_start(self) -> StartDecorator[ClientT]:
106
+ return self.root_router.on_start()
107
+
108
+ def iter_routers(self) -> Generator[Router[ClientT], Any, None]:
109
+ yield from self._iter_router(self.root_router)
110
+
111
+ def _iter_router(self, router: Router[ClientT]) -> Generator[Router[ClientT], Any, None]:
112
+ yield router
113
+
114
+ for child in router.children:
115
+ yield from self._iter_router(child)
116
+
117
+ async def emit_start(self, client: ClientT) -> None:
118
+ tasks: list[asyncio.Task[Any]] = []
119
+
120
+ for router in self.iter_routers():
121
+ handler = router.on_start_handler
122
+ if handler is None:
123
+ continue
124
+
125
+ result = handler(client)
126
+
127
+ if inspect.iscoroutine(result):
128
+ task = asyncio.create_task(result)
129
+ task.add_done_callback(_log_task_error)
130
+ tasks.append(task)
131
+
132
+ self.startup_tasks = tasks
133
+
134
+ async def stop_startup_tasks(self) -> None:
135
+ if not self.startup_tasks:
136
+ return
137
+
138
+ for task in self.startup_tasks:
139
+ if not task.done():
140
+ task.cancel()
141
+
142
+ for task in self.startup_tasks:
143
+ with suppress(asyncio.CancelledError, Exception):
144
+ await task
145
+
146
+ self.startup_tasks = []
147
+
148
+ async def dispatch(self, frame: InboundFrame) -> None:
149
+ event_type = self.resolver.resolve(frame)
150
+
151
+ if event_type is not None:
152
+ logger.debug("dispatching event type=%s", event_type)
153
+ event = self.mapper.map(event_type, frame)
154
+ await self._dispatch_to_router(self.internal_router, event_type, event)
155
+ await self._dispatch_to_router(self.root_router, event_type, event)
156
+ else:
157
+ logger.debug("dispatching raw event only opcode=%s cmd=%s", frame.opcode, frame.cmd)
158
+
159
+ await self._dispatch_to_router(self.root_router, EventType.RAW, frame)
160
+
161
+ async def _dispatch_to_router(
162
+ self,
163
+ router: Router[ClientT],
164
+ event_type: EventType,
165
+ event: Any,
166
+ ) -> None:
167
+ for entry in router.handlers.get(event_type, []):
168
+ if await self._matches(entry, event):
169
+ logger.debug(
170
+ "calling handler event=%s callback=%s",
171
+ event_type,
172
+ _callback_name(entry.callback),
173
+ )
174
+ await self._call(entry.callback, event)
175
+
176
+ for child in router.children:
177
+ await self._dispatch_to_router(child, event_type, event)
178
+
179
+ async def _matches(
180
+ self,
181
+ entry: HandlerEntry[Any, ClientT],
182
+ event: Any,
183
+ ) -> bool:
184
+ for flt in entry.filters:
185
+ result = flt(event)
186
+ if inspect.isawaitable(result):
187
+ result = await result
188
+ if not result:
189
+ logger.debug(
190
+ "handler skipped by filter callback=%s",
191
+ _callback_name(entry.callback),
192
+ )
193
+ return False
194
+ return True
195
+
196
+ async def _call(self, callback: HandlerCallback[Any, ClientT], event: Any) -> Any:
197
+ if self.client is None:
198
+ raise RuntimeError("client is not bound")
199
+
200
+ result = callback(event, self.client)
201
+
202
+ if inspect.isawaitable(result):
203
+ return await result
204
+
205
+ return result
206
+
207
+
208
+ def _callback_name(callback: Any) -> str:
209
+ return getattr(
210
+ callback,
211
+ "__qualname__",
212
+ getattr(callback, "__name__", repr(callback)),
213
+ )
214
+
215
+
216
+ def _log_task_error(task: asyncio.Task[Any]) -> None:
217
+ try:
218
+ task.result()
219
+ except asyncio.CancelledError:
220
+ pass
221
+ except Exception:
222
+ logger.exception("startup task failed")
@@ -0,0 +1,12 @@
1
+ from enum import Enum
2
+
3
+
4
+ class EventType(str, Enum):
5
+ MESSAGE_NEW = "message_new"
6
+ MESSAGE_EDIT = "message_edit"
7
+ MESSAGE_DELETE = "message_delete"
8
+ CHAT_UPDATE = "chat_update"
9
+ USER_UPDATE = "user_update"
10
+ VIDEO_READY = "video_ready"
11
+ FILE_READY = "file_ready"
12
+ RAW = "raw"
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import TYPE_CHECKING
5
+
6
+ from pymax.protocol import InboundFrame, Opcode
7
+ from pymax.protocol.enums import Command
8
+ from pymax.types import Chat, MessageDeleteEvent
9
+ from pymax.types.domain import Message
10
+ from pymax.types.events import FileUploadSignal, VideoUploadSignal
11
+
12
+ from .enums import EventType
13
+ from .resolvers import resolve_attach, resolve_chat, resolve_message, resolve_message_delete
14
+
15
+ if TYPE_CHECKING:
16
+ from pymax.app import App
17
+
18
+ Resolver = Callable[[InboundFrame], EventType | None]
19
+
20
+ EVENT_MAP: dict[Opcode, Resolver] = {
21
+ Opcode.NOTIF_MESSAGE: resolve_message,
22
+ Opcode.MSG_EDIT: resolve_message,
23
+ Opcode.NOTIF_CHAT: resolve_chat,
24
+ Opcode.NOTIF_MSG_DELETE: resolve_message_delete,
25
+ Opcode.NOTIF_ATTACH: resolve_attach,
26
+ }
27
+
28
+
29
+ class EventResolver:
30
+ def resolve(self, frame: InboundFrame) -> EventType | None:
31
+ if frame.cmd != Command.EVENT:
32
+ return None
33
+
34
+ try:
35
+ opcode = Opcode(frame.opcode)
36
+ except ValueError:
37
+ return None
38
+
39
+ handler = EVENT_MAP.get(opcode)
40
+
41
+ if handler:
42
+ return handler(frame)
43
+ return None
44
+
45
+
46
+ class EventMapper:
47
+ def __init__(self, app: App) -> None:
48
+ self.app = app
49
+
50
+ def map(self, event_type: EventType, frame: InboundFrame):
51
+ if frame.cmd != Command.EVENT:
52
+ return None
53
+
54
+ if frame.payload:
55
+ if event_type in (EventType.MESSAGE_NEW, EventType.MESSAGE_EDIT):
56
+ return Message.model_validate(frame.payload).bind(self.app.api.messages)
57
+ elif event_type == EventType.CHAT_UPDATE:
58
+ return Chat.model_validate(frame.payload["chat"]).bind(
59
+ self.app.api.messages,
60
+ self.app.api.chats,
61
+ )
62
+ elif event_type == EventType.MESSAGE_DELETE:
63
+ model = MessageDeleteEvent.model_validate(frame.payload)
64
+ model.chat.bind(
65
+ self.app.api.messages,
66
+ self.app.api.chats,
67
+ )
68
+ return model
69
+ elif event_type == EventType.VIDEO_READY:
70
+ return VideoUploadSignal.model_validate(frame.payload)
71
+ elif event_type == EventType.FILE_READY:
72
+ return FileUploadSignal.model_validate(frame.payload)
73
+ return frame
@@ -0,0 +1,52 @@
1
+ from pydantic import ValidationError
2
+
3
+ from pymax.logging import get_logger
4
+ from pymax.protocol import InboundFrame
5
+ from pymax.protocol.enums import Opcode
6
+ from pymax.types import Message
7
+ from pymax.types.domain.enums import MessageStatus
8
+ from pymax.types.events import FileUploadSignal, VideoUploadSignal
9
+
10
+ from .enums import EventType
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ def resolve_chat(_: InboundFrame) -> EventType | None:
16
+ return EventType.CHAT_UPDATE
17
+
18
+
19
+ def resolve_message_delete(_: InboundFrame) -> EventType | None:
20
+ return EventType.MESSAGE_DELETE
21
+
22
+
23
+ def resolve_attach(frame: InboundFrame) -> EventType | None:
24
+ try:
25
+ FileUploadSignal.model_validate(frame.payload)
26
+ return EventType.FILE_READY
27
+ except ValidationError:
28
+ logger.debug("attach event is not a file upload signal")
29
+
30
+ try:
31
+ VideoUploadSignal.model_validate(frame.payload)
32
+ return EventType.VIDEO_READY
33
+ except ValidationError:
34
+ logger.debug("attach event is not a video upload signal")
35
+
36
+ return None
37
+
38
+
39
+ def resolve_message(frame: InboundFrame) -> EventType | None:
40
+ if frame.opcode not in (Opcode.NOTIF_MESSAGE, Opcode.MSG_EDIT):
41
+ return None
42
+
43
+ try:
44
+ model = Message.model_validate(frame.payload)
45
+
46
+ if model.status == MessageStatus.EDITED:
47
+ return EventType.MESSAGE_EDIT
48
+ else:
49
+ return EventType.MESSAGE_NEW
50
+ except ValidationError:
51
+ logger.debug("failed to resolve message event", exc_info=True)
52
+ return None
@@ -0,0 +1,216 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from collections.abc import Awaitable, Callable
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar
7
+
8
+ from pymax.types import MessageDeleteEvent
9
+
10
+ from .enums import EventType
11
+
12
+ if TYPE_CHECKING:
13
+ from pymax.client import Client
14
+ from pymax.protocol import InboundFrame
15
+ from pymax.types import Chat
16
+ from pymax.types.domain import Message
17
+
18
+
19
+ _EventT = TypeVar("_EventT")
20
+ ClientT = TypeVar("ClientT")
21
+
22
+ HandlerCallback: TypeAlias = Callable[
23
+ [_EventT, ClientT],
24
+ Awaitable[Any] | Any,
25
+ ]
26
+ HandlerDecorator: TypeAlias = Callable[
27
+ [HandlerCallback[_EventT, ClientT]],
28
+ HandlerCallback[_EventT, ClientT],
29
+ ]
30
+ FilterCallback: TypeAlias = Callable[
31
+ [_EventT],
32
+ Awaitable[bool] | bool,
33
+ ]
34
+ StartCallback: TypeAlias = Callable[
35
+ [ClientT],
36
+ Awaitable[Any] | Any,
37
+ ]
38
+ StartDecorator: TypeAlias = Callable[
39
+ [StartCallback[ClientT]],
40
+ StartCallback[ClientT],
41
+ ]
42
+
43
+
44
+ @dataclass(slots=True)
45
+ class HandlerEntry(Generic[_EventT, ClientT]):
46
+ callback: HandlerCallback[_EventT, ClientT]
47
+ filters: tuple[FilterCallback[_EventT], ...] = ()
48
+
49
+
50
+ class Router(Generic[ClientT]):
51
+ """Контейнер обработчиков событий PyMax.
52
+
53
+ Роутер хранит обработчики и фильтры. Когда приходит событие, dispatcher
54
+ проходит по root router и его дочерним роутерам, проверяет фильтры и
55
+ вызывает подходящие callbacks как ``handler(event, client)``.
56
+
57
+ Example:
58
+ .. code-block:: python
59
+
60
+ from pymax import Client, Message, Router
61
+
62
+ router = Router[Client]()
63
+
64
+ def is_start(message: Message) -> bool:
65
+ return message.text == "/start"
66
+
67
+ @router.on_message(is_start)
68
+ async def start(message: Message, client: Client) -> None:
69
+ await message.answer("Привет")
70
+
71
+ client = Client(phone="+79990000000")
72
+ client.include_router(router)
73
+ """
74
+
75
+ def __init__(self) -> None:
76
+ self.handlers: dict[
77
+ EventType,
78
+ list[HandlerEntry[Any, ClientT]],
79
+ ] = defaultdict(list)
80
+
81
+ self.children: list[Router[ClientT]] = []
82
+ self.on_start_handler: StartCallback[ClientT] | None = None
83
+
84
+ def on(
85
+ self,
86
+ event: EventType,
87
+ /,
88
+ *filters: FilterCallback[_EventT],
89
+ ) -> HandlerDecorator[_EventT, ClientT]:
90
+ """Регистрирует обработчик события по ``EventType``.
91
+
92
+ Args:
93
+ event: Тип события.
94
+ *filters: Фильтры, которые получают событие и возвращают bool.
95
+
96
+ Returns:
97
+ Декоратор для ``handler(event, client)``.
98
+
99
+ Example:
100
+ .. code-block:: python
101
+
102
+ from pymax import Client
103
+ from pymax.protocol import InboundFrame
104
+
105
+ @router.on(EventType.RAW)
106
+ async def raw(frame: InboundFrame, client: Client) -> None:
107
+ print(frame.payload)
108
+ """
109
+ def decorator(
110
+ handler: HandlerCallback[_EventT, ClientT],
111
+ ) -> HandlerCallback[_EventT, ClientT]:
112
+ self.handlers[event].append(
113
+ HandlerEntry(
114
+ callback=handler,
115
+ filters=filters,
116
+ )
117
+ )
118
+ return handler
119
+
120
+ return decorator
121
+
122
+ def include_router(self, router: Router[ClientT]) -> None:
123
+ """Подключает дочерний роутер.
124
+
125
+ Args:
126
+ router: Router, обработчики которого нужно добавить в дерево.
127
+
128
+ Returns:
129
+ ``None``.
130
+ """
131
+ self.children.append(router)
132
+
133
+ def on_start(self) -> StartDecorator:
134
+ """Регистрирует обработчик старта клиента.
135
+
136
+ Returns:
137
+ Декоратор для ``handler(client)``.
138
+ """
139
+ def decorator(handler: StartCallback) -> StartCallback:
140
+ self.on_start_handler = handler
141
+ return handler
142
+
143
+ return decorator
144
+
145
+ def on_message(
146
+ self,
147
+ *filters: FilterCallback[Message],
148
+ ) -> HandlerDecorator[Message, ClientT]:
149
+ """Регистрирует обработчик новых сообщений.
150
+
151
+ Args:
152
+ *filters: Фильтры для ``Message``.
153
+
154
+ Returns:
155
+ Декоратор для ``handler(message, client)``.
156
+ """
157
+ return self.on(EventType.MESSAGE_NEW, *filters)
158
+
159
+ def on_message_edit(
160
+ self,
161
+ *filters: FilterCallback[Message],
162
+ ) -> HandlerDecorator[Message, ClientT]:
163
+ """Регистрирует обработчик редактирования сообщений.
164
+
165
+ Args:
166
+ *filters: Фильтры для ``Message``.
167
+
168
+ Returns:
169
+ Декоратор для ``handler(message, client)``.
170
+ """
171
+ return self.on(EventType.MESSAGE_EDIT, *filters)
172
+
173
+ def on_message_delete(
174
+ self,
175
+ *filters: FilterCallback[MessageDeleteEvent],
176
+ ) -> HandlerDecorator[MessageDeleteEvent, ClientT]:
177
+ """Регистрирует обработчик удаления сообщений.
178
+
179
+ Args:
180
+ *filters: Фильтры для ``MessageDeleteEvent``.
181
+
182
+ Returns:
183
+ Декоратор для ``handler(event, client)``.
184
+ """
185
+ return self.on(EventType.MESSAGE_DELETE, *filters)
186
+
187
+ def on_chat_update(
188
+ self,
189
+ *filters: FilterCallback[Chat],
190
+ ) -> HandlerDecorator[Chat, ClientT]:
191
+ """Регистрирует обработчик обновления чата.
192
+
193
+ Args:
194
+ *filters: Фильтры для ``Chat``.
195
+
196
+ Returns:
197
+ Декоратор для ``handler(chat, client)``.
198
+ """
199
+ return self.on(EventType.CHAT_UPDATE, *filters)
200
+
201
+ def on_raw(
202
+ self,
203
+ *filters: FilterCallback[InboundFrame],
204
+ ) -> HandlerDecorator[InboundFrame, ClientT]:
205
+ """Регистрирует обработчик исходных frame-ов.
206
+
207
+ Args:
208
+ *filters: Фильтры для ``InboundFrame``.
209
+
210
+ Returns:
211
+ Декоратор для ``handler(frame, client)``.
212
+ """
213
+ return self.on(EventType.RAW, *filters)
214
+
215
+
216
+ ClientRouter: TypeAlias = Router["Client"]
pymax/exceptions.py CHANGED
@@ -1,108 +1,41 @@
1
- class InvalidPhoneError(Exception):
2
- """
3
- Исключение, вызываемое при неверном формате номера телефона.
1
+ from typing import Any
4
2
 
5
- Args:
6
- phone (str): Некорректный номер телефона.
7
- """
8
3
 
9
- def __init__(self, phone: str) -> None:
10
- super().__init__(f"Invalid phone number format: {phone}")
4
+ class PyMaxError(Exception):
5
+ pass
11
6
 
12
7
 
13
- class WebSocketNotConnectedError(Exception):
14
- """
15
- Исключение, вызываемое при попытке обращения к WebSocket,
16
- если соединение не установлено.
17
- """
8
+ class UploadError(PyMaxError):
9
+ pass
18
10
 
19
- def __init__(self) -> None:
20
- super().__init__("WebSocket is not connected")
21
-
22
-
23
- class SocketNotConnectedError(Exception):
24
- """
25
- Исключение, вызываемое при попытке обращения к сокету,
26
- если соединение не установлено.
27
- """
28
-
29
- def __init__(self) -> None:
30
- super().__init__("Socket is not connected")
31
-
32
-
33
- class SocketSendError(Exception):
34
- """
35
- Исключение, вызываемое при ошибке отправки данных через сокет.
36
- """
37
-
38
- def __init__(self) -> None:
39
- super().__init__("Send and wait failed (socket)")
40
-
41
-
42
- class ResponseError(Exception):
43
- """
44
- Исключение, вызываемое при ошибке в ответе от сервера.
45
- """
46
-
47
- def __init__(self, message: str) -> None:
48
- super().__init__(f"Response error: {message}")
49
-
50
-
51
- class ResponseStructureError(Exception):
52
- """
53
- Исключение, вызываемое при неверной структуре ответа от сервера.
54
- """
55
-
56
- def __init__(self, message: str) -> None:
57
- super().__init__(f"Response structure error: {message}")
58
-
59
-
60
- class Error(Exception):
61
- """
62
- Базовое исключение для ошибок PyMax.
63
- """
64
11
 
12
+ class ApiError(PyMaxError):
65
13
  def __init__(
66
14
  self,
67
- error: str,
68
- message: str,
69
- title: str,
15
+ *,
16
+ opcode: int,
17
+ error: str | None = None,
18
+ message: str | None = None,
70
19
  localized_message: str | None = None,
20
+ title: str | None = None,
21
+ payload: dict[str, Any] | None = None,
71
22
  ) -> None:
23
+ self.opcode = opcode
72
24
  self.error = error
73
25
  self.message = message
74
- self.title = title
75
26
  self.localized_message = localized_message
27
+ self.title = title
28
+ self.payload = payload or {}
76
29
 
77
30
  parts = []
78
- if localized_message:
79
- parts.append(localized_message)
80
- if message:
81
- parts.append(message)
31
+ for part in (localized_message, message):
32
+ if part and part not in parts:
33
+ parts.append(part)
82
34
  if title:
83
35
  parts.append(f"({title})")
84
- parts.append(f"[{error}]")
85
-
86
- super().__init__("PyMax Error: " + " ".join(parts))
87
-
88
-
89
- class RateLimitError(Error):
90
- """
91
- Исключение, вызываемое при превышении лимита запросов.
92
- """
93
-
94
- def __init__(
95
- self, error: str, message: str, title: str, localized_message: str | None = None
96
- ) -> None:
97
- super().__init__(error, message, title, localized_message)
36
+ if error:
37
+ parts.append(f"[{error}]")
98
38
 
39
+ text = " ".join(parts) or "API request failed"
99
40
 
100
- class LoginError(Error):
101
- """
102
- Исключение, вызываемое при ошибке авторизации.
103
- """
104
-
105
- def __init__(
106
- self, error: str, message: str, title: str, localized_message: str | None = None
107
- ) -> None:
108
- super().__init__(error, message, title, localized_message)
41
+ super().__init__(text)