telegrinder 1.0.0rc1__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 (215) hide show
  1. telegrinder/__init__.py +258 -0
  2. telegrinder/__meta__.py +1 -0
  3. telegrinder/api/__init__.py +15 -0
  4. telegrinder/api/api.py +175 -0
  5. telegrinder/api/error.py +50 -0
  6. telegrinder/api/response.py +23 -0
  7. telegrinder/api/token.py +30 -0
  8. telegrinder/api/validators.py +30 -0
  9. telegrinder/bot/__init__.py +144 -0
  10. telegrinder/bot/bot.py +70 -0
  11. telegrinder/bot/cute_types/__init__.py +41 -0
  12. telegrinder/bot/cute_types/base.py +228 -0
  13. telegrinder/bot/cute_types/base.pyi +49 -0
  14. telegrinder/bot/cute_types/business_connection.py +9 -0
  15. telegrinder/bot/cute_types/business_messages_deleted.py +9 -0
  16. telegrinder/bot/cute_types/callback_query.py +248 -0
  17. telegrinder/bot/cute_types/chat_boost_removed.py +9 -0
  18. telegrinder/bot/cute_types/chat_boost_updated.py +9 -0
  19. telegrinder/bot/cute_types/chat_join_request.py +59 -0
  20. telegrinder/bot/cute_types/chat_member_updated.py +158 -0
  21. telegrinder/bot/cute_types/chosen_inline_result.py +11 -0
  22. telegrinder/bot/cute_types/inline_query.py +41 -0
  23. telegrinder/bot/cute_types/message.py +2809 -0
  24. telegrinder/bot/cute_types/message_reaction_count_updated.py +9 -0
  25. telegrinder/bot/cute_types/message_reaction_updated.py +9 -0
  26. telegrinder/bot/cute_types/paid_media_purchased.py +11 -0
  27. telegrinder/bot/cute_types/poll.py +9 -0
  28. telegrinder/bot/cute_types/poll_answer.py +9 -0
  29. telegrinder/bot/cute_types/pre_checkout_query.py +36 -0
  30. telegrinder/bot/cute_types/shipping_query.py +11 -0
  31. telegrinder/bot/cute_types/update.py +209 -0
  32. telegrinder/bot/cute_types/utils.py +141 -0
  33. telegrinder/bot/dispatch/__init__.py +99 -0
  34. telegrinder/bot/dispatch/abc.py +74 -0
  35. telegrinder/bot/dispatch/action.py +99 -0
  36. telegrinder/bot/dispatch/context.py +162 -0
  37. telegrinder/bot/dispatch/dispatch.py +362 -0
  38. telegrinder/bot/dispatch/handler/__init__.py +23 -0
  39. telegrinder/bot/dispatch/handler/abc.py +25 -0
  40. telegrinder/bot/dispatch/handler/audio_reply.py +43 -0
  41. telegrinder/bot/dispatch/handler/base.py +34 -0
  42. telegrinder/bot/dispatch/handler/document_reply.py +43 -0
  43. telegrinder/bot/dispatch/handler/func.py +73 -0
  44. telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
  45. telegrinder/bot/dispatch/handler/message_reply.py +35 -0
  46. telegrinder/bot/dispatch/handler/photo_reply.py +43 -0
  47. telegrinder/bot/dispatch/handler/sticker_reply.py +36 -0
  48. telegrinder/bot/dispatch/handler/video_reply.py +43 -0
  49. telegrinder/bot/dispatch/middleware/__init__.py +13 -0
  50. telegrinder/bot/dispatch/middleware/abc.py +112 -0
  51. telegrinder/bot/dispatch/middleware/box.py +32 -0
  52. telegrinder/bot/dispatch/middleware/filter.py +88 -0
  53. telegrinder/bot/dispatch/middleware/media_group.py +69 -0
  54. telegrinder/bot/dispatch/process.py +93 -0
  55. telegrinder/bot/dispatch/return_manager/__init__.py +21 -0
  56. telegrinder/bot/dispatch/return_manager/abc.py +107 -0
  57. telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
  58. telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
  59. telegrinder/bot/dispatch/return_manager/message.py +34 -0
  60. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +19 -0
  61. telegrinder/bot/dispatch/return_manager/utils.py +20 -0
  62. telegrinder/bot/dispatch/router/__init__.py +4 -0
  63. telegrinder/bot/dispatch/router/abc.py +15 -0
  64. telegrinder/bot/dispatch/router/base.py +154 -0
  65. telegrinder/bot/dispatch/view/__init__.py +15 -0
  66. telegrinder/bot/dispatch/view/abc.py +15 -0
  67. telegrinder/bot/dispatch/view/base.py +226 -0
  68. telegrinder/bot/dispatch/view/box.py +207 -0
  69. telegrinder/bot/dispatch/view/media_group.py +25 -0
  70. telegrinder/bot/dispatch/waiter_machine/__init__.py +25 -0
  71. telegrinder/bot/dispatch/waiter_machine/actions.py +16 -0
  72. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +13 -0
  73. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +53 -0
  74. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +61 -0
  75. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +49 -0
  76. telegrinder/bot/dispatch/waiter_machine/machine.py +264 -0
  77. telegrinder/bot/dispatch/waiter_machine/middleware.py +77 -0
  78. telegrinder/bot/dispatch/waiter_machine/short_state.py +105 -0
  79. telegrinder/bot/polling/__init__.py +4 -0
  80. telegrinder/bot/polling/abc.py +25 -0
  81. telegrinder/bot/polling/error_handler.py +93 -0
  82. telegrinder/bot/polling/polling.py +167 -0
  83. telegrinder/bot/polling/utils.py +12 -0
  84. telegrinder/bot/rules/__init__.py +166 -0
  85. telegrinder/bot/rules/abc.py +150 -0
  86. telegrinder/bot/rules/button.py +20 -0
  87. telegrinder/bot/rules/callback_data.py +109 -0
  88. telegrinder/bot/rules/chat_join.py +28 -0
  89. telegrinder/bot/rules/chat_member_updated.py +145 -0
  90. telegrinder/bot/rules/command.py +137 -0
  91. telegrinder/bot/rules/enum_text.py +29 -0
  92. telegrinder/bot/rules/func.py +21 -0
  93. telegrinder/bot/rules/fuzzy.py +21 -0
  94. telegrinder/bot/rules/inline.py +45 -0
  95. telegrinder/bot/rules/integer.py +19 -0
  96. telegrinder/bot/rules/is_from.py +213 -0
  97. telegrinder/bot/rules/logic.py +22 -0
  98. telegrinder/bot/rules/magic.py +60 -0
  99. telegrinder/bot/rules/markup.py +51 -0
  100. telegrinder/bot/rules/media.py +13 -0
  101. telegrinder/bot/rules/mention.py +15 -0
  102. telegrinder/bot/rules/message_entities.py +37 -0
  103. telegrinder/bot/rules/node.py +43 -0
  104. telegrinder/bot/rules/payload.py +89 -0
  105. telegrinder/bot/rules/payment_invoice.py +14 -0
  106. telegrinder/bot/rules/regex.py +34 -0
  107. telegrinder/bot/rules/rule_enum.py +71 -0
  108. telegrinder/bot/rules/start.py +73 -0
  109. telegrinder/bot/rules/state.py +35 -0
  110. telegrinder/bot/rules/text.py +27 -0
  111. telegrinder/bot/rules/update.py +14 -0
  112. telegrinder/bot/scenario/__init__.py +5 -0
  113. telegrinder/bot/scenario/abc.py +16 -0
  114. telegrinder/bot/scenario/checkbox.py +183 -0
  115. telegrinder/bot/scenario/choice.py +44 -0
  116. telegrinder/client/__init__.py +11 -0
  117. telegrinder/client/abc.py +136 -0
  118. telegrinder/client/form_data.py +34 -0
  119. telegrinder/client/rnet.py +198 -0
  120. telegrinder/model.py +133 -0
  121. telegrinder/model.pyi +57 -0
  122. telegrinder/modules.py +1081 -0
  123. telegrinder/msgspec_utils/__init__.py +42 -0
  124. telegrinder/msgspec_utils/abc.py +16 -0
  125. telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
  126. telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
  127. telegrinder/msgspec_utils/custom_types/enum_meta.py +61 -0
  128. telegrinder/msgspec_utils/custom_types/literal.py +25 -0
  129. telegrinder/msgspec_utils/custom_types/option.py +17 -0
  130. telegrinder/msgspec_utils/decoder.py +388 -0
  131. telegrinder/msgspec_utils/encoder.py +204 -0
  132. telegrinder/msgspec_utils/json.py +15 -0
  133. telegrinder/msgspec_utils/tools.py +80 -0
  134. telegrinder/node/__init__.py +80 -0
  135. telegrinder/node/compose.py +193 -0
  136. telegrinder/node/nodes/__init__.py +96 -0
  137. telegrinder/node/nodes/attachment.py +169 -0
  138. telegrinder/node/nodes/callback_query.py +25 -0
  139. telegrinder/node/nodes/channel.py +97 -0
  140. telegrinder/node/nodes/command.py +33 -0
  141. telegrinder/node/nodes/error.py +43 -0
  142. telegrinder/node/nodes/event.py +70 -0
  143. telegrinder/node/nodes/file.py +39 -0
  144. telegrinder/node/nodes/global_node.py +66 -0
  145. telegrinder/node/nodes/i18n.py +110 -0
  146. telegrinder/node/nodes/me.py +26 -0
  147. telegrinder/node/nodes/message_entities.py +15 -0
  148. telegrinder/node/nodes/payload.py +84 -0
  149. telegrinder/node/nodes/reply_message.py +14 -0
  150. telegrinder/node/nodes/source.py +172 -0
  151. telegrinder/node/nodes/state_mutator.py +71 -0
  152. telegrinder/node/nodes/text.py +62 -0
  153. telegrinder/node/scope.py +88 -0
  154. telegrinder/node/utils.py +38 -0
  155. telegrinder/py.typed +0 -0
  156. telegrinder/rules.py +1 -0
  157. telegrinder/tools/__init__.py +183 -0
  158. telegrinder/tools/aio.py +147 -0
  159. telegrinder/tools/final.py +21 -0
  160. telegrinder/tools/formatting/__init__.py +85 -0
  161. telegrinder/tools/formatting/deep_links/__init__.py +39 -0
  162. telegrinder/tools/formatting/deep_links/links.py +468 -0
  163. telegrinder/tools/formatting/deep_links/parsing.py +88 -0
  164. telegrinder/tools/formatting/deep_links/validators.py +8 -0
  165. telegrinder/tools/formatting/html.py +241 -0
  166. telegrinder/tools/fullname.py +82 -0
  167. telegrinder/tools/global_context/__init__.py +13 -0
  168. telegrinder/tools/global_context/abc.py +63 -0
  169. telegrinder/tools/global_context/builtin_context.py +45 -0
  170. telegrinder/tools/global_context/global_context.py +614 -0
  171. telegrinder/tools/input_file_directory.py +30 -0
  172. telegrinder/tools/keyboard/__init__.py +6 -0
  173. telegrinder/tools/keyboard/abc.py +84 -0
  174. telegrinder/tools/keyboard/base.py +108 -0
  175. telegrinder/tools/keyboard/button.py +181 -0
  176. telegrinder/tools/keyboard/data.py +31 -0
  177. telegrinder/tools/keyboard/keyboard.py +160 -0
  178. telegrinder/tools/keyboard/utils.py +95 -0
  179. telegrinder/tools/lifespan.py +188 -0
  180. telegrinder/tools/limited_dict.py +35 -0
  181. telegrinder/tools/loop_wrapper.py +271 -0
  182. telegrinder/tools/magic/__init__.py +29 -0
  183. telegrinder/tools/magic/annotations.py +172 -0
  184. telegrinder/tools/magic/descriptors.py +57 -0
  185. telegrinder/tools/magic/function.py +254 -0
  186. telegrinder/tools/magic/inspect.py +16 -0
  187. telegrinder/tools/magic/shortcut.py +107 -0
  188. telegrinder/tools/member_descriptor_proxy.py +95 -0
  189. telegrinder/tools/parse_mode.py +12 -0
  190. telegrinder/tools/serialization/__init__.py +5 -0
  191. telegrinder/tools/serialization/abc.py +34 -0
  192. telegrinder/tools/serialization/json_ser.py +60 -0
  193. telegrinder/tools/serialization/msgpack_ser.py +197 -0
  194. telegrinder/tools/serialization/utils.py +18 -0
  195. telegrinder/tools/singleton/__init__.py +4 -0
  196. telegrinder/tools/singleton/abc.py +14 -0
  197. telegrinder/tools/singleton/singleton.py +18 -0
  198. telegrinder/tools/state_mutator/__init__.py +4 -0
  199. telegrinder/tools/state_mutator/mutation.py +85 -0
  200. telegrinder/tools/state_storage/__init__.py +4 -0
  201. telegrinder/tools/state_storage/abc.py +38 -0
  202. telegrinder/tools/state_storage/memory.py +27 -0
  203. telegrinder/tools/strings.py +22 -0
  204. telegrinder/types/__init__.py +323 -0
  205. telegrinder/types/enums.py +754 -0
  206. telegrinder/types/input_file.py +51 -0
  207. telegrinder/types/methods.py +6143 -0
  208. telegrinder/types/methods_utils.py +66 -0
  209. telegrinder/types/objects.py +8184 -0
  210. telegrinder/types/webapp.py +129 -0
  211. telegrinder/verification_utils.py +35 -0
  212. telegrinder-1.0.0rc1.dist-info/METADATA +166 -0
  213. telegrinder-1.0.0rc1.dist-info/RECORD +215 -0
  214. telegrinder-1.0.0rc1.dist-info/WHEEL +4 -0
  215. telegrinder-1.0.0rc1.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,264 @@
