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.
- telegrinder/__init__.py +258 -0
- telegrinder/__meta__.py +1 -0
- telegrinder/api/__init__.py +15 -0
- telegrinder/api/api.py +175 -0
- telegrinder/api/error.py +50 -0
- telegrinder/api/response.py +23 -0
- telegrinder/api/token.py +30 -0
- telegrinder/api/validators.py +30 -0
- telegrinder/bot/__init__.py +144 -0
- telegrinder/bot/bot.py +70 -0
- telegrinder/bot/cute_types/__init__.py +41 -0
- telegrinder/bot/cute_types/base.py +228 -0
- telegrinder/bot/cute_types/base.pyi +49 -0
- telegrinder/bot/cute_types/business_connection.py +9 -0
- telegrinder/bot/cute_types/business_messages_deleted.py +9 -0
- telegrinder/bot/cute_types/callback_query.py +248 -0
- telegrinder/bot/cute_types/chat_boost_removed.py +9 -0
- telegrinder/bot/cute_types/chat_boost_updated.py +9 -0
- telegrinder/bot/cute_types/chat_join_request.py +59 -0
- telegrinder/bot/cute_types/chat_member_updated.py +158 -0
- telegrinder/bot/cute_types/chosen_inline_result.py +11 -0
- telegrinder/bot/cute_types/inline_query.py +41 -0
- telegrinder/bot/cute_types/message.py +2809 -0
- telegrinder/bot/cute_types/message_reaction_count_updated.py +9 -0
- telegrinder/bot/cute_types/message_reaction_updated.py +9 -0
- telegrinder/bot/cute_types/paid_media_purchased.py +11 -0
- telegrinder/bot/cute_types/poll.py +9 -0
- telegrinder/bot/cute_types/poll_answer.py +9 -0
- telegrinder/bot/cute_types/pre_checkout_query.py +36 -0
- telegrinder/bot/cute_types/shipping_query.py +11 -0
- telegrinder/bot/cute_types/update.py +209 -0
- telegrinder/bot/cute_types/utils.py +141 -0
- telegrinder/bot/dispatch/__init__.py +99 -0
- telegrinder/bot/dispatch/abc.py +74 -0
- telegrinder/bot/dispatch/action.py +99 -0
- telegrinder/bot/dispatch/context.py +162 -0
- telegrinder/bot/dispatch/dispatch.py +362 -0
- telegrinder/bot/dispatch/handler/__init__.py +23 -0
- telegrinder/bot/dispatch/handler/abc.py +25 -0
- telegrinder/bot/dispatch/handler/audio_reply.py +43 -0
- telegrinder/bot/dispatch/handler/base.py +34 -0
- telegrinder/bot/dispatch/handler/document_reply.py +43 -0
- telegrinder/bot/dispatch/handler/func.py +73 -0
- telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
- telegrinder/bot/dispatch/handler/message_reply.py +35 -0
- telegrinder/bot/dispatch/handler/photo_reply.py +43 -0
- telegrinder/bot/dispatch/handler/sticker_reply.py +36 -0
- telegrinder/bot/dispatch/handler/video_reply.py +43 -0
- telegrinder/bot/dispatch/middleware/__init__.py +13 -0
- telegrinder/bot/dispatch/middleware/abc.py +112 -0
- telegrinder/bot/dispatch/middleware/box.py +32 -0
- telegrinder/bot/dispatch/middleware/filter.py +88 -0
- telegrinder/bot/dispatch/middleware/media_group.py +69 -0
- telegrinder/bot/dispatch/process.py +93 -0
- telegrinder/bot/dispatch/return_manager/__init__.py +21 -0
- telegrinder/bot/dispatch/return_manager/abc.py +107 -0
- telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
- telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
- telegrinder/bot/dispatch/return_manager/message.py +34 -0
- telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +19 -0
- telegrinder/bot/dispatch/return_manager/utils.py +20 -0
- telegrinder/bot/dispatch/router/__init__.py +4 -0
- telegrinder/bot/dispatch/router/abc.py +15 -0
- telegrinder/bot/dispatch/router/base.py +154 -0
- telegrinder/bot/dispatch/view/__init__.py +15 -0
- telegrinder/bot/dispatch/view/abc.py +15 -0
- telegrinder/bot/dispatch/view/base.py +226 -0
- telegrinder/bot/dispatch/view/box.py +207 -0
- telegrinder/bot/dispatch/view/media_group.py +25 -0
- telegrinder/bot/dispatch/waiter_machine/__init__.py +25 -0
- telegrinder/bot/dispatch/waiter_machine/actions.py +16 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +13 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +53 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +61 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +49 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +264 -0
- telegrinder/bot/dispatch/waiter_machine/middleware.py +77 -0
- telegrinder/bot/dispatch/waiter_machine/short_state.py +105 -0
- telegrinder/bot/polling/__init__.py +4 -0
- telegrinder/bot/polling/abc.py +25 -0
- telegrinder/bot/polling/error_handler.py +93 -0
- telegrinder/bot/polling/polling.py +167 -0
- telegrinder/bot/polling/utils.py +12 -0
- telegrinder/bot/rules/__init__.py +166 -0
- telegrinder/bot/rules/abc.py +150 -0
- telegrinder/bot/rules/button.py +20 -0
- telegrinder/bot/rules/callback_data.py +109 -0
- telegrinder/bot/rules/chat_join.py +28 -0
- telegrinder/bot/rules/chat_member_updated.py +145 -0
- telegrinder/bot/rules/command.py +137 -0
- telegrinder/bot/rules/enum_text.py +29 -0
- telegrinder/bot/rules/func.py +21 -0
- telegrinder/bot/rules/fuzzy.py +21 -0
- telegrinder/bot/rules/inline.py +45 -0
- telegrinder/bot/rules/integer.py +19 -0
- telegrinder/bot/rules/is_from.py +213 -0
- telegrinder/bot/rules/logic.py +22 -0
- telegrinder/bot/rules/magic.py +60 -0
- telegrinder/bot/rules/markup.py +51 -0
- telegrinder/bot/rules/media.py +13 -0
- telegrinder/bot/rules/mention.py +15 -0
- telegrinder/bot/rules/message_entities.py +37 -0
- telegrinder/bot/rules/node.py +43 -0
- telegrinder/bot/rules/payload.py +89 -0
- telegrinder/bot/rules/payment_invoice.py +14 -0
- telegrinder/bot/rules/regex.py +34 -0
- telegrinder/bot/rules/rule_enum.py +71 -0
- telegrinder/bot/rules/start.py +73 -0
- telegrinder/bot/rules/state.py +35 -0
- telegrinder/bot/rules/text.py +27 -0
- telegrinder/bot/rules/update.py +14 -0
- telegrinder/bot/scenario/__init__.py +5 -0
- telegrinder/bot/scenario/abc.py +16 -0
- telegrinder/bot/scenario/checkbox.py +183 -0
- telegrinder/bot/scenario/choice.py +44 -0
- telegrinder/client/__init__.py +11 -0
- telegrinder/client/abc.py +136 -0
- telegrinder/client/form_data.py +34 -0
- telegrinder/client/rnet.py +198 -0
- telegrinder/model.py +133 -0
- telegrinder/model.pyi +57 -0
- telegrinder/modules.py +1081 -0
- telegrinder/msgspec_utils/__init__.py +42 -0
- telegrinder/msgspec_utils/abc.py +16 -0
- telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
- telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
- telegrinder/msgspec_utils/custom_types/enum_meta.py +61 -0
- telegrinder/msgspec_utils/custom_types/literal.py +25 -0
- telegrinder/msgspec_utils/custom_types/option.py +17 -0
- telegrinder/msgspec_utils/decoder.py +388 -0
- telegrinder/msgspec_utils/encoder.py +204 -0
- telegrinder/msgspec_utils/json.py +15 -0
- telegrinder/msgspec_utils/tools.py +80 -0
- telegrinder/node/__init__.py +80 -0
- telegrinder/node/compose.py +193 -0
- telegrinder/node/nodes/__init__.py +96 -0
- telegrinder/node/nodes/attachment.py +169 -0
- telegrinder/node/nodes/callback_query.py +25 -0
- telegrinder/node/nodes/channel.py +97 -0
- telegrinder/node/nodes/command.py +33 -0
- telegrinder/node/nodes/error.py +43 -0
- telegrinder/node/nodes/event.py +70 -0
- telegrinder/node/nodes/file.py +39 -0
- telegrinder/node/nodes/global_node.py +66 -0
- telegrinder/node/nodes/i18n.py +110 -0
- telegrinder/node/nodes/me.py +26 -0
- telegrinder/node/nodes/message_entities.py +15 -0
- telegrinder/node/nodes/payload.py +84 -0
- telegrinder/node/nodes/reply_message.py +14 -0
- telegrinder/node/nodes/source.py +172 -0
- telegrinder/node/nodes/state_mutator.py +71 -0
- telegrinder/node/nodes/text.py +62 -0
- telegrinder/node/scope.py +88 -0
- telegrinder/node/utils.py +38 -0
- telegrinder/py.typed +0 -0
- telegrinder/rules.py +1 -0
- telegrinder/tools/__init__.py +183 -0
- telegrinder/tools/aio.py +147 -0
- telegrinder/tools/final.py +21 -0
- telegrinder/tools/formatting/__init__.py +85 -0
- telegrinder/tools/formatting/deep_links/__init__.py +39 -0
- telegrinder/tools/formatting/deep_links/links.py +468 -0
- telegrinder/tools/formatting/deep_links/parsing.py +88 -0
- telegrinder/tools/formatting/deep_links/validators.py +8 -0
- telegrinder/tools/formatting/html.py +241 -0
- telegrinder/tools/fullname.py +82 -0
- telegrinder/tools/global_context/__init__.py +13 -0
- telegrinder/tools/global_context/abc.py +63 -0
- telegrinder/tools/global_context/builtin_context.py +45 -0
- telegrinder/tools/global_context/global_context.py +614 -0
- telegrinder/tools/input_file_directory.py +30 -0
- telegrinder/tools/keyboard/__init__.py +6 -0
- telegrinder/tools/keyboard/abc.py +84 -0
- telegrinder/tools/keyboard/base.py +108 -0
- telegrinder/tools/keyboard/button.py +181 -0
- telegrinder/tools/keyboard/data.py +31 -0
- telegrinder/tools/keyboard/keyboard.py +160 -0
- telegrinder/tools/keyboard/utils.py +95 -0
- telegrinder/tools/lifespan.py +188 -0
- telegrinder/tools/limited_dict.py +35 -0
- telegrinder/tools/loop_wrapper.py +271 -0
- telegrinder/tools/magic/__init__.py +29 -0
- telegrinder/tools/magic/annotations.py +172 -0
- telegrinder/tools/magic/descriptors.py +57 -0
- telegrinder/tools/magic/function.py +254 -0
- telegrinder/tools/magic/inspect.py +16 -0
- telegrinder/tools/magic/shortcut.py +107 -0
- telegrinder/tools/member_descriptor_proxy.py +95 -0
- telegrinder/tools/parse_mode.py +12 -0
- telegrinder/tools/serialization/__init__.py +5 -0
- telegrinder/tools/serialization/abc.py +34 -0
- telegrinder/tools/serialization/json_ser.py +60 -0
- telegrinder/tools/serialization/msgpack_ser.py +197 -0
- telegrinder/tools/serialization/utils.py +18 -0
- telegrinder/tools/singleton/__init__.py +4 -0
- telegrinder/tools/singleton/abc.py +14 -0
- telegrinder/tools/singleton/singleton.py +18 -0
- telegrinder/tools/state_mutator/__init__.py +4 -0
- telegrinder/tools/state_mutator/mutation.py +85 -0
- telegrinder/tools/state_storage/__init__.py +4 -0
- telegrinder/tools/state_storage/abc.py +38 -0
- telegrinder/tools/state_storage/memory.py +27 -0
- telegrinder/tools/strings.py +22 -0
- telegrinder/types/__init__.py +323 -0
- telegrinder/types/enums.py +754 -0
- telegrinder/types/input_file.py +51 -0
- telegrinder/types/methods.py +6143 -0
- telegrinder/types/methods_utils.py +66 -0
- telegrinder/types/objects.py +8184 -0
- telegrinder/types/webapp.py +129 -0
- telegrinder/verification_utils.py +35 -0
- telegrinder-1.0.0rc1.dist-info/METADATA +166 -0
- telegrinder-1.0.0rc1.dist-info/RECORD +215 -0
- telegrinder-1.0.0rc1.dist-info/WHEEL +4 -0
- 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,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",)
|