telegrinder 0.3.0__py3-none-any.whl → 0.3.0.post2__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 +1 -1
- telegrinder/bot/cute_types/callback_query.py +2 -14
- telegrinder/bot/cute_types/chat_join_request.py +1 -1
- telegrinder/bot/cute_types/chat_member_updated.py +1 -1
- telegrinder/bot/cute_types/inline_query.py +1 -6
- telegrinder/bot/cute_types/message.py +1 -21
- telegrinder/bot/cute_types/update.py +1 -1
- telegrinder/bot/dispatch/abc.py +45 -3
- telegrinder/bot/dispatch/dispatch.py +8 -8
- telegrinder/bot/dispatch/handler/func.py +8 -10
- telegrinder/bot/dispatch/process.py +1 -1
- telegrinder/bot/dispatch/view/base.py +31 -20
- telegrinder/bot/dispatch/view/raw.py +20 -16
- telegrinder/bot/dispatch/waiter_machine/actions.py +3 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +15 -18
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +21 -13
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +15 -16
- telegrinder/bot/dispatch/waiter_machine/hasher/state.py +6 -6
- telegrinder/bot/dispatch/waiter_machine/machine.py +26 -32
- telegrinder/bot/dispatch/waiter_machine/short_state.py +10 -4
- telegrinder/bot/rules/abc.py +37 -7
- telegrinder/bot/rules/adapter/raw_update.py +1 -3
- telegrinder/bot/rules/inline.py +1 -2
- telegrinder/bot/rules/markup.py +5 -2
- telegrinder/bot/rules/start.py +1 -1
- telegrinder/bot/rules/text.py +5 -3
- telegrinder/bot/scenario/checkbox.py +1 -1
- telegrinder/msgspec_utils.py +11 -3
- telegrinder/node/attachment.py +6 -6
- telegrinder/node/base.py +17 -11
- telegrinder/node/callback_query.py +1 -1
- telegrinder/node/command.py +1 -1
- telegrinder/node/composer.py +5 -2
- telegrinder/node/container.py +1 -1
- telegrinder/node/event.py +1 -1
- telegrinder/node/message.py +1 -1
- telegrinder/node/polymorphic.py +6 -3
- telegrinder/node/rule.py +1 -1
- telegrinder/node/source.py +5 -7
- telegrinder/node/text.py +2 -2
- telegrinder/node/tools/generator.py +1 -2
- telegrinder/node/update.py +3 -3
- telegrinder/rules.py +2 -0
- telegrinder/tools/buttons.py +4 -4
- telegrinder/tools/error_handler/abc.py +7 -7
- telegrinder/tools/error_handler/error_handler.py +58 -47
- telegrinder/tools/formatting/html.py +0 -2
- telegrinder/tools/functional.py +3 -0
- telegrinder/tools/global_context/telegrinder_ctx.py +2 -0
- telegrinder/tools/i18n/__init__.py +1 -1
- telegrinder/tools/i18n/{base.py → abc.py} +0 -0
- telegrinder/tools/i18n/middleware/__init__.py +1 -1
- telegrinder/tools/i18n/middleware/{base.py → abc.py} +3 -2
- telegrinder/tools/i18n/simple.py +11 -12
- telegrinder/tools/keyboard.py +9 -9
- {telegrinder-0.3.0.dist-info → telegrinder-0.3.0.post2.dist-info}/METADATA +1 -1
- {telegrinder-0.3.0.dist-info → telegrinder-0.3.0.post2.dist-info}/RECORD +59 -59
- {telegrinder-0.3.0.dist-info → telegrinder-0.3.0.post2.dist-info}/LICENSE +0 -0
- {telegrinder-0.3.0.dist-info → telegrinder-0.3.0.post2.dist-info}/WHEEL +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from telegrinder.bot.cute_types import MessageCute as Message
|
|
2
2
|
from telegrinder.bot.dispatch.view import MessageView
|
|
3
|
-
|
|
4
|
-
from .hasher import Hasher
|
|
3
|
+
from telegrinder.bot.dispatch.waiter_machine.hasher.hasher import Hasher
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
def from_chat_hash(chat_id: int) -> int:
|
|
@@ -12,9 +11,12 @@ def get_chat_from_event(event: Message) -> int:
|
|
|
12
11
|
return event.chat.id
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
def from_user_in_chat_hash(chat_and_user: tuple[int, int]) -> str:
|
|
15
|
+
return f"{chat_and_user[0]}_{chat_and_user[1]}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_user_in_chat_from_event(event: Message) -> tuple[int, int]:
|
|
19
|
+
return event.chat.id, event.from_user.id
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
def from_user_hash(from_id: int) -> int:
|
|
@@ -25,23 +27,20 @@ def get_user_from_event(event: Message) -> int:
|
|
|
25
27
|
return event.from_user.id
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
MESSAGE_IN_CHAT = Hasher(
|
|
31
|
+
view_class=MessageView,
|
|
32
|
+
get_hash_from_data=from_chat_hash,
|
|
33
|
+
get_data_from_event=get_chat_from_event,
|
|
34
|
+
)
|
|
35
|
+
|
|
28
36
|
MESSAGE_FROM_USER = Hasher(
|
|
29
|
-
|
|
37
|
+
view_class=MessageView,
|
|
30
38
|
get_hash_from_data=from_user_hash,
|
|
31
39
|
get_data_from_event=get_user_from_event,
|
|
32
40
|
)
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
def from_user_in_chat_hash(chat_and_user: tuple[int, int]) -> str:
|
|
36
|
-
return f"{chat_and_user[0]}_{chat_and_user[1]}"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def get_user_in_chat_from_event(event: Message) -> tuple[int, int]:
|
|
40
|
-
return event.chat.id, event.from_user.id
|
|
41
|
-
|
|
42
|
-
|
|
43
42
|
MESSAGE_FROM_USER_IN_CHAT = Hasher(
|
|
44
|
-
|
|
43
|
+
view_class=MessageView,
|
|
45
44
|
get_hash_from_data=from_user_in_chat_hash,
|
|
46
45
|
get_data_from_event=get_user_in_chat_from_event,
|
|
47
46
|
)
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
from fntypes import Option
|
|
1
|
+
from fntypes.option import Option
|
|
2
2
|
|
|
3
3
|
from telegrinder.bot.dispatch.view import BaseStateView
|
|
4
|
+
from telegrinder.bot.dispatch.waiter_machine.hasher.hasher import ECHO, Event, Hasher
|
|
4
5
|
from telegrinder.tools.functional import from_optional
|
|
5
6
|
|
|
6
|
-
from .hasher import ECHO, Event, Hasher
|
|
7
|
-
|
|
8
7
|
|
|
9
8
|
class StateViewHasher(Hasher[Event, int]):
|
|
10
|
-
view: BaseStateView
|
|
9
|
+
view: BaseStateView[Event]
|
|
11
10
|
|
|
12
|
-
def __init__(self, view:
|
|
13
|
-
|
|
11
|
+
def __init__(self, view: BaseStateView[Event]) -> None:
|
|
12
|
+
self.view = view
|
|
13
|
+
super().__init__(view.__class__, get_hash_from_data=ECHO)
|
|
14
14
|
|
|
15
15
|
def get_data_from_event(self, event: Event) -> Option[int]:
|
|
16
16
|
return from_optional(self.view.get_state_key(event))
|
|
@@ -41,16 +41,25 @@ class WaiterMachine:
|
|
|
41
41
|
self.storage: Storage = {}
|
|
42
42
|
|
|
43
43
|
def __repr__(self) -> str:
|
|
44
|
-
return "<{}: max_storage_size={}, {}>".format(
|
|
44
|
+
return "<{}: max_storage_size={}, base_state_lifetime={!r}>".format(
|
|
45
45
|
self.__class__.__name__,
|
|
46
46
|
self.max_storage_size,
|
|
47
|
-
|
|
48
|
-
f"{view_name}: {len(self.storage[view_name].values())} shortstates"
|
|
49
|
-
for view_name in self.storage
|
|
50
|
-
)
|
|
51
|
-
or "empty",
|
|
47
|
+
self.base_state_lifetime,
|
|
52
48
|
)
|
|
53
49
|
|
|
50
|
+
async def drop_all(self) -> None:
|
|
51
|
+
"""Drops all waiters in storage."""
|
|
52
|
+
|
|
53
|
+
for hasher in self.storage:
|
|
54
|
+
for ident, short_state in self.storage[hasher].items():
|
|
55
|
+
if short_state.context:
|
|
56
|
+
await self.drop(
|
|
57
|
+
hasher,
|
|
58
|
+
ident,
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
short_state.cancel()
|
|
62
|
+
|
|
54
63
|
async def drop(
|
|
55
64
|
self,
|
|
56
65
|
hasher: Hasher[EventModel, HasherData],
|
|
@@ -66,7 +75,7 @@ class WaiterMachine:
|
|
|
66
75
|
short_state = self.storage[hasher].pop(waiter_id, None)
|
|
67
76
|
if short_state is None:
|
|
68
77
|
raise LookupError(
|
|
69
|
-
"Waiter with identificator {} is not found for hasher {!r}".format(waiter_id, hasher)
|
|
78
|
+
"Waiter with identificator {} is not found for hasher {!r}.".format(waiter_id, hasher)
|
|
70
79
|
)
|
|
71
80
|
|
|
72
81
|
if on_drop := short_state.actions.get("on_drop"):
|
|
@@ -74,19 +83,6 @@ class WaiterMachine:
|
|
|
74
83
|
|
|
75
84
|
short_state.cancel()
|
|
76
85
|
|
|
77
|
-
async def drop_all(self) -> None:
|
|
78
|
-
"""Drops all waiters in storage."""
|
|
79
|
-
|
|
80
|
-
for hasher in self.storage:
|
|
81
|
-
for ident, short_state in self.storage[hasher].items():
|
|
82
|
-
if short_state.context:
|
|
83
|
-
await self.drop(
|
|
84
|
-
hasher,
|
|
85
|
-
ident,
|
|
86
|
-
)
|
|
87
|
-
else:
|
|
88
|
-
short_state.cancel()
|
|
89
|
-
|
|
90
86
|
async def wait_from_event(
|
|
91
87
|
self,
|
|
92
88
|
view: BaseStateView[EventModel],
|
|
@@ -95,13 +91,13 @@ class WaiterMachine:
|
|
|
95
91
|
filter: ABCRule | None = None,
|
|
96
92
|
release: ABCRule | None = None,
|
|
97
93
|
lifetime: datetime.timedelta | float | None = None,
|
|
98
|
-
**actions: typing.Unpack[WaiterActions],
|
|
94
|
+
**actions: typing.Unpack[WaiterActions[EventModel]],
|
|
99
95
|
) -> ShortStateContext[EventModel]:
|
|
100
|
-
hasher = StateViewHasher(view
|
|
96
|
+
hasher = StateViewHasher(view)
|
|
101
97
|
return await self.wait(
|
|
102
98
|
hasher=hasher,
|
|
103
99
|
data=hasher.get_data_from_event(event).expect(
|
|
104
|
-
RuntimeError("Hasher couldn't create data from event.")
|
|
100
|
+
RuntimeError("Hasher couldn't create data from event."),
|
|
105
101
|
),
|
|
106
102
|
filter=filter,
|
|
107
103
|
release=release,
|
|
@@ -117,25 +113,25 @@ class WaiterMachine:
|
|
|
117
113
|
filter: ABCRule | None = None,
|
|
118
114
|
release: ABCRule | None = None,
|
|
119
115
|
lifetime: datetime.timedelta | float | None = None,
|
|
120
|
-
**actions: typing.Unpack[WaiterActions],
|
|
116
|
+
**actions: typing.Unpack[WaiterActions[EventModel]],
|
|
121
117
|
) -> ShortStateContext[EventModel]:
|
|
122
118
|
if isinstance(lifetime, int | float):
|
|
123
119
|
lifetime = datetime.timedelta(seconds=lifetime)
|
|
124
120
|
|
|
125
121
|
event = asyncio.Event()
|
|
126
122
|
short_state = ShortState[EventModel](
|
|
127
|
-
|
|
123
|
+
event,
|
|
124
|
+
actions,
|
|
128
125
|
release=release,
|
|
129
|
-
|
|
126
|
+
filter=filter,
|
|
130
127
|
lifetime=lifetime or self.base_state_lifetime,
|
|
131
|
-
actions=actions,
|
|
132
128
|
)
|
|
133
129
|
waiter_hash = hasher.get_hash_from_data(data).expect(RuntimeError("Hasher couldn't create hash."))
|
|
134
130
|
|
|
135
131
|
if hasher not in self.storage:
|
|
136
132
|
if self.dispatch:
|
|
137
|
-
view: BaseView[EventModel] = self.dispatch.get_view(hasher.
|
|
138
|
-
RuntimeError(f"View {hasher.
|
|
133
|
+
view: BaseView[EventModel] = self.dispatch.get_view(hasher.view_class).expect(
|
|
134
|
+
RuntimeError(f"View {hasher.view_class.__name__!r} is not defined in dispatch.")
|
|
139
135
|
)
|
|
140
136
|
view.middlewares.insert(0, WaiterMiddleware(self, hasher))
|
|
141
137
|
self.storage[hasher] = LimitedDict(maxlimit=self.max_storage_size)
|
|
@@ -148,9 +144,7 @@ class WaiterMachine:
|
|
|
148
144
|
assert short_state.context is not None
|
|
149
145
|
return short_state.context
|
|
150
146
|
|
|
151
|
-
async def clear_storage(
|
|
152
|
-
self,
|
|
153
|
-
) -> None:
|
|
147
|
+
async def clear_storage(self) -> None:
|
|
154
148
|
"""Clears storage."""
|
|
155
149
|
|
|
156
150
|
for hasher in self.storage:
|
|
@@ -27,10 +27,17 @@ class ShortStateContext(typing.Generic[EventModel], typing.NamedTuple):
|
|
|
27
27
|
@dataclasses.dataclass(slots=True)
|
|
28
28
|
class ShortState(typing.Generic[EventModel]):
|
|
29
29
|
event: asyncio.Event
|
|
30
|
-
actions: "WaiterActions"
|
|
30
|
+
actions: "WaiterActions[EventModel]"
|
|
31
|
+
|
|
32
|
+
release: ABCRule | None = dataclasses.field(
|
|
33
|
+
default=None,
|
|
34
|
+
kw_only=True,
|
|
35
|
+
)
|
|
36
|
+
filter: ABCRule | None = dataclasses.field(
|
|
37
|
+
default=None,
|
|
38
|
+
kw_only=True,
|
|
39
|
+
)
|
|
31
40
|
|
|
32
|
-
release: ABCRule | None = None
|
|
33
|
-
filter: ABCRule | None = None
|
|
34
41
|
lifetime: dataclasses.InitVar[datetime.timedelta | None] = dataclasses.field(
|
|
35
42
|
default=None,
|
|
36
43
|
kw_only=True,
|
|
@@ -38,7 +45,6 @@ class ShortState(typing.Generic[EventModel]):
|
|
|
38
45
|
|
|
39
46
|
expiration_date: datetime.datetime | None = dataclasses.field(init=False, kw_only=True)
|
|
40
47
|
creation_date: datetime.datetime = dataclasses.field(init=False)
|
|
41
|
-
|
|
42
48
|
context: ShortStateContext[EventModel] | None = dataclasses.field(default=None, init=False, kw_only=True)
|
|
43
49
|
|
|
44
50
|
def __post_init__(self, expiration: datetime.timedelta | None = None) -> None:
|
telegrinder/bot/rules/abc.py
CHANGED
|
@@ -10,7 +10,7 @@ from telegrinder.bot.dispatch.process import check_rule
|
|
|
10
10
|
from telegrinder.bot.rules.adapter import ABCAdapter, RawUpdateAdapter
|
|
11
11
|
from telegrinder.bot.rules.adapter.node import Event
|
|
12
12
|
from telegrinder.node.base import Node, get_nodes, is_node
|
|
13
|
-
from telegrinder.tools.i18n.
|
|
13
|
+
from telegrinder.tools.i18n.abc import ABCTranslator
|
|
14
14
|
from telegrinder.tools.magic import (
|
|
15
15
|
cache_translation,
|
|
16
16
|
get_annotations,
|
|
@@ -22,7 +22,7 @@ from telegrinder.types.objects import Update as UpdateObject
|
|
|
22
22
|
if typing.TYPE_CHECKING:
|
|
23
23
|
from telegrinder.node.composer import NodeCollection
|
|
24
24
|
|
|
25
|
-
AdaptTo = typing.TypeVar("AdaptTo", default=typing.Any)
|
|
25
|
+
AdaptTo = typing.TypeVar("AdaptTo", default=typing.Any, contravariant=True)
|
|
26
26
|
|
|
27
27
|
Message: typing.TypeAlias = MessageCute
|
|
28
28
|
Update: typing.TypeAlias = UpdateCute
|
|
@@ -45,12 +45,42 @@ class ABCRule(ABC, typing.Generic[AdaptTo]):
|
|
|
45
45
|
adapter: ABCAdapter[UpdateObject, AdaptTo]
|
|
46
46
|
requires: list["ABCRule"] = []
|
|
47
47
|
|
|
48
|
-
if
|
|
48
|
+
if typing.TYPE_CHECKING:
|
|
49
|
+
|
|
50
|
+
@typing.overload
|
|
51
|
+
async def check(self) -> bool: ...
|
|
52
|
+
|
|
53
|
+
@typing.overload
|
|
54
|
+
async def check(self, event: AdaptTo, /) -> bool: ...
|
|
55
|
+
|
|
56
|
+
@typing.overload
|
|
57
|
+
async def check(self, event: AdaptTo, ctx: Context, /) -> bool: ...
|
|
58
|
+
|
|
59
|
+
@typing.overload
|
|
60
|
+
async def check(
|
|
61
|
+
self,
|
|
62
|
+
event: AdaptTo,
|
|
63
|
+
ctx: Context,
|
|
64
|
+
/,
|
|
65
|
+
*args: typing.Any,
|
|
66
|
+
**kwargs: typing.Any,
|
|
67
|
+
) -> bool: ...
|
|
68
|
+
|
|
69
|
+
@typing.overload
|
|
70
|
+
async def check(self, event: AdaptTo, /, *args: typing.Any, **kwargs: typing.Any) -> bool: ...
|
|
71
|
+
|
|
72
|
+
@typing.overload
|
|
73
|
+
async def check(self, ctx: Context, /, *args: typing.Any, **kwargs: typing.Any) -> bool: ...
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
async def check(self, *args: typing.Any, **kwargs: typing.Any) -> bool:
|
|
77
|
+
pass
|
|
78
|
+
else:
|
|
49
79
|
adapter = RawUpdateAdapter()
|
|
50
80
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
81
|
+
@abstractmethod
|
|
82
|
+
async def check(self, *args, **kwargs):
|
|
83
|
+
pass
|
|
54
84
|
|
|
55
85
|
def __init_subclass__(cls, requires: list["ABCRule"] | None = None) -> None:
|
|
56
86
|
"""Merges requirements from inherited classes and rule-specific requirements."""
|
|
@@ -141,7 +171,7 @@ class ABCRule(ABC, typing.Generic[AdaptTo]):
|
|
|
141
171
|
"because it cannot be resolved."
|
|
142
172
|
)
|
|
143
173
|
|
|
144
|
-
return await bound_check_rule(**kw)
|
|
174
|
+
return await bound_check_rule(**kw) # type: ignore
|
|
145
175
|
|
|
146
176
|
async def translate(self, translator: ABCTranslator) -> typing.Self:
|
|
147
177
|
return self
|
|
@@ -22,9 +22,7 @@ class RawUpdateAdapter(ABCAdapter[Update, UpdateCute]):
|
|
|
22
22
|
) -> Result[UpdateCute, AdapterError]:
|
|
23
23
|
if self.ADAPTED_VALUE_KEY not in context:
|
|
24
24
|
context[self.ADAPTED_VALUE_KEY] = (
|
|
25
|
-
UpdateCute.from_update(update, api)
|
|
26
|
-
if not isinstance(update, UpdateCute)
|
|
27
|
-
else update
|
|
25
|
+
UpdateCute.from_update(update, api) if not isinstance(update, UpdateCute) else update
|
|
28
26
|
)
|
|
29
27
|
return Ok(context[self.ADAPTED_VALUE_KEY])
|
|
30
28
|
|
telegrinder/bot/rules/inline.py
CHANGED
|
@@ -16,8 +16,7 @@ class InlineQueryRule(ABCRule[InlineQuery], abc.ABC):
|
|
|
16
16
|
adapter: EventAdapter[InlineQuery] = EventAdapter(UpdateType.INLINE_QUERY, InlineQuery)
|
|
17
17
|
|
|
18
18
|
@abc.abstractmethod
|
|
19
|
-
async def check(self, query: InlineQuery, ctx: Context) -> bool:
|
|
20
|
-
...
|
|
19
|
+
async def check(self, query: InlineQuery, ctx: Context) -> bool: ...
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
class HasLocation(InlineQueryRule):
|
telegrinder/bot/rules/markup.py
CHANGED
|
@@ -24,13 +24,16 @@ def check_string(patterns: list[vbml.Pattern], s: str, ctx: Context) -> bool:
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class Markup(ABCRule):
|
|
27
|
-
"""Markup Language. See [
|
|
27
|
+
"""Markup Language. See the [vbml documentation](https://github.com/tesseradecade/vbml/blob/master/docs/index.md)."""
|
|
28
28
|
|
|
29
29
|
def __init__(self, patterns: PatternLike | list[PatternLike], /) -> None:
|
|
30
30
|
if not isinstance(patterns, list):
|
|
31
31
|
patterns = [patterns]
|
|
32
32
|
self.patterns = [
|
|
33
|
-
vbml.Pattern(pattern
|
|
33
|
+
vbml.Pattern(pattern, flags=global_ctx.vbml_pattern_flags)
|
|
34
|
+
if isinstance(pattern, str)
|
|
35
|
+
else pattern
|
|
36
|
+
for pattern in patterns
|
|
34
37
|
]
|
|
35
38
|
|
|
36
39
|
async def check(self, text: Text, ctx: Context) -> bool:
|
telegrinder/bot/rules/start.py
CHANGED
telegrinder/bot/rules/text.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
1
3
|
from telegrinder import node
|
|
2
|
-
from telegrinder.tools.i18n.
|
|
4
|
+
from telegrinder.tools.i18n.abc import ABCTranslator
|
|
3
5
|
|
|
4
6
|
from .abc import ABCRule, with_caching_translations
|
|
5
7
|
from .node import NodeRule
|
|
@@ -21,8 +23,8 @@ class Text(ABCRule):
|
|
|
21
23
|
return (text if not self.ignore_case else text.lower()) in self.texts
|
|
22
24
|
|
|
23
25
|
@with_caching_translations
|
|
24
|
-
async def translate(self, translator: ABCTranslator) ->
|
|
25
|
-
return
|
|
26
|
+
async def translate(self, translator: ABCTranslator) -> typing.Self:
|
|
27
|
+
return self.__class__(
|
|
26
28
|
texts=[translator.get(text) for text in self.texts],
|
|
27
29
|
ignore_case=self.ignore_case,
|
|
28
30
|
)
|
|
@@ -124,7 +124,7 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
|
|
|
124
124
|
).unwrap()
|
|
125
125
|
|
|
126
126
|
while True:
|
|
127
|
-
q, _ = await self.waiter_machine.wait(StateViewHasher(view
|
|
127
|
+
q, _ = await self.waiter_machine.wait(StateViewHasher(view), message.message_id)
|
|
128
128
|
should_continue = await self.handle(q)
|
|
129
129
|
await q.answer(self.CALLBACK_ANSWER)
|
|
130
130
|
if not should_continue:
|
telegrinder/msgspec_utils.py
CHANGED
|
@@ -210,12 +210,18 @@ class Decoder:
|
|
|
210
210
|
|
|
211
211
|
@typing.overload
|
|
212
212
|
def __call__(
|
|
213
|
-
self,
|
|
213
|
+
self,
|
|
214
|
+
type: type[T],
|
|
215
|
+
*,
|
|
216
|
+
strict: bool = True,
|
|
214
217
|
) -> typing.ContextManager[msgspec.json.Decoder[T]]: ...
|
|
215
218
|
|
|
216
219
|
@typing.overload
|
|
217
220
|
def __call__(
|
|
218
|
-
self,
|
|
221
|
+
self,
|
|
222
|
+
type: typing.Any,
|
|
223
|
+
*,
|
|
224
|
+
strict: bool = True,
|
|
219
225
|
) -> typing.ContextManager[msgspec.json.Decoder[typing.Any]]: ...
|
|
220
226
|
|
|
221
227
|
@contextmanager
|
|
@@ -223,7 +229,9 @@ class Decoder:
|
|
|
223
229
|
"""Context manager returns the `msgspec.json.Decoder` object with the `dec_hook`."""
|
|
224
230
|
|
|
225
231
|
dec_obj = msgspec.json.Decoder(
|
|
226
|
-
type=typing.Any if type is object else type,
|
|
232
|
+
type=typing.Any if type is object else type,
|
|
233
|
+
strict=strict,
|
|
234
|
+
dec_hook=self.dec_hook,
|
|
227
235
|
)
|
|
228
236
|
yield dec_obj
|
|
229
237
|
|
telegrinder/node/attachment.py
CHANGED
|
@@ -31,7 +31,7 @@ class Attachment(DataNode):
|
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
@classmethod
|
|
34
|
-
|
|
34
|
+
def compose(cls, message: MessageNode) -> "Attachment":
|
|
35
35
|
for attachment_type in ("audio", "document", "photo", "poll", "video"):
|
|
36
36
|
match getattr(message, attachment_type, Nothing()):
|
|
37
37
|
case Some(attachment):
|
|
@@ -44,7 +44,7 @@ class Photo(DataNode):
|
|
|
44
44
|
sizes: list[telegrinder.types.PhotoSize]
|
|
45
45
|
|
|
46
46
|
@classmethod
|
|
47
|
-
|
|
47
|
+
def compose(cls, attachment: Attachment) -> typing.Self:
|
|
48
48
|
if not attachment.photo:
|
|
49
49
|
raise ComposeError("Attachment is not a photo.")
|
|
50
50
|
return cls(attachment.photo.unwrap())
|
|
@@ -52,7 +52,7 @@ class Photo(DataNode):
|
|
|
52
52
|
|
|
53
53
|
class Video(ScalarNode, telegrinder.types.Video):
|
|
54
54
|
@classmethod
|
|
55
|
-
|
|
55
|
+
def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
|
|
56
56
|
if not attachment.video:
|
|
57
57
|
raise ComposeError("Attachment is not a video.")
|
|
58
58
|
return attachment.video.unwrap()
|
|
@@ -60,7 +60,7 @@ class Video(ScalarNode, telegrinder.types.Video):
|
|
|
60
60
|
|
|
61
61
|
class Audio(ScalarNode, telegrinder.types.Audio):
|
|
62
62
|
@classmethod
|
|
63
|
-
|
|
63
|
+
def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
|
|
64
64
|
if not attachment.audio:
|
|
65
65
|
raise ComposeError("Attachment is not an audio.")
|
|
66
66
|
return attachment.audio.unwrap()
|
|
@@ -68,7 +68,7 @@ class Audio(ScalarNode, telegrinder.types.Audio):
|
|
|
68
68
|
|
|
69
69
|
class Document(ScalarNode, telegrinder.types.Document):
|
|
70
70
|
@classmethod
|
|
71
|
-
|
|
71
|
+
def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
|
|
72
72
|
if not attachment.document:
|
|
73
73
|
raise ComposeError("Attachment is not a document.")
|
|
74
74
|
return attachment.document.unwrap()
|
|
@@ -76,7 +76,7 @@ class Document(ScalarNode, telegrinder.types.Document):
|
|
|
76
76
|
|
|
77
77
|
class Poll(ScalarNode, telegrinder.types.Poll):
|
|
78
78
|
@classmethod
|
|
79
|
-
|
|
79
|
+
def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
|
|
80
80
|
if not attachment.poll:
|
|
81
81
|
raise ComposeError("Attachment is not a poll.")
|
|
82
82
|
return attachment.poll.unwrap()
|
telegrinder/node/base.py
CHANGED
|
@@ -4,12 +4,11 @@ import typing
|
|
|
4
4
|
from types import AsyncGeneratorType
|
|
5
5
|
|
|
6
6
|
from telegrinder.node.scope import NodeScope
|
|
7
|
-
from telegrinder.tools.magic import
|
|
8
|
-
cache_magic_value,
|
|
9
|
-
get_annotations,
|
|
10
|
-
)
|
|
7
|
+
from telegrinder.tools.magic import cache_magic_value, get_annotations
|
|
11
8
|
|
|
12
|
-
ComposeResult: typing.TypeAlias =
|
|
9
|
+
ComposeResult: typing.TypeAlias = (
|
|
10
|
+
typing.Awaitable[typing.Any] | typing.AsyncGenerator[typing.Any, None] | typing.Any
|
|
11
|
+
)
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
def is_node(maybe_node: typing.Any) -> typing.TypeGuard[type["Node"]]:
|
|
@@ -28,7 +27,9 @@ def get_nodes(function: typing.Callable[..., typing.Any]) -> dict[str, type["Nod
|
|
|
28
27
|
|
|
29
28
|
|
|
30
29
|
@cache_magic_value("__is_generator__")
|
|
31
|
-
def is_generator(
|
|
30
|
+
def is_generator(
|
|
31
|
+
function: typing.Callable[..., typing.Any],
|
|
32
|
+
) -> typing.TypeGuard[AsyncGeneratorType[typing.Any, None]]:
|
|
32
33
|
return inspect.isasyncgenfunction(function)
|
|
33
34
|
|
|
34
35
|
|
|
@@ -103,20 +104,25 @@ if typing.TYPE_CHECKING:
|
|
|
103
104
|
pass
|
|
104
105
|
|
|
105
106
|
else:
|
|
107
|
+
|
|
106
108
|
def __init_subclass__(cls, *args, **kwargs): # noqa: N807
|
|
107
|
-
if any(issubclass(base,
|
|
109
|
+
if any(issubclass(base, ScalarNodeProto) for base in cls.__bases__ if base is not ScalarNode):
|
|
108
110
|
raise RuntimeError("Scalar nodes do not support inheritance.")
|
|
109
111
|
|
|
110
|
-
def
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
def _as_node(cls, bases, dct):
|
|
113
|
+
if not hasattr(cls, "_scalar_node_type"):
|
|
114
|
+
dct.update(cls.__dict__)
|
|
115
|
+
scalar_node_type = type(cls.__name__, bases, dct)
|
|
116
|
+
setattr(cls, "_scalar_node_type", scalar_node_type)
|
|
117
|
+
return scalar_node_type
|
|
118
|
+
return getattr(cls, "_scalar_node_type")
|
|
113
119
|
|
|
114
120
|
def create_class(name, bases, dct):
|
|
115
121
|
return type(
|
|
116
122
|
"Scalar",
|
|
117
123
|
(SCALAR_NODE,),
|
|
118
124
|
{
|
|
119
|
-
"as_node": classmethod(lambda cls:
|
|
125
|
+
"as_node": classmethod(lambda cls: _as_node(cls, bases, dct)),
|
|
120
126
|
"scope": Node.scope,
|
|
121
127
|
"__init_subclass__": __init_subclass__,
|
|
122
128
|
},
|
|
@@ -5,7 +5,7 @@ from telegrinder.node.update import UpdateNode
|
|
|
5
5
|
|
|
6
6
|
class CallbackQueryNode(ScalarNode, CallbackQueryCute):
|
|
7
7
|
@classmethod
|
|
8
|
-
|
|
8
|
+
def compose(cls, update: UpdateNode) -> CallbackQueryCute:
|
|
9
9
|
if not update.callback_query:
|
|
10
10
|
raise ComposeError("Update is not a callback_query.")
|
|
11
11
|
return update.callback_query.unwrap()
|
telegrinder/node/command.py
CHANGED
|
@@ -24,7 +24,7 @@ class CommandInfo(DataNode):
|
|
|
24
24
|
mention: Option[str] = field(default_factory=Nothing)
|
|
25
25
|
|
|
26
26
|
@classmethod
|
|
27
|
-
|
|
27
|
+
def compose(cls, text: Text) -> typing.Self:
|
|
28
28
|
name, arguments = single_split(text, separator=" ")
|
|
29
29
|
name, mention = cut_mention(name)
|
|
30
30
|
return cls(name, arguments, mention)
|
telegrinder/node/composer.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import dataclasses
|
|
2
|
+
import inspect
|
|
2
3
|
import typing
|
|
3
4
|
|
|
4
|
-
from fntypes import Error, Ok, Result
|
|
5
5
|
from fntypes.error import UnwrapError
|
|
6
|
+
from fntypes.result import Error, Ok, Result
|
|
6
7
|
|
|
7
8
|
from telegrinder.api.api import API
|
|
8
9
|
from telegrinder.bot.cute_types.update import Update, UpdateCute
|
|
@@ -33,7 +34,9 @@ async def compose_node(
|
|
|
33
34
|
value = await generator.asend(None)
|
|
34
35
|
else:
|
|
35
36
|
generator = None
|
|
36
|
-
value =
|
|
37
|
+
value = typing.cast(typing.Awaitable[typing.Any] | typing.Any, node.compose(**kwargs))
|
|
38
|
+
if inspect.isawaitable(value):
|
|
39
|
+
value = await value
|
|
37
40
|
|
|
38
41
|
return NodeSession(_node, value, {}, generator)
|
|
39
42
|
|
telegrinder/node/container.py
CHANGED
|
@@ -7,7 +7,7 @@ class ContainerNode(Node):
|
|
|
7
7
|
linked_nodes: typing.ClassVar[list[type[Node]]]
|
|
8
8
|
|
|
9
9
|
@classmethod
|
|
10
|
-
|
|
10
|
+
def compose(cls, **kw) -> tuple[Node, ...]:
|
|
11
11
|
return tuple(t[1] for t in sorted(kw.items(), key=lambda t: t[0]))
|
|
12
12
|
|
|
13
13
|
@classmethod
|
telegrinder/node/event.py
CHANGED
|
@@ -31,7 +31,7 @@ class _EventNode(Node):
|
|
|
31
31
|
return cls(dataclass)
|
|
32
32
|
|
|
33
33
|
@classmethod
|
|
34
|
-
|
|
34
|
+
def compose(cls, raw_update: UpdateNode, ctx: Context, api: API) -> "DataclassType":
|
|
35
35
|
dataclass_type = typing.get_origin(cls.dataclass) or cls.dataclass
|
|
36
36
|
|
|
37
37
|
try:
|
telegrinder/node/message.py
CHANGED
|
@@ -5,7 +5,7 @@ from telegrinder.node.update import UpdateNode
|
|
|
5
5
|
|
|
6
6
|
class MessageNode(ScalarNode, MessageCute):
|
|
7
7
|
@classmethod
|
|
8
|
-
|
|
8
|
+
def compose(cls, update: UpdateNode) -> MessageCute:
|
|
9
9
|
if not update.message:
|
|
10
10
|
raise ComposeError("Update is not a message.")
|
|
11
11
|
return update.message.unwrap()
|
telegrinder/node/polymorphic.py
CHANGED
|
@@ -13,7 +13,7 @@ from telegrinder.tools.magic import get_impls, impl
|
|
|
13
13
|
class Polymorphic(Node):
|
|
14
14
|
@classmethod
|
|
15
15
|
async def compose(cls, update: UpdateNode, context: Context) -> typing.Any:
|
|
16
|
-
logger.debug(f"Composing
|
|
16
|
+
logger.debug(f"Composing polymorphic node {cls.__name__!r}...")
|
|
17
17
|
scope = getattr(cls, "scope", None)
|
|
18
18
|
node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
19
19
|
|
|
@@ -27,7 +27,10 @@ class Polymorphic(Node):
|
|
|
27
27
|
|
|
28
28
|
# To determine whether this is a right morph, all subnodes must be resolved
|
|
29
29
|
if scope is NodeScope.PER_EVENT and (cls, i) in node_ctx:
|
|
30
|
-
logger.debug(
|
|
30
|
+
logger.debug(
|
|
31
|
+
"Morph is already cached as per_event node, using its value. Impl {!r} succeeded!",
|
|
32
|
+
impl_.__name__,
|
|
33
|
+
)
|
|
31
34
|
res: NodeSession = node_ctx[(cls, i)]
|
|
32
35
|
await node_collection.close_all()
|
|
33
36
|
return res.value
|
|
@@ -40,7 +43,7 @@ class Polymorphic(Node):
|
|
|
40
43
|
node_ctx[(cls, i)] = NodeSession(cls, result, {})
|
|
41
44
|
|
|
42
45
|
await node_collection.close_all(with_value=result)
|
|
43
|
-
logger.debug("Impl {!r} succeeded with value: {}", impl_.__name__, result)
|
|
46
|
+
logger.debug("Impl {!r} succeeded with value: {!r}", impl_.__name__, result)
|
|
44
47
|
return result
|
|
45
48
|
|
|
46
49
|
raise ComposeError("No implementation found.")
|
telegrinder/node/rule.py
CHANGED