1
+ import asyncio
2
+ import datetime
3
+ import typing
4
+
5
+ from telegrinder.bot.cute_types.base import BaseCute
6
+ from telegrinder.bot.dispatch.context import Context
7
+ from telegrinder.bot.dispatch.waiter_machine.actions import WaiterActions
8
+ from telegrinder.bot.dispatch.waiter_machine.hasher import Hasher
9
+ from telegrinder.bot.dispatch.waiter_machine.middleware import WaiterMiddleware
10
+ from telegrinder.bot.dispatch.waiter_machine.short_state import (
11
+ ShortState,
12
+ ShortStateContext,
13
+ )
14
+ from telegrinder.bot.rules.abc import ABCRule
15
+ from telegrinder.modules import logger
16
+ from telegrinder.tools.aio import maybe_awaitable
17
+ from telegrinder.tools.lifespan import Lifespan
18
+ from telegrinder.tools.limited_dict import LimitedDict
19
+ from telegrinder.tools.magic.function import bundle
20
+
21
+ if typing.TYPE_CHECKING:
22
+ from telegrinder.bot.dispatch.view.base import View
23
+
24
+ type Storage[Event: BaseCute, HasherData] = dict[
25
+ Hasher[Event, HasherData],
26
+ LimitedDict[typing.Hashable, ShortState[Event]],
27
+ ]
28
+ type HasherWithData[Event: BaseCute, ViewType: View, Data] = tuple[Hasher[Event, Data], ViewType, Data]
29
+
30
+ _NODATA: typing.Final = object()
31
+ MAX_STORAGE_SIZE: typing.Final = 10000
32
+ ONE_MINUTE: typing.Final = datetime.timedelta(minutes=1)
33
+ WEEK: typing.Final = datetime.timedelta(days=7)
34
+
35
+
36
+ def unpack_to_context(context: Context, /) -> tuple[Context]:
37
+ return (context,)
38
+
39
+
40
+ def no_unpack(_: Context, /) -> tuple[()]:
41
+ return ()
42
+
43
+
44
+ class ContextUnpackProto[*Ts](typing.Protocol):
45
+ __name__: str
46
+
47
+ def __call__(self, context: Context, /) -> tuple[*Ts]: ...
48
+
49
+
50
+ class WaiterMachine:
51
+ storage: Storage[typing.Any, typing.Any]
52
+
53
+ def __init__(
54
+ self,
55
+ *,
56
+ max_storage_size: int = MAX_STORAGE_SIZE,
57
+ base_state_lifetime: datetime.timedelta = WEEK,
58
+ clear_storage_interval: datetime.timedelta = ONE_MINUTE,
59
+ ) -> None:
60
+ self.max_storage_size = max_storage_size
61
+ self.base_state_lifetime = base_state_lifetime
62
+ self.storage = {}
63
+
64
+ def __repr__(self) -> str:
65
+ return "<{}: with {} storage items and max_storage_size={}, base_state_lifetime={}>".format(
66
+ self.__class__.__name__,
67
+ len(self.storage),
68
+ self.max_storage_size,
69
+ self.base_state_lifetime,
70
+ )
71
+
72
+ def add_hasher[Event: BaseCute, HasherData](
73
+ self,
74
+ hasher: Hasher[Event, HasherData],
75
+ view: View,
76
+ /,
77
+ ) -> None:
78
+ self.storage[hasher] = LimitedDict(maxlimit=self.max_storage_size)
79
+ view.middlewares.insert(0, WaiterMiddleware(self, hasher))
80
+
81
+ async def drop_all(self) -> None:
82
+ for hasher in self.storage.copy():
83
+ for ident, short_state in self.storage[hasher].items():
84
+ if short_state.context:
85
+ await self.drop(hasher, data=ident)
86
+ else:
87
+ short_state.cancel_drop()
88
+ await short_state.cancel()
89
+
90
+ del self.storage[hasher]
91
+
92
+ async def drop[Event: BaseCute, HasherData](
93
+ self,
94
+ hasher: Hasher[Event, HasherData],
95
+ data: HasherData,
96
+ *,
97
+ expired: bool = False,
98
+ **context: typing.Any,
99
+ ) -> None:
100
+ if hasher not in self.storage:
101
+ raise LookupError("No record of hasher {!r} found.".format(hasher))
102
+
103
+ waiter_id: typing.Hashable = hasher.get_hash_from_data(data).expect(
104
+ RuntimeError("Couldn't create hash from data"),
105
+ )
106
+ short_state = self.storage[hasher].pop(waiter_id, None)
107
+ if short_state is None:
108
+ raise LookupError("Waiter with identificator {} is not found for hasher {!r}.".format(waiter_id, hasher))
109
+
110
+ try:
111
+ if not expired:
112
+ short_state.cancel_drop()
113
+
114
+ context["short_state"] = short_state
115
+
116
+ if on_drop := short_state.actions.get("on_drop"):
117
+ await maybe_awaitable(bundle(on_drop, context)())
118
+ finally:
119
+ await short_state.cancel()
120
+
121
+ async def drop_state[Event: BaseCute, HasherData](
122
+ self,
123
+ short_state: ShortState[Event],
124
+ hasher: Hasher[Event, HasherData],
125
+ data: HasherData,
126
+ *,
127
+ expired: bool = False,
128
+ **context: typing.Any,
129
+ ) -> None:
130
+ preset_context = (
131
+ short_state.context.context.copy().as_dict() | context if short_state.context is not None else context
132
+ )
133
+
134
+ try:
135
+ await self.drop(hasher, data, expired=expired, **preset_context)
136
+ except Exception as e:
137
+ await logger.aerror("Error dropping state for hasher {!r}: {}", hasher, e)
138
+
139
+ async def drop_state_many[Event: BaseCute, ViewType: View, HasherData](
140
+ self,
141
+ short_state: ShortState[Event],
142
+ *hashers: HasherWithData[Event, ViewType, HasherData],
143
+ expired: bool = False,
144
+ **context: typing.Any,
145
+ ) -> None:
146
+ for hasher, _, data in hashers:
147
+ await self.drop_state(short_state, hasher, data, expired=expired, **context)
148
+
149
+ async def wait[Event: BaseCute, ViewType: View, HasherData](
150
+ self,
151
+ hasher: Hasher[Event, HasherData] | HasherWithData[Event, ViewType, HasherData],
152
+ view: ViewType | None = None,
153
+ data: HasherData = _NODATA,
154
+ *,
155
+ filter: ABCRule | None = None,
156
+ release: ABCRule | None = None,
157
+ lifetime: datetime.timedelta | float | None = None,
158
+ lifespan: Lifespan | None = None,
159
+ **actions: typing.Unpack[WaiterActions[Event]],
160
+ ) -> ShortStateContext[Event]:
161
+ if isinstance(lifetime, int | float):
162
+ lifetime = datetime.timedelta(seconds=lifetime)
163
+ elif lifetime is None:
164
+ lifetime = self.base_state_lifetime
165
+
166
+ lifespan = lifespan or Lifespan()
167
+ event = asyncio.Event()
168
+ short_state = ShortState(
169
+ event,
170
+ actions,
171
+ release=release,
172
+ filter=filter,
173
+ expiration=lifetime,
174
+ )
175
+
176
+ hasher, view, data = hasher if not isinstance(hasher, Hasher) else (hasher, view, data)
177
+
178
+ if data is _NODATA:
179
+ raise ValueError("Hasher requires data.")
180
+
181
+ if view is None:
182
+ raise ValueError("Hasher requires view.")
183
+
184
+ waiter_hash = hasher.get_hash_from_data(data).expect(RuntimeError("Hasher couldn't create hash."))
185
+
186
+ if hasher not in self.storage:
187
+ self.add_hasher(hasher, view)
188
+
189
+ if (deleted_short_state := self.storage[hasher].set(waiter_hash, short_state)) is not None:
190
+ deleted_short_state.cancel_drop()
191
+ await deleted_short_state.cancel()
192
+
193
+ async with lifespan:
194
+ short_state.schedule_drop(self.drop_state, hasher, data, lifetime=lifetime)
195
+ await event.wait()
196
+ short_state.cancel_drop()
197
+
198
+ self.storage[hasher].pop(waiter_hash, None)
199
+
200
+ if short_state.context is None:
201
+ raise LookupError("No context in short_state.")
202
+ return short_state.context
203
+
204
+ async def wait_many[Event: BaseCute[typing.Any], ViewType: View, Data, *Ts](
205
+ self,
206
+ *hashers: HasherWithData[Event, ViewType, Data],
207
+ filter: ABCRule | None = None,
208
+ release: ABCRule | None = None,
209
+ lifetime: datetime.timedelta | float | None = None,
210
+ lifespan: Lifespan | None = None,
211
+ unpack: ContextUnpackProto[*Ts] = unpack_to_context,
212
+ **actions: typing.Unpack[WaiterActions[Event]],
213
+ ) -> tuple[HasherWithData[Event, ViewType, Data], Event, *Ts]:
214
+ if isinstance(lifetime, int | float):
215
+ lifetime = datetime.timedelta(seconds=lifetime)
216
+ elif lifetime is None:
217
+ lifetime = self.base_state_lifetime
218
+
219
+ lifespan = lifespan or Lifespan()
220
+ event = asyncio.Event()
221
+ short_state = ShortState(
222
+ event,
223
+ actions,
224
+ release=release,
225
+ filter=filter,
226
+ expiration=lifetime,
227
+ )
228
+ waiter_hashes: dict[Hasher[Event, Data], typing.Hashable] = {}
229
+
230
+ for hasher, view, data in hashers:
231
+ waiter_hash = hasher.get_hash_from_data(data).expect(RuntimeError("Hasher couldn't create hash."))
232
+
233
+ if hasher not in self.storage:
234
+ self.add_hasher(hasher, view)
235
+
236
+ if (deleted_short_state := self.storage[hasher].set(waiter_hash, short_state)) is not None:
237
+ deleted_short_state.cancel_drop()
238
+ await deleted_short_state.cancel()
239
+
240
+ waiter_hashes[hasher] = waiter_hash
241
+
242
+ async with lifespan:
243
+ short_state.schedule_drop_many(self.drop_state_many, *hashers, lifetime=lifetime)
244
+ await event.wait()
245
+ short_state.cancel_drop()
246
+
247
+ if short_state.context is None:
248
+ raise LookupError("No context in short_state.")
249
+
250
+ initiator = short_state.context.context.get("initiator")
251
+ if initiator is None:
252
+ raise LookupError("Initiator not found in short_state context.")
253
+
254
+ for hasher, waiter_hash in waiter_hashes.items():
255
+ self.storage[hasher].pop(waiter_hash, None)
256
+
257
+ return (
258
+ initiator,
259
+ short_state.context.event,
260
+ *unpack(short_state.context.context),
261
+ )
262
+
263
+
264
+ __all__ = ("WaiterMachine",)
@@ -0,0 +1,77 @@
1
+ import datetime
2
+ import typing
3
+
4
+ from telegrinder.api.api import API
5
+ from telegrinder.bot.cute_types.update import UpdateCute
6
+ from telegrinder.bot.dispatch.context import Context
7
+ from telegrinder.bot.dispatch.handler.func import FuncHandler
8
+ from telegrinder.bot.dispatch.middleware.abc import ABCMiddleware
9
+ from telegrinder.bot.dispatch.process import check_rule
10
+ from telegrinder.bot.dispatch.waiter_machine.short_state import ShortStateContext
11
+ from telegrinder.modules import logger
12
+ from telegrinder.types.objects import Update
13
+
14
+ if typing.TYPE_CHECKING:
15
+ from telegrinder.bot.dispatch.waiter_machine.hasher import Hasher
16
+ from telegrinder.bot.dispatch.waiter_machine.machine import WaiterMachine
17
+ from telegrinder.bot.dispatch.waiter_machine.short_state import ShortState
18
+
19
+
20
+ class WaiterMiddleware(ABCMiddleware):
21
+ def __init__(
22
+ self,
23
+ machine: WaiterMachine,
24
+ hasher: Hasher[typing.Any, typing.Any],
25
+ ) -> None:
26
+ self.machine = machine
27
+ self.hasher = hasher
28
+
29
+ async def pre(self, update_cute: UpdateCute, raw_update: Update, api: API, ctx: Context) -> bool:
30
+ if self.hasher not in self.machine.storage:
31
+ return True
32
+
33
+ event = update_cute.incoming_update
34
+ key = self.hasher.get_hash_from_data_from_event(event)
35
+ if not key:
36
+ await logger.ainfo("Unable to get hash from event with hasher {!r}", self.hasher)
37
+ return True
38
+
39
+ short_state: ShortState | None = self.machine.storage[self.hasher].get(key.unwrap())
40
+ if not short_state:
41
+ return True
42
+
43
+ # Just ignore update if state expired, so it will be dropped automatically by WaiterMachine
44
+ if short_state.expiration_date is not None and datetime.datetime.now() >= short_state.expiration_date:
45
+ return True
46
+
47
+ preset_context = Context(short_state=short_state)
48
+ if short_state.context is not None:
49
+ preset_context |= short_state.context.context
50
+
51
+ if short_state.filter is not None and not await check_rule(short_state.filter, preset_context):
52
+ await logger.adebug("Filter rule {!r} failed!", short_state.filter)
53
+ return True
54
+
55
+ handler = FuncHandler(
56
+ function=self.pass_runtime,
57
+ rules=(short_state.release,) if short_state.release is not None else None,
58
+ preset_context=preset_context,
59
+ )
60
+
61
+ if not await handler.run(api, raw_update, ctx) and (on_miss := short_state.actions.get("on_miss")) is not None:
62
+ await on_miss.run(api, raw_update, ctx)
63
+
64
+ return False
65
+
66
+ async def pass_runtime(
67
+ self,
68
+ event: UpdateCute,
69
+ ctx: Context,
70
+ short_state: ShortState,
71
+ ) -> None:
72
+ ctx.initiator = self.hasher
73
+ short_state.context = ShortStateContext(event.incoming_update, ctx)
74
+ short_state.event.set()
75
+
76
+
77
+ __all__ = ("WaiterMiddleware",)
@@ -0,0 +1,105 @@
1
+ import asyncio
2
+ import dataclasses
3
+ import datetime
4
+ import typing
5
+ from functools import partial
6
+
7
+ from telegrinder.bot.dispatch.context import Context
8
+ from telegrinder.bot.rules.abc import ABCRule
9
+ from telegrinder.tools.aio import cancel_future
10
+ from telegrinder.tools.global_context.builtin_context import TelegrinderContext
11
+
12
+ if typing.TYPE_CHECKING:
13
+ from telegrinder.bot.cute_types.base import BaseCute
14
+ from telegrinder.bot.dispatch.view.base import View
15
+ from telegrinder.bot.dispatch.waiter_machine.actions import WaiterActions
16
+ from telegrinder.bot.dispatch.waiter_machine.hasher import Hasher
17
+ from telegrinder.bot.dispatch.waiter_machine.machine import HasherWithData
18
+
19
+ TELEGRINDER_CONTEXT: typing.Final = TelegrinderContext()
20
+
21
+
22
+ def _wrap_async_fn[**P](
23
+ fn: typing.Callable[P, typing.Any],
24
+ /,
25
+ ) -> typing.Callable[P, None]:
26
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
27
+ TELEGRINDER_CONTEXT.loop_wrapper.add_task(fn(*args, **kwargs))
28
+
29
+ return wrapper
30
+
31
+
32
+ class ShortStateContext[Event: BaseCute[typing.Any] = typing.Any](typing.NamedTuple):
33
+ event: Event
34
+ context: Context
35
+
36
+
37
+ @dataclasses.dataclass
38
+ class ShortState[Event: BaseCute[typing.Any] = typing.Any]:
39
+ event: asyncio.Event
40
+ actions: WaiterActions[Event]
41
+
42
+ release: ABCRule | None = dataclasses.field(
43
+ default=None,
44
+ kw_only=True,
45
+ )
46
+ filter: ABCRule | None = dataclasses.field(
47
+ default=None,
48
+ kw_only=True,
49
+ )
50
+ expiration: dataclasses.InitVar[datetime.timedelta] = dataclasses.field(
51
+ kw_only=True,
52
+ )
53
+
54
+ expiration_date: datetime.datetime | None = dataclasses.field(init=False, kw_only=True)
55
+ creation_date: datetime.datetime = dataclasses.field(init=False)
56
+ context: ShortStateContext[Event] | None = dataclasses.field(default=None, init=False, kw_only=True)
57
+ drop_timer: asyncio.TimerHandle | None = dataclasses.field(default=None, init=False)
58
+
59
+ def __post_init__(self, expiration: datetime.timedelta) -> None:
60
+ self.lifetime = expiration
61
+ self.creation_date = datetime.datetime.now()
62
+ self.expiration_date = (self.creation_date + expiration) if expiration is not None else None
63
+
64
+ def schedule_drop[HasherData](
65
+ self,
66
+ dropper: typing.Callable[..., typing.Any],
67
+ /,
68
+ hasher: Hasher[Event, HasherData],
69
+ data: HasherData,
70
+ **context: typing.Any,
71
+ ) -> None:
72
+ self.drop_timer = asyncio.get_running_loop().call_later(
73
+ delay=self.lifetime.total_seconds(),
74
+ callback=partial(_wrap_async_fn(dropper), self, hasher, data, expired=True, **context),
75
+ )
76
+
77
+ def schedule_drop_many[ViewType: View, HasherData](
78
+ self,
79
+ dropper: typing.Callable[..., typing.Any],
80
+ /,
81
+ *hashers: HasherWithData[Event, ViewType, HasherData],
82
+ **context: typing.Any,
83
+ ) -> None:
84
+ self.drop_timer = asyncio.get_running_loop().call_later(
85
+ delay=self.lifetime.total_seconds(),
86
+ callback=partial(_wrap_async_fn(dropper), self, *hashers, expired=True, **context),
87
+ )
88
+
89
+ def cancel_drop(self) -> None:
90
+ if self.drop_timer is not None:
91
+ self.drop_timer.cancel()
92
+ self.drop_timer = None
93
+
94
+ async def cancel(self) -> None:
95
+ waiters = typing.cast(
96
+ "typing.Iterable[asyncio.Future[typing.Any]]",
97
+ self.event._waiters, # type: ignore
98
+ )
99
+
100
+ for future in waiters:
101
+ if not future.cancelled():
102
+ await cancel_future(future)
103
+
104
+
105
+ __all__ = ("ShortState", "ShortStateContext")
@@ -0,0 +1,4 @@
1
+ from .abc import ABCPolling
2
+ from .polling import Polling
3
+
4
+ __all__ = ("ABCPolling", "Polling")
@@ -0,0 +1,25 @@
1
+ import typing
2
+ from abc import ABC, abstractmethod
3
+
4
+ import msgspec
5
+
6
+ from telegrinder.types.objects import Update
7
+
8
+
9
+ class ABCPolling(ABC):
10
+ offset: int
11
+
12
+ @abstractmethod
13
+ async def get_updates(self) -> list[msgspec.Raw]:
14
+ pass
15
+
16
+ @abstractmethod
17
+ async def listen(self) -> typing.AsyncGenerator[list[Update], None]:
18
+ yield []
19
+
20
+ @abstractmethod
21
+ def stop(self) -> None:
22
+ pass
23
+
24
+
25
+ __all__ = ("ABCPolling",)
@@ -0,0 +1,93 @@
1
+ import asyncio
2
+ import sys
3
+ import typing
4
+
5
+ from telegrinder.api.error import APIServerError, InvalidTokenError
6
+ from telegrinder.modules import logger
7
+ from telegrinder.tools.aio import maybe_awaitable
8
+
9
+ if typing.TYPE_CHECKING:
10
+ from telegrinder.bot.polling.polling import Polling
11
+
12
+
13
+ class ErrorHandler:
14
+ _handlers: dict[type[BaseException], typing.Callable[[BaseException], typing.Any]]
15
+
16
+ __slots__ = ("_polling", "_handlers")
17
+
18
+ def __init__(self, polling: Polling, /) -> None:
19
+ self._polling = polling
20
+ self._handlers = { # type: ignore
21
+ InvalidTokenError: self._handle_invalid_token_error,
22
+ asyncio.CancelledError: self._handle_cancelled_error,
23
+ APIServerError: self._handle_api_server_error,
24
+ **{e: self._handle_connection_timeout_error for e in polling.api.http.CONNECTION_TIMEOUT_ERRORS},
25
+ **{e: self._handle_client_connection_error for e in polling.api.http.CLIENT_CONNECTION_ERRORS},
26
+ }
27
+
28
+ async def handle(self, error: BaseException) -> bool:
29
+ error_class = type(error)
30
+
31
+ if error_class is SystemExit:
32
+ self._handle_system_exit(error) # type: ignore
33
+
34
+ if error_class not in self._handlers:
35
+ return False
36
+
37
+ try:
38
+ await maybe_awaitable(self._handlers[error_class](error))
39
+ return True
40
+ except SystemExit as sys_exit_err:
41
+ await self._handle_system_exit(sys_exit_err)
42
+
43
+ async def _handle_system_exit(self, error: SystemExit) -> typing.NoReturn:
44
+ await logger.aerror("Forced exit from the program with code {}.", error.code)
45
+ raise error from None
46
+
47
+ async def _handle_invalid_token_error(
48
+ self,
49
+ error: InvalidTokenError,
50
+ ) -> typing.NoReturn:
51
+ await logger.aerror("{}", error)
52
+ self._polling.stop()
53
+ sys.exit(3)
54
+
55
+ async def _handle_cancelled_error(self, _: asyncio.CancelledError) -> None:
56
+ await logger.ainfo("Caught cancel, stopping polling")
57
+ self._polling.stop()
58
+
59
+ async def _handle_connection_timeout_error(self, _: BaseException) -> None:
60
+ if self._polling.reconnects_counter > self._polling.max_reconnects:
61
+ await logger.aerror(
62
+ "Failed to reconnect to Telegram API server after {} attempts, stopping polling",
63
+ self._polling.max_reconnects,
64
+ )
65
+ self._polling.stop()
66
+ sys.exit(6)
67
+
68
+ await logger.awarning(
69
+ "Server disconnected, waiting {} seconds to reconnect...",
70
+ self._polling.reconnect_after,
71
+ )
72
+ await asyncio.sleep(self._polling.reconnect_after)
73
+
74
+ async def _handle_client_connection_error(self, _: BaseException) -> None:
75
+ await logger.aerror(
76
+ "Client connection failed, attempt to reconnect after {} seconds...",
77
+ self._polling.reconnect_after,
78
+ )
79
+ await asyncio.sleep(self._polling.reconnect_after)
80
+
81
+ async def _handle_api_server_error(
82
+ self,
83
+ error: APIServerError,
84
+ ) -> None:
85
+ if error.retry_after is None:
86
+ await logger.aerror("{}", error)
87
+ sys.exit(9)
88
+
89
+ await logger.aerror("{}, waiting {} seconds to the next request...", error, error.retry_after)
90
+ await asyncio.sleep(error.retry_after)
91
+
92
+
93
+ __all__ = ("ErrorHandler",)