telegrinder 0.3.4.post1__py3-none-any.whl → 0.4.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.
Potentially problematic release.
This version of telegrinder might be problematic. Click here for more details.
- telegrinder/__init__.py +30 -31
- telegrinder/api/__init__.py +2 -1
- telegrinder/api/api.py +28 -20
- telegrinder/api/error.py +8 -4
- telegrinder/api/response.py +2 -2
- telegrinder/api/token.py +2 -2
- telegrinder/bot/__init__.py +6 -0
- telegrinder/bot/bot.py +38 -31
- telegrinder/bot/cute_types/__init__.py +2 -0
- telegrinder/bot/cute_types/base.py +54 -128
- telegrinder/bot/cute_types/callback_query.py +76 -61
- telegrinder/bot/cute_types/chat_join_request.py +4 -3
- telegrinder/bot/cute_types/chat_member_updated.py +28 -31
- telegrinder/bot/cute_types/inline_query.py +5 -4
- telegrinder/bot/cute_types/message.py +555 -602
- telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
- telegrinder/bot/cute_types/update.py +20 -12
- telegrinder/bot/cute_types/utils.py +3 -36
- telegrinder/bot/dispatch/__init__.py +4 -0
- telegrinder/bot/dispatch/abc.py +8 -9
- telegrinder/bot/dispatch/context.py +5 -7
- telegrinder/bot/dispatch/dispatch.py +85 -33
- telegrinder/bot/dispatch/handler/abc.py +5 -6
- telegrinder/bot/dispatch/handler/audio_reply.py +2 -2
- telegrinder/bot/dispatch/handler/base.py +3 -3
- telegrinder/bot/dispatch/handler/document_reply.py +2 -2
- telegrinder/bot/dispatch/handler/func.py +36 -42
- telegrinder/bot/dispatch/handler/media_group_reply.py +5 -4
- telegrinder/bot/dispatch/handler/message_reply.py +2 -2
- telegrinder/bot/dispatch/handler/photo_reply.py +2 -2
- telegrinder/bot/dispatch/handler/sticker_reply.py +2 -2
- telegrinder/bot/dispatch/handler/video_reply.py +2 -2
- telegrinder/bot/dispatch/middleware/abc.py +83 -8
- telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
- telegrinder/bot/dispatch/process.py +44 -50
- telegrinder/bot/dispatch/return_manager/__init__.py +2 -0
- telegrinder/bot/dispatch/return_manager/abc.py +6 -10
- telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +20 -0
- telegrinder/bot/dispatch/view/__init__.py +2 -0
- telegrinder/bot/dispatch/view/abc.py +10 -6
- telegrinder/bot/dispatch/view/base.py +81 -50
- telegrinder/bot/dispatch/view/box.py +20 -9
- telegrinder/bot/dispatch/view/callback_query.py +3 -4
- telegrinder/bot/dispatch/view/chat_join_request.py +2 -7
- telegrinder/bot/dispatch/view/chat_member.py +3 -5
- telegrinder/bot/dispatch/view/inline_query.py +3 -4
- telegrinder/bot/dispatch/view/message.py +3 -4
- telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
- telegrinder/bot/dispatch/view/raw.py +42 -40
- telegrinder/bot/dispatch/waiter_machine/actions.py +5 -4
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +9 -7
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/state.py +3 -2
- telegrinder/bot/dispatch/waiter_machine/machine.py +113 -34
- telegrinder/bot/dispatch/waiter_machine/middleware.py +15 -10
- telegrinder/bot/dispatch/waiter_machine/short_state.py +7 -18
- telegrinder/bot/polling/polling.py +62 -54
- telegrinder/bot/rules/__init__.py +24 -1
- telegrinder/bot/rules/abc.py +17 -10
- telegrinder/bot/rules/callback_data.py +20 -61
- telegrinder/bot/rules/chat_join.py +6 -4
- telegrinder/bot/rules/command.py +4 -4
- telegrinder/bot/rules/enum_text.py +1 -4
- telegrinder/bot/rules/func.py +5 -3
- telegrinder/bot/rules/fuzzy.py +1 -1
- telegrinder/bot/rules/id.py +24 -0
- telegrinder/bot/rules/inline.py +6 -4
- telegrinder/bot/rules/integer.py +2 -1
- telegrinder/bot/rules/logic.py +18 -0
- telegrinder/bot/rules/markup.py +5 -6
- telegrinder/bot/rules/message.py +2 -4
- telegrinder/bot/rules/message_entities.py +1 -3
- telegrinder/bot/rules/node.py +15 -9
- telegrinder/bot/rules/payload.py +81 -0
- telegrinder/bot/rules/payment_invoice.py +29 -0
- telegrinder/bot/rules/regex.py +5 -6
- telegrinder/bot/rules/state.py +1 -3
- telegrinder/bot/rules/text.py +10 -5
- telegrinder/bot/rules/update.py +0 -0
- telegrinder/bot/scenario/abc.py +2 -4
- telegrinder/bot/scenario/checkbox.py +12 -14
- telegrinder/bot/scenario/choice.py +6 -9
- telegrinder/client/__init__.py +9 -1
- telegrinder/client/abc.py +35 -10
- telegrinder/client/aiohttp.py +28 -24
- telegrinder/client/form_data.py +31 -0
- telegrinder/client/sonic.py +212 -0
- telegrinder/model.py +38 -145
- telegrinder/modules.py +3 -1
- telegrinder/msgspec_utils.py +136 -68
- telegrinder/node/__init__.py +74 -13
- telegrinder/node/attachment.py +92 -16
- telegrinder/node/base.py +196 -68
- telegrinder/node/callback_query.py +17 -16
- telegrinder/node/command.py +3 -2
- telegrinder/node/composer.py +40 -75
- telegrinder/node/container.py +13 -7
- telegrinder/node/either.py +82 -0
- telegrinder/node/event.py +20 -31
- telegrinder/node/file.py +51 -0
- telegrinder/node/me.py +4 -5
- telegrinder/node/payload.py +78 -0
- telegrinder/node/polymorphic.py +27 -8
- telegrinder/node/rule.py +2 -6
- telegrinder/node/scope.py +4 -6
- telegrinder/node/source.py +37 -21
- telegrinder/node/text.py +20 -8
- telegrinder/node/tools/generator.py +7 -11
- telegrinder/py.typed +0 -0
- telegrinder/rules.py +0 -61
- telegrinder/tools/__init__.py +97 -38
- telegrinder/tools/adapter/__init__.py +19 -0
- telegrinder/tools/adapter/abc.py +49 -0
- telegrinder/tools/adapter/dataclass.py +56 -0
- telegrinder/{bot/rules → tools}/adapter/event.py +8 -10
- telegrinder/{bot/rules → tools}/adapter/node.py +8 -10
- telegrinder/{bot/rules → tools}/adapter/raw_event.py +2 -2
- telegrinder/{bot/rules → tools}/adapter/raw_update.py +2 -2
- telegrinder/tools/buttons.py +52 -26
- telegrinder/tools/callback_data_serilization/__init__.py +5 -0
- telegrinder/tools/callback_data_serilization/abc.py +51 -0
- telegrinder/tools/callback_data_serilization/json_ser.py +60 -0
- telegrinder/tools/callback_data_serilization/msgpack_ser.py +172 -0
- telegrinder/tools/error_handler/abc.py +4 -7
- telegrinder/tools/error_handler/error.py +0 -0
- telegrinder/tools/error_handler/error_handler.py +34 -48
- telegrinder/tools/formatting/__init__.py +57 -37
- telegrinder/tools/formatting/deep_links.py +541 -0
- telegrinder/tools/formatting/{html.py → html_formatter.py} +51 -79
- telegrinder/tools/formatting/spec_html_formats.py +14 -60
- telegrinder/tools/functional.py +1 -5
- telegrinder/tools/global_context/global_context.py +26 -51
- telegrinder/tools/global_context/telegrinder_ctx.py +3 -3
- telegrinder/tools/i18n/abc.py +0 -0
- telegrinder/tools/i18n/middleware/abc.py +3 -6
- telegrinder/tools/input_file_directory.py +30 -0
- telegrinder/tools/keyboard.py +9 -9
- telegrinder/tools/lifespan.py +105 -0
- telegrinder/tools/limited_dict.py +5 -10
- telegrinder/tools/loop_wrapper/abc.py +7 -2
- telegrinder/tools/loop_wrapper/loop_wrapper.py +40 -95
- telegrinder/tools/magic.py +184 -34
- telegrinder/tools/state_storage/__init__.py +0 -0
- telegrinder/tools/state_storage/abc.py +5 -9
- telegrinder/tools/state_storage/memory.py +1 -1
- telegrinder/tools/strings.py +13 -0
- telegrinder/types/__init__.py +8 -0
- telegrinder/types/enums.py +31 -21
- telegrinder/types/input_file.py +51 -0
- telegrinder/types/methods.py +531 -109
- telegrinder/types/objects.py +934 -826
- telegrinder/verification_utils.py +0 -2
- {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/LICENSE +2 -2
- telegrinder-0.4.0.dist-info/METADATA +144 -0
- telegrinder-0.4.0.dist-info/RECORD +182 -0
- {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/WHEEL +1 -1
- telegrinder/bot/rules/adapter/__init__.py +0 -17
- telegrinder/bot/rules/adapter/abc.py +0 -31
- telegrinder/node/message.py +0 -14
- telegrinder/node/update.py +0 -15
- telegrinder/tools/formatting/links.py +0 -38
- telegrinder/tools/kb_set/__init__.py +0 -4
- telegrinder/tools/kb_set/base.py +0 -15
- telegrinder/tools/kb_set/yaml.py +0 -63
- telegrinder-0.3.4.post1.dist-info/METADATA +0 -110
- telegrinder-0.3.4.post1.dist-info/RECORD +0 -165
- /telegrinder/{bot/rules → tools}/adapter/errors.py +0 -0
|
@@ -1,93 +1,94 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
3
|
from telegrinder.api.api import API
|
|
4
|
+
from telegrinder.bot.cute_types.base import BaseCute
|
|
4
5
|
from telegrinder.bot.cute_types.update import UpdateCute
|
|
6
|
+
from telegrinder.bot.dispatch.context import Context
|
|
5
7
|
from telegrinder.bot.dispatch.handler.func import Func, FuncHandler
|
|
6
8
|
from telegrinder.bot.dispatch.process import process_inner
|
|
7
9
|
from telegrinder.bot.dispatch.view.abc import ABCEventRawView
|
|
8
|
-
from telegrinder.bot.dispatch.view.base import BaseView
|
|
10
|
+
from telegrinder.bot.dispatch.view.base import BaseView
|
|
9
11
|
from telegrinder.bot.rules.abc import ABCRule
|
|
10
12
|
from telegrinder.tools.error_handler.error_handler import ABCErrorHandler, ErrorHandler
|
|
11
13
|
from telegrinder.types.enums import UpdateType
|
|
12
14
|
from telegrinder.types.objects import Update
|
|
13
15
|
|
|
14
|
-
T = typing.TypeVar("T", contravariant=True)
|
|
15
|
-
R = typing.TypeVar("R", covariant=True)
|
|
16
|
-
P = typing.ParamSpec("P")
|
|
17
|
-
|
|
18
16
|
|
|
19
17
|
class RawEventView(ABCEventRawView[UpdateCute], BaseView[UpdateCute]):
|
|
20
|
-
def __init__(self) -> None:
|
|
21
|
-
self.auto_rules = []
|
|
22
|
-
self.handlers = []
|
|
23
|
-
self.middlewares = []
|
|
24
|
-
self.return_manager = None
|
|
25
|
-
|
|
26
18
|
@typing.overload
|
|
27
|
-
def __call__(
|
|
19
|
+
def __call__[**P, R](
|
|
28
20
|
self,
|
|
29
|
-
update_type: UpdateType,
|
|
30
21
|
*rules: ABCRule,
|
|
22
|
+
update_type: UpdateType,
|
|
23
|
+
final: bool = True,
|
|
31
24
|
) -> typing.Callable[
|
|
32
25
|
[Func[P, R]],
|
|
33
|
-
FuncHandler[
|
|
26
|
+
FuncHandler[BaseCute[typing.Any], Func[P, R], ErrorHandler[BaseCute[typing.Any]]],
|
|
34
27
|
]: ...
|
|
35
28
|
|
|
36
29
|
@typing.overload
|
|
37
|
-
def __call__(
|
|
30
|
+
def __call__[**P, Dataclass, R](
|
|
38
31
|
self,
|
|
39
|
-
update_type: UpdateType,
|
|
40
32
|
*rules: ABCRule,
|
|
41
|
-
dataclass: type[
|
|
42
|
-
|
|
33
|
+
dataclass: type[Dataclass],
|
|
34
|
+
final: bool = True,
|
|
35
|
+
) -> typing.Callable[[Func[P, R]], FuncHandler[Dataclass, Func[P, R], ErrorHandler[Dataclass]]]: ...
|
|
43
36
|
|
|
44
37
|
@typing.overload
|
|
45
|
-
def __call__(
|
|
38
|
+
def __call__[**P, Dataclass, R](
|
|
46
39
|
self,
|
|
40
|
+
*rules: ABCRule,
|
|
47
41
|
update_type: UpdateType,
|
|
42
|
+
dataclass: type[Dataclass],
|
|
43
|
+
final: bool = True,
|
|
44
|
+
) -> typing.Callable[[Func[P, R]], FuncHandler[Dataclass, Func[P, R], ErrorHandler[Dataclass]]]: ...
|
|
45
|
+
|
|
46
|
+
@typing.overload
|
|
47
|
+
def __call__[**P, ErrorHandlerT: ABCErrorHandler, R](
|
|
48
|
+
self,
|
|
48
49
|
*rules: ABCRule,
|
|
49
50
|
error_handler: ErrorHandlerT,
|
|
51
|
+
final: bool = True,
|
|
50
52
|
) -> typing.Callable[
|
|
51
53
|
[Func[P, R]],
|
|
52
|
-
FuncHandler[
|
|
54
|
+
FuncHandler[BaseCute[typing.Any], Func[P, R], ErrorHandlerT],
|
|
53
55
|
]: ...
|
|
54
56
|
|
|
55
57
|
@typing.overload
|
|
56
|
-
def __call__(
|
|
58
|
+
def __call__[**P, ErrorHandlerT: ABCErrorHandler, R](
|
|
57
59
|
self,
|
|
58
|
-
update_type: UpdateType,
|
|
59
60
|
*rules: ABCRule,
|
|
60
|
-
dataclass: type[typing.Any],
|
|
61
61
|
error_handler: ErrorHandlerT,
|
|
62
|
-
is_blocking: bool = True,
|
|
63
|
-
) -> typing.Callable[[Func[P, R]], FuncHandler[UpdateCute, Func[P, R], ErrorHandlerT]]: ...
|
|
64
|
-
|
|
65
|
-
@typing.overload
|
|
66
|
-
def __call__(
|
|
67
|
-
self,
|
|
68
62
|
update_type: UpdateType,
|
|
69
|
-
|
|
70
|
-
dataclass: None = None,
|
|
71
|
-
error_handler: None = None,
|
|
72
|
-
is_blocking: bool = True,
|
|
63
|
+
final: bool = True,
|
|
73
64
|
) -> typing.Callable[
|
|
74
65
|
[Func[P, R]],
|
|
75
|
-
FuncHandler[
|
|
66
|
+
FuncHandler[BaseCute[typing.Any], Func[P, R], ErrorHandlerT],
|
|
76
67
|
]: ...
|
|
77
68
|
|
|
78
|
-
|
|
69
|
+
@typing.overload
|
|
70
|
+
def __call__[**P, Dataclass, ErrorHandlerT: ABCErrorHandler, R](
|
|
79
71
|
self,
|
|
72
|
+
*rules: ABCRule,
|
|
80
73
|
update_type: UpdateType,
|
|
74
|
+
dataclass: type[Dataclass],
|
|
75
|
+
error_handler: ErrorHandlerT,
|
|
76
|
+
final: bool = True,
|
|
77
|
+
) -> typing.Callable[[Func[P, R]], FuncHandler[Dataclass, Func[P, R], ErrorHandlerT]]: ...
|
|
78
|
+
|
|
79
|
+
def __call__(
|
|
80
|
+
self,
|
|
81
81
|
*rules: ABCRule,
|
|
82
|
+
update_type: UpdateType | None = None,
|
|
82
83
|
dataclass: type[typing.Any] | None = None,
|
|
83
84
|
error_handler: ABCErrorHandler | None = None,
|
|
84
|
-
|
|
85
|
-
):
|
|
86
|
-
def wrapper(func):
|
|
85
|
+
final: bool = True,
|
|
86
|
+
) -> typing.Callable[..., typing.Any]:
|
|
87
|
+
def wrapper(func: typing.Callable[..., typing.Any]):
|
|
87
88
|
func_handler = FuncHandler(
|
|
88
89
|
func,
|
|
89
90
|
rules=[*self.auto_rules, *rules],
|
|
90
|
-
|
|
91
|
+
final=final,
|
|
91
92
|
dataclass=dataclass,
|
|
92
93
|
error_handler=error_handler or ErrorHandler(),
|
|
93
94
|
update_type=update_type,
|
|
@@ -100,11 +101,12 @@ class RawEventView(ABCEventRawView[UpdateCute], BaseView[UpdateCute]):
|
|
|
100
101
|
async def check(self, event: Update) -> bool:
|
|
101
102
|
return bool(self.handlers) or bool(self.middlewares)
|
|
102
103
|
|
|
103
|
-
async def process(self, event: Update, api: API) -> bool:
|
|
104
|
+
async def process(self, event: Update, api: API, context: Context) -> bool:
|
|
104
105
|
return await process_inner(
|
|
105
106
|
api,
|
|
106
107
|
UpdateCute.from_update(event, bound_api=api),
|
|
107
108
|
event,
|
|
109
|
+
context,
|
|
108
110
|
self.middlewares,
|
|
109
111
|
self.handlers,
|
|
110
112
|
self.return_manager,
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
|
+
from telegrinder.bot.cute_types import BaseCute
|
|
3
4
|
from telegrinder.bot.dispatch.handler.abc import ABCHandler
|
|
4
5
|
|
|
5
|
-
from .short_state import
|
|
6
|
+
from .short_state import ShortState
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
class WaiterActions(typing.TypedDict
|
|
9
|
-
on_miss: typing.NotRequired[ABCHandler[
|
|
10
|
-
on_drop: typing.NotRequired[typing.Callable[[ShortState[
|
|
9
|
+
class WaiterActions[Event: BaseCute](typing.TypedDict):
|
|
10
|
+
on_miss: typing.NotRequired[ABCHandler[Event]]
|
|
11
|
+
on_drop: typing.NotRequired[typing.Callable[[ShortState[Event]], None]]
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
__all__ = ("WaiterActions",)
|
|
File without changes
|
|
File without changes
|
|
@@ -7,12 +7,11 @@ from telegrinder.bot.cute_types import BaseCute
|
|
|
7
7
|
from telegrinder.bot.dispatch.view.base import BaseView
|
|
8
8
|
from telegrinder.tools.functional import from_optional
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Data = typing.TypeVar("Data")
|
|
10
|
+
Event = typing.TypeVar("Event", bound=BaseCute, covariant=True)
|
|
11
|
+
Data = typing.TypeVar("Data", covariant=True)
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
def _echo(__x: T) -> T:
|
|
14
|
+
def _echo[T](__x: T) -> T:
|
|
16
15
|
return __x
|
|
17
16
|
|
|
18
17
|
|
|
@@ -40,17 +39,20 @@ class Hasher(typing.Generic[Event, Data]):
|
|
|
40
39
|
def name(self) -> str:
|
|
41
40
|
return f"{self.view_class.__name__}_{id(self)}"
|
|
42
41
|
|
|
43
|
-
def get_hash_from_data(self, data:
|
|
42
|
+
def get_hash_from_data[D](self: "Hasher[Event, D]", data: D) -> Option[typing.Hashable]:
|
|
44
43
|
if self._get_hash_from_data is None:
|
|
45
44
|
raise NotImplementedError
|
|
46
45
|
return from_optional(self._get_hash_from_data(data))
|
|
47
46
|
|
|
48
|
-
def get_data_from_event(self, event:
|
|
47
|
+
def get_data_from_event[E: BaseCute](self: "Hasher[E, Data]", event: E) -> Option[Data]:
|
|
49
48
|
if not self._get_data_from_event:
|
|
50
49
|
raise NotImplementedError
|
|
51
50
|
return from_optional(self._get_data_from_event(event))
|
|
52
51
|
|
|
53
|
-
def get_hash_from_data_from_event
|
|
52
|
+
def get_hash_from_data_from_event[E: BaseCute](
|
|
53
|
+
self: "Hasher[E, Data]",
|
|
54
|
+
event: E,
|
|
55
|
+
) -> Option[typing.Hashable]:
|
|
54
56
|
return self.get_data_from_event(event).and_then(self.get_hash_from_data) # type: ignore
|
|
55
57
|
|
|
56
58
|
|
|
File without changes
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from fntypes.option import Option
|
|
2
2
|
|
|
3
|
+
from telegrinder.bot.cute_types import BaseCute
|
|
3
4
|
from telegrinder.bot.dispatch.view import BaseStateView
|
|
4
|
-
from telegrinder.bot.dispatch.waiter_machine.hasher.hasher import ECHO,
|
|
5
|
+
from telegrinder.bot.dispatch.waiter_machine.hasher.hasher import ECHO, Hasher
|
|
5
6
|
from telegrinder.tools.functional import from_optional
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
class StateViewHasher(Hasher[Event, int]):
|
|
9
|
+
class StateViewHasher[Event: BaseCute](Hasher[Event, int]):
|
|
9
10
|
view: BaseStateView[Event]
|
|
10
11
|
|
|
11
12
|
def __init__(self, view: BaseStateView[Event]) -> None:
|
|
@@ -2,29 +2,45 @@ import asyncio
|
|
|
2
2
|
import datetime
|
|
3
3
|
import typing
|
|
4
4
|
|
|
5
|
+
from telegrinder.bot.cute_types.base import BaseCute
|
|
5
6
|
from telegrinder.bot.dispatch.abc import ABCDispatch
|
|
7
|
+
from telegrinder.bot.dispatch.context import Context
|
|
6
8
|
from telegrinder.bot.dispatch.view.base import BaseStateView, BaseView
|
|
7
|
-
from telegrinder.bot.dispatch.waiter_machine.middleware import WaiterMiddleware
|
|
9
|
+
from telegrinder.bot.dispatch.waiter_machine.middleware import INITIATOR_CONTEXT_KEY, WaiterMiddleware
|
|
8
10
|
from telegrinder.bot.dispatch.waiter_machine.short_state import (
|
|
9
|
-
EventModel,
|
|
10
11
|
ShortState,
|
|
11
12
|
ShortStateContext,
|
|
12
13
|
)
|
|
13
14
|
from telegrinder.bot.rules.abc import ABCRule
|
|
15
|
+
from telegrinder.tools.lifespan import Lifespan
|
|
14
16
|
from telegrinder.tools.limited_dict import LimitedDict
|
|
15
17
|
|
|
16
18
|
from .actions import WaiterActions
|
|
17
19
|
from .hasher import Hasher, StateViewHasher
|
|
18
20
|
|
|
19
|
-
HasherData =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Hasher[EventModel, HasherData], LimitedDict[typing.Hashable, ShortState[EventModel]]
|
|
21
|
+
type Storage[Event: BaseCute, HasherData] = dict[
|
|
22
|
+
Hasher[Event, HasherData],
|
|
23
|
+
LimitedDict[typing.Hashable, ShortState[Event]],
|
|
23
24
|
]
|
|
25
|
+
type HasherWithData[Event: BaseCute, Data] = tuple[Hasher[Event, Data], Data]
|
|
24
26
|
|
|
25
27
|
WEEK: typing.Final[datetime.timedelta] = datetime.timedelta(days=7)
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
class ContextUnpackProto[*Ts](typing.Protocol):
|
|
31
|
+
__name__: str
|
|
32
|
+
|
|
33
|
+
def __call__(self, context: Context, /) -> tuple[*Ts]: ...
|
|
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
|
+
|
|
28
44
|
class WaiterMachine:
|
|
29
45
|
def __init__(
|
|
30
46
|
self,
|
|
@@ -39,20 +55,20 @@ class WaiterMachine:
|
|
|
39
55
|
self.storage: Storage = {}
|
|
40
56
|
|
|
41
57
|
def __repr__(self) -> str:
|
|
42
|
-
return "<{}: max_storage_size={}, base_state_lifetime={!r}>".format(
|
|
58
|
+
return "<{}: with {} storage items and max_storage_size={}, base_state_lifetime={!r}>".format(
|
|
43
59
|
self.__class__.__name__,
|
|
60
|
+
len(self.storage),
|
|
44
61
|
self.max_storage_size,
|
|
45
62
|
self.base_state_lifetime,
|
|
46
63
|
)
|
|
47
64
|
|
|
48
|
-
def create_middleware(self, view: BaseStateView[
|
|
65
|
+
def create_middleware[Event: BaseCute](self, view: BaseStateView[Event]) -> WaiterMiddleware[Event]:
|
|
49
66
|
hasher = StateViewHasher(view)
|
|
50
67
|
self.storage[hasher] = LimitedDict(maxlimit=self.max_storage_size)
|
|
51
68
|
return WaiterMiddleware(self, hasher)
|
|
52
69
|
|
|
53
70
|
async def drop_all(self) -> None:
|
|
54
71
|
"""Drops all waiters in storage."""
|
|
55
|
-
|
|
56
72
|
for hasher in self.storage.copy():
|
|
57
73
|
for ident, short_state in self.storage[hasher].items():
|
|
58
74
|
if short_state.context:
|
|
@@ -62,17 +78,17 @@ class WaiterMachine:
|
|
|
62
78
|
|
|
63
79
|
del self.storage[hasher]
|
|
64
80
|
|
|
65
|
-
async def drop(
|
|
81
|
+
async def drop[Event: BaseCute, HasherData](
|
|
66
82
|
self,
|
|
67
|
-
hasher: Hasher[
|
|
68
|
-
|
|
83
|
+
hasher: Hasher[Event, HasherData],
|
|
84
|
+
data: HasherData,
|
|
69
85
|
**context: typing.Any,
|
|
70
86
|
) -> None:
|
|
71
87
|
if hasher not in self.storage:
|
|
72
88
|
raise LookupError("No record of hasher {!r} found.".format(hasher))
|
|
73
89
|
|
|
74
|
-
waiter_id: typing.Hashable = hasher.get_hash_from_data(
|
|
75
|
-
RuntimeError("Couldn't create hash from data")
|
|
90
|
+
waiter_id: typing.Hashable = hasher.get_hash_from_data(data).expect(
|
|
91
|
+
RuntimeError("Couldn't create hash from data"),
|
|
76
92
|
)
|
|
77
93
|
short_state = self.storage[hasher].pop(waiter_id, None)
|
|
78
94
|
if short_state is None:
|
|
@@ -85,16 +101,17 @@ class WaiterMachine:
|
|
|
85
101
|
|
|
86
102
|
await short_state.cancel()
|
|
87
103
|
|
|
88
|
-
async def wait_from_event(
|
|
104
|
+
async def wait_from_event[Event: BaseCute](
|
|
89
105
|
self,
|
|
90
|
-
view: BaseStateView[
|
|
91
|
-
event:
|
|
106
|
+
view: BaseStateView[Event],
|
|
107
|
+
event: Event,
|
|
92
108
|
*,
|
|
93
109
|
filter: ABCRule | None = None,
|
|
94
110
|
release: ABCRule | None = None,
|
|
95
111
|
lifetime: datetime.timedelta | float | None = None,
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
lifespan: Lifespan | None = None,
|
|
113
|
+
**actions: typing.Unpack[WaiterActions[Event]],
|
|
114
|
+
) -> ShortStateContext[Event]:
|
|
98
115
|
hasher = StateViewHasher(view)
|
|
99
116
|
return await self.wait(
|
|
100
117
|
hasher=hasher,
|
|
@@ -104,24 +121,27 @@ class WaiterMachine:
|
|
|
104
121
|
filter=filter,
|
|
105
122
|
release=release,
|
|
106
123
|
lifetime=lifetime,
|
|
124
|
+
lifespan=lifespan or Lifespan(),
|
|
107
125
|
**actions,
|
|
108
126
|
)
|
|
109
127
|
|
|
110
|
-
async def wait(
|
|
128
|
+
async def wait[Event: BaseCute, HasherData](
|
|
111
129
|
self,
|
|
112
|
-
hasher: Hasher[
|
|
130
|
+
hasher: Hasher[Event, HasherData],
|
|
113
131
|
data: HasherData,
|
|
114
132
|
*,
|
|
115
133
|
filter: ABCRule | None = None,
|
|
116
134
|
release: ABCRule | None = None,
|
|
117
135
|
lifetime: datetime.timedelta | float | None = None,
|
|
118
|
-
|
|
119
|
-
|
|
136
|
+
lifespan: Lifespan | None = None,
|
|
137
|
+
**actions: typing.Unpack[WaiterActions[Event]],
|
|
138
|
+
) -> ShortStateContext[Event]:
|
|
120
139
|
if isinstance(lifetime, int | float):
|
|
121
140
|
lifetime = datetime.timedelta(seconds=lifetime)
|
|
122
141
|
|
|
142
|
+
lifespan = lifespan or Lifespan()
|
|
123
143
|
event = asyncio.Event()
|
|
124
|
-
short_state = ShortState[
|
|
144
|
+
short_state = ShortState[Event](
|
|
125
145
|
event,
|
|
126
146
|
actions,
|
|
127
147
|
release=release,
|
|
@@ -132,8 +152,8 @@ class WaiterMachine:
|
|
|
132
152
|
|
|
133
153
|
if hasher not in self.storage:
|
|
134
154
|
if self.dispatch:
|
|
135
|
-
view: BaseView[
|
|
136
|
-
RuntimeError(f"View {hasher.view_class.__name__!r} is not defined in dispatch.")
|
|
155
|
+
view: BaseView[Event] = self.dispatch.get_view(hasher.view_class).expect(
|
|
156
|
+
RuntimeError(f"View {hasher.view_class.__name__!r} is not defined in dispatch."),
|
|
137
157
|
)
|
|
138
158
|
view.middlewares.insert(0, WaiterMiddleware(self, hasher))
|
|
139
159
|
self.storage[hasher] = LimitedDict(maxlimit=self.max_storage_size)
|
|
@@ -141,23 +161,82 @@ class WaiterMachine:
|
|
|
141
161
|
if (deleted_short_state := self.storage[hasher].set(waiter_hash, short_state)) is not None:
|
|
142
162
|
await deleted_short_state.cancel()
|
|
143
163
|
|
|
144
|
-
|
|
164
|
+
async with lifespan:
|
|
165
|
+
await event.wait()
|
|
166
|
+
|
|
145
167
|
self.storage[hasher].pop(waiter_hash, None)
|
|
146
|
-
|
|
168
|
+
|
|
169
|
+
if short_state.context is None:
|
|
170
|
+
raise LookupError("No context in short_state.")
|
|
147
171
|
return short_state.context
|
|
148
172
|
|
|
173
|
+
async def wait_many[RestEvent: BaseCute[typing.Any], Data, *Ts](
|
|
174
|
+
self,
|
|
175
|
+
*hashers: HasherWithData[RestEvent, Data],
|
|
176
|
+
filter: ABCRule | None = None,
|
|
177
|
+
release: ABCRule | None = None,
|
|
178
|
+
lifetime: datetime.timedelta | float | None = None,
|
|
179
|
+
lifespan: Lifespan | None = None,
|
|
180
|
+
unpack: ContextUnpackProto[*Ts] = unpack_to_context,
|
|
181
|
+
**actions: typing.Unpack[WaiterActions[BaseCute[typing.Any]]],
|
|
182
|
+
) -> tuple[HasherWithData[RestEvent, Data], RestEvent, *Ts]:
|
|
183
|
+
if isinstance(lifetime, int | float):
|
|
184
|
+
lifetime = datetime.timedelta(seconds=lifetime)
|
|
185
|
+
|
|
186
|
+
lifespan = lifespan or Lifespan()
|
|
187
|
+
event = asyncio.Event()
|
|
188
|
+
short_state = ShortState(
|
|
189
|
+
event,
|
|
190
|
+
actions,
|
|
191
|
+
release=release,
|
|
192
|
+
filter=filter,
|
|
193
|
+
lifetime=lifetime or self.base_state_lifetime,
|
|
194
|
+
)
|
|
195
|
+
waiter_hashes: dict[Hasher[RestEvent, Data], typing.Hashable] = {}
|
|
196
|
+
|
|
197
|
+
for hasher, data in hashers:
|
|
198
|
+
waiter_hash = hasher.get_hash_from_data(data).expect(RuntimeError("Hasher couldn't create hash."))
|
|
199
|
+
|
|
200
|
+
if hasher not in self.storage:
|
|
201
|
+
if self.dispatch:
|
|
202
|
+
view = self.dispatch.get_view(hasher.view_class).expect(
|
|
203
|
+
RuntimeError(f"View {hasher.view_class.__name__!r} is not defined in dispatch."),
|
|
204
|
+
)
|
|
205
|
+
view.middlewares.insert(0, WaiterMiddleware(self, hasher))
|
|
206
|
+
self.storage[hasher] = LimitedDict(maxlimit=self.max_storage_size)
|
|
207
|
+
|
|
208
|
+
if (deleted_short_state := self.storage[hasher].set(waiter_hash, short_state)) is not None:
|
|
209
|
+
await deleted_short_state.cancel()
|
|
210
|
+
|
|
211
|
+
waiter_hashes[hasher] = waiter_hash
|
|
212
|
+
|
|
213
|
+
async with lifespan:
|
|
214
|
+
await event.wait()
|
|
215
|
+
|
|
216
|
+
if short_state.context is None:
|
|
217
|
+
raise LookupError("No context in short_state.")
|
|
218
|
+
|
|
219
|
+
initiator = short_state.context.context.get(INITIATOR_CONTEXT_KEY)
|
|
220
|
+
if initiator is None:
|
|
221
|
+
raise LookupError("Initiator not found in short_state context.")
|
|
222
|
+
|
|
223
|
+
for hasher, waiter_hash in waiter_hashes.items():
|
|
224
|
+
self.storage[hasher].pop(waiter_hash, None)
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
initiator,
|
|
228
|
+
short_state.context.event, # type: ignore
|
|
229
|
+
*unpack(short_state.context.context),
|
|
230
|
+
)
|
|
231
|
+
|
|
149
232
|
async def clear_storage(self) -> None:
|
|
150
233
|
"""Clears storage."""
|
|
234
|
+
now = datetime.datetime.now()
|
|
151
235
|
|
|
152
236
|
for hasher in self.storage:
|
|
153
|
-
now = datetime.datetime.now()
|
|
154
237
|
for ident, short_state in self.storage.get(hasher, {}).copy().items():
|
|
155
238
|
if short_state.expiration_date is not None and now > short_state.expiration_date:
|
|
156
|
-
await self.drop(
|
|
157
|
-
hasher,
|
|
158
|
-
ident,
|
|
159
|
-
force=True,
|
|
160
|
-
)
|
|
239
|
+
await self.drop(hasher, data=ident, force=True)
|
|
161
240
|
|
|
162
241
|
|
|
163
242
|
async def clear_wm_storage_worker(
|
|
@@ -15,10 +15,11 @@ if typing.TYPE_CHECKING:
|
|
|
15
15
|
from .machine import WaiterMachine
|
|
16
16
|
from .short_state import ShortState
|
|
17
17
|
|
|
18
|
-
EventType = typing.TypeVar("EventType", bound=BaseCute)
|
|
19
18
|
|
|
19
|
+
INITIATOR_CONTEXT_KEY = "initiator"
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
class WaiterMiddleware[Event: BaseCute](ABCMiddleware[Event]):
|
|
22
23
|
def __init__(
|
|
23
24
|
self,
|
|
24
25
|
machine: "WaiterMachine",
|
|
@@ -27,16 +28,16 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
|
|
|
27
28
|
self.machine = machine
|
|
28
29
|
self.hasher = hasher
|
|
29
30
|
|
|
30
|
-
async def pre(self, event:
|
|
31
|
+
async def pre(self, event: Event, ctx: Context) -> bool:
|
|
31
32
|
if self.hasher not in self.machine.storage:
|
|
32
33
|
return True
|
|
33
34
|
|
|
34
35
|
key = self.hasher.get_hash_from_data_from_event(event)
|
|
35
|
-
if key
|
|
36
|
-
logger.info(f"Unable to get hash from event with hasher {self.hasher}")
|
|
36
|
+
if not key:
|
|
37
|
+
logger.info(f"Unable to get hash from event with hasher {self.hasher!r}")
|
|
37
38
|
return True
|
|
38
39
|
|
|
39
|
-
short_state: "ShortState[
|
|
40
|
+
short_state: "ShortState[Event] | None" = self.machine.storage[self.hasher].get(key.unwrap())
|
|
40
41
|
if not short_state:
|
|
41
42
|
return True
|
|
42
43
|
|
|
@@ -46,7 +47,10 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
|
|
|
46
47
|
|
|
47
48
|
# Run filter rule
|
|
48
49
|
if short_state.filter and not await check_rule(
|
|
49
|
-
event.ctx_api,
|
|
50
|
+
event.ctx_api,
|
|
51
|
+
short_state.filter,
|
|
52
|
+
ctx.raw_update,
|
|
53
|
+
preset_context,
|
|
50
54
|
):
|
|
51
55
|
logger.debug("Filter rule {!r} failed", short_state.filter)
|
|
52
56
|
return True
|
|
@@ -62,9 +66,9 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
|
|
|
62
66
|
handler = FuncHandler(
|
|
63
67
|
self.pass_runtime,
|
|
64
68
|
[short_state.release] if short_state.release else [],
|
|
65
|
-
dataclass=None,
|
|
66
69
|
preset_context=preset_context,
|
|
67
70
|
)
|
|
71
|
+
handler.get_name_event_param = lambda event: "event" # FIXME: HOTFIX
|
|
68
72
|
result = await handler.check(event.ctx_api, ctx.raw_update, ctx)
|
|
69
73
|
|
|
70
74
|
if result is True:
|
|
@@ -78,10 +82,11 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
|
|
|
78
82
|
|
|
79
83
|
async def pass_runtime(
|
|
80
84
|
self,
|
|
81
|
-
event:
|
|
82
|
-
short_state: "ShortState[
|
|
85
|
+
event: Event,
|
|
86
|
+
short_state: "ShortState[Event]",
|
|
83
87
|
ctx: Context,
|
|
84
88
|
) -> None:
|
|
89
|
+
ctx.initiator = self.hasher
|
|
85
90
|
short_state.context = ShortStateContext(event, ctx)
|
|
86
91
|
short_state.event.set()
|
|
87
92
|
|
|
@@ -2,33 +2,25 @@ import asyncio
|
|
|
2
2
|
import dataclasses
|
|
3
3
|
import datetime
|
|
4
4
|
import typing
|
|
5
|
-
from contextlib import suppress
|
|
6
5
|
|
|
7
6
|
from telegrinder.bot.cute_types import BaseCute
|
|
8
7
|
from telegrinder.bot.dispatch.context import Context
|
|
9
|
-
from telegrinder.bot.dispatch.handler.abc import ABCHandler
|
|
10
8
|
from telegrinder.bot.rules.abc import ABCRule
|
|
11
|
-
from telegrinder.
|
|
9
|
+
from telegrinder.tools.magic import cancel_future
|
|
12
10
|
|
|
13
11
|
if typing.TYPE_CHECKING:
|
|
14
12
|
from .actions import WaiterActions
|
|
15
13
|
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Behaviour: typing.TypeAlias = ABCHandler[T] | None
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class ShortStateContext(typing.Generic[EventModel], typing.NamedTuple):
|
|
24
|
-
event: EventModel
|
|
15
|
+
class ShortStateContext[Event: BaseCute](typing.NamedTuple):
|
|
16
|
+
event: Event
|
|
25
17
|
context: Context
|
|
26
18
|
|
|
27
19
|
|
|
28
20
|
@dataclasses.dataclass(slots=True)
|
|
29
|
-
class ShortState
|
|
21
|
+
class ShortState[Event: BaseCute]:
|
|
30
22
|
event: asyncio.Event
|
|
31
|
-
actions: "WaiterActions[
|
|
23
|
+
actions: "WaiterActions[Event]"
|
|
32
24
|
|
|
33
25
|
release: ABCRule | None = dataclasses.field(
|
|
34
26
|
default=None,
|
|
@@ -46,7 +38,7 @@ class ShortState(typing.Generic[EventModel]):
|
|
|
46
38
|
|
|
47
39
|
expiration_date: datetime.datetime | None = dataclasses.field(init=False, kw_only=True)
|
|
48
40
|
creation_date: datetime.datetime = dataclasses.field(init=False)
|
|
49
|
-
context: ShortStateContext[
|
|
41
|
+
context: ShortStateContext[Event] | None = dataclasses.field(default=None, init=False, kw_only=True)
|
|
50
42
|
|
|
51
43
|
def __post_init__(self, expiration: datetime.timedelta | None = None) -> None:
|
|
52
44
|
self.creation_date = datetime.datetime.now()
|
|
@@ -54,15 +46,12 @@ class ShortState(typing.Generic[EventModel]):
|
|
|
54
46
|
|
|
55
47
|
async def cancel(self) -> None:
|
|
56
48
|
"""Cancel schedule waiters."""
|
|
57
|
-
|
|
58
49
|
waiters = typing.cast(
|
|
59
50
|
typing.Iterable[asyncio.Future[typing.Any]],
|
|
60
51
|
self.event._waiters, # type: ignore
|
|
61
52
|
)
|
|
62
53
|
for future in waiters:
|
|
63
|
-
future
|
|
64
|
-
with suppress(asyncio.CancelledError):
|
|
65
|
-
await future
|
|
54
|
+
await cancel_future(future)
|
|
66
55
|
|
|
67
56
|
|
|
68
57
|
__all__ = ("ShortState", "ShortStateContext")
|