telegrinder 0.3.3.post1__py3-none-any.whl → 0.3.4.post1__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 +144 -144
- telegrinder/api/__init__.py +8 -8
- telegrinder/api/api.py +93 -93
- telegrinder/api/error.py +16 -16
- telegrinder/api/response.py +20 -20
- telegrinder/api/token.py +36 -36
- telegrinder/bot/__init__.py +66 -66
- telegrinder/bot/bot.py +76 -76
- telegrinder/bot/cute_types/__init__.py +17 -17
- telegrinder/bot/cute_types/base.py +258 -258
- telegrinder/bot/cute_types/callback_query.py +385 -385
- telegrinder/bot/cute_types/chat_join_request.py +61 -61
- telegrinder/bot/cute_types/chat_member_updated.py +160 -160
- telegrinder/bot/cute_types/inline_query.py +43 -43
- telegrinder/bot/cute_types/message.py +2637 -2637
- telegrinder/bot/cute_types/update.py +104 -109
- telegrinder/bot/cute_types/utils.py +95 -95
- telegrinder/bot/dispatch/__init__.py +55 -55
- telegrinder/bot/dispatch/abc.py +77 -77
- telegrinder/bot/dispatch/context.py +98 -98
- telegrinder/bot/dispatch/dispatch.py +202 -202
- telegrinder/bot/dispatch/handler/__init__.py +13 -13
- telegrinder/bot/dispatch/handler/abc.py +24 -24
- telegrinder/bot/dispatch/handler/audio_reply.py +44 -44
- telegrinder/bot/dispatch/handler/base.py +57 -57
- telegrinder/bot/dispatch/handler/document_reply.py +44 -44
- telegrinder/bot/dispatch/handler/func.py +135 -135
- telegrinder/bot/dispatch/handler/media_group_reply.py +43 -43
- telegrinder/bot/dispatch/handler/message_reply.py +36 -36
- telegrinder/bot/dispatch/handler/photo_reply.py +44 -44
- telegrinder/bot/dispatch/handler/sticker_reply.py +37 -37
- telegrinder/bot/dispatch/handler/video_reply.py +44 -44
- telegrinder/bot/dispatch/middleware/__init__.py +3 -3
- telegrinder/bot/dispatch/middleware/abc.py +22 -16
- telegrinder/bot/dispatch/process.py +157 -132
- telegrinder/bot/dispatch/return_manager/__init__.py +13 -13
- telegrinder/bot/dispatch/return_manager/abc.py +108 -108
- telegrinder/bot/dispatch/return_manager/callback_query.py +20 -20
- telegrinder/bot/dispatch/return_manager/inline_query.py +15 -15
- telegrinder/bot/dispatch/return_manager/message.py +36 -36
- telegrinder/bot/dispatch/view/__init__.py +13 -13
- telegrinder/bot/dispatch/view/abc.py +41 -41
- telegrinder/bot/dispatch/view/base.py +200 -200
- telegrinder/bot/dispatch/view/box.py +129 -129
- telegrinder/bot/dispatch/view/callback_query.py +17 -17
- telegrinder/bot/dispatch/view/chat_join_request.py +16 -16
- telegrinder/bot/dispatch/view/chat_member.py +39 -39
- telegrinder/bot/dispatch/view/inline_query.py +17 -17
- telegrinder/bot/dispatch/view/message.py +44 -44
- telegrinder/bot/dispatch/view/raw.py +114 -114
- telegrinder/bot/dispatch/waiter_machine/__init__.py +17 -17
- telegrinder/bot/dispatch/waiter_machine/actions.py +13 -13
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +8 -8
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +55 -55
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +57 -57
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +51 -51
- telegrinder/bot/dispatch/waiter_machine/hasher/state.py +19 -19
- telegrinder/bot/dispatch/waiter_machine/machine.py +172 -167
- telegrinder/bot/dispatch/waiter_machine/middleware.py +89 -89
- telegrinder/bot/dispatch/waiter_machine/short_state.py +68 -68
- telegrinder/bot/polling/__init__.py +4 -4
- telegrinder/bot/polling/abc.py +25 -25
- telegrinder/bot/polling/polling.py +131 -131
- telegrinder/bot/rules/__init__.py +62 -62
- telegrinder/bot/rules/abc.py +213 -213
- telegrinder/bot/rules/adapter/__init__.py +12 -9
- telegrinder/bot/rules/adapter/abc.py +31 -29
- telegrinder/bot/rules/adapter/errors.py +5 -5
- telegrinder/bot/rules/adapter/event.py +65 -67
- telegrinder/bot/rules/adapter/node.py +48 -48
- telegrinder/bot/rules/adapter/raw_event.py +27 -0
- telegrinder/bot/rules/adapter/raw_update.py +30 -30
- telegrinder/bot/rules/callback_data.py +170 -170
- telegrinder/bot/rules/chat_join.py +46 -46
- telegrinder/bot/rules/command.py +126 -126
- telegrinder/bot/rules/enum_text.py +36 -36
- telegrinder/bot/rules/func.py +26 -26
- telegrinder/bot/rules/fuzzy.py +24 -24
- telegrinder/bot/rules/inline.py +60 -60
- telegrinder/bot/rules/integer.py +20 -20
- telegrinder/bot/rules/is_from.py +127 -127
- telegrinder/bot/rules/markup.py +43 -43
- telegrinder/bot/rules/mention.py +14 -14
- telegrinder/bot/rules/message.py +17 -17
- telegrinder/bot/rules/message_entities.py +35 -35
- telegrinder/bot/rules/node.py +27 -27
- telegrinder/bot/rules/regex.py +37 -37
- telegrinder/bot/rules/rule_enum.py +72 -72
- telegrinder/bot/rules/start.py +42 -42
- telegrinder/bot/rules/state.py +37 -37
- telegrinder/bot/rules/text.py +33 -33
- telegrinder/bot/rules/update.py +15 -15
- telegrinder/bot/scenario/__init__.py +5 -5
- telegrinder/bot/scenario/abc.py +19 -19
- telegrinder/bot/scenario/checkbox.py +176 -167
- telegrinder/bot/scenario/choice.py +51 -46
- telegrinder/client/__init__.py +4 -4
- telegrinder/client/abc.py +75 -75
- telegrinder/client/aiohttp.py +130 -130
- telegrinder/model.py +320 -295
- telegrinder/modules.py +237 -237
- telegrinder/msgspec_json.py +14 -14
- telegrinder/msgspec_utils.py +410 -410
- telegrinder/node/__init__.py +0 -0
- telegrinder/node/attachment.py +87 -87
- telegrinder/node/base.py +166 -166
- telegrinder/node/callback_query.py +53 -53
- telegrinder/node/command.py +33 -33
- telegrinder/node/composer.py +198 -198
- telegrinder/node/container.py +27 -27
- telegrinder/node/event.py +65 -65
- telegrinder/node/me.py +16 -16
- telegrinder/node/message.py +14 -14
- telegrinder/node/polymorphic.py +48 -48
- telegrinder/node/rule.py +76 -76
- telegrinder/node/scope.py +38 -38
- telegrinder/node/source.py +71 -71
- telegrinder/node/text.py +41 -41
- telegrinder/node/tools/__init__.py +3 -3
- telegrinder/node/tools/generator.py +40 -40
- telegrinder/node/update.py +15 -15
- telegrinder/rules.py +0 -0
- telegrinder/tools/__init__.py +74 -74
- telegrinder/tools/buttons.py +79 -79
- telegrinder/tools/error_handler/__init__.py +7 -7
- telegrinder/tools/error_handler/abc.py +33 -33
- telegrinder/tools/error_handler/error.py +9 -9
- telegrinder/tools/error_handler/error_handler.py +193 -193
- telegrinder/tools/formatting/__init__.py +46 -46
- telegrinder/tools/formatting/html.py +283 -283
- telegrinder/tools/formatting/links.py +33 -33
- telegrinder/tools/formatting/spec_html_formats.py +111 -111
- telegrinder/tools/functional.py +12 -12
- telegrinder/tools/global_context/__init__.py +7 -7
- telegrinder/tools/global_context/abc.py +63 -63
- telegrinder/tools/global_context/global_context.py +412 -412
- telegrinder/tools/global_context/telegrinder_ctx.py +27 -27
- telegrinder/tools/i18n/__init__.py +7 -7
- telegrinder/tools/i18n/abc.py +30 -30
- telegrinder/tools/i18n/middleware/__init__.py +3 -3
- telegrinder/tools/i18n/middleware/abc.py +25 -25
- telegrinder/tools/i18n/simple.py +43 -43
- telegrinder/tools/kb_set/__init__.py +4 -4
- telegrinder/tools/kb_set/base.py +15 -15
- telegrinder/tools/kb_set/yaml.py +63 -63
- telegrinder/tools/keyboard.py +132 -132
- telegrinder/tools/limited_dict.py +37 -37
- telegrinder/tools/loop_wrapper/__init__.py +4 -4
- telegrinder/tools/loop_wrapper/abc.py +15 -15
- telegrinder/tools/loop_wrapper/loop_wrapper.py +224 -224
- telegrinder/tools/magic.py +157 -157
- telegrinder/tools/parse_mode.py +6 -6
- telegrinder/tools/state_storage/__init__.py +4 -4
- telegrinder/tools/state_storage/abc.py +35 -35
- telegrinder/tools/state_storage/memory.py +25 -25
- telegrinder/types/__init__.py +260 -260
- telegrinder/types/enums.py +701 -701
- telegrinder/types/methods.py +4633 -4633
- telegrinder/types/objects.py +6950 -8561
- telegrinder/verification_utils.py +32 -32
- {telegrinder-0.3.3.post1.dist-info → telegrinder-0.3.4.post1.dist-info}/LICENSE +22 -22
- {telegrinder-0.3.3.post1.dist-info → telegrinder-0.3.4.post1.dist-info}/METADATA +1 -1
- telegrinder-0.3.4.post1.dist-info/RECORD +165 -0
- telegrinder-0.3.3.post1.dist-info/RECORD +0 -164
- {telegrinder-0.3.3.post1.dist-info → telegrinder-0.3.4.post1.dist-info}/WHEEL +0 -0
telegrinder/node/command.py
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import typing
|
|
2
|
-
from dataclasses import dataclass, field
|
|
3
|
-
|
|
4
|
-
from fntypes.option import Nothing, Option, Some
|
|
5
|
-
|
|
6
|
-
from telegrinder.node.base import DataNode
|
|
7
|
-
from telegrinder.node.text import Text
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def single_split(s: str, separator: str) -> tuple[str, str]:
|
|
11
|
-
left, *right = s.split(separator, 1)
|
|
12
|
-
return left, (right[0] if right else "")
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def cut_mention(text: str) -> tuple[str, Option[str]]:
|
|
16
|
-
left, right = single_split(text, "@")
|
|
17
|
-
return left, Some(right) if right else Nothing()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@dataclass(slots=True)
|
|
21
|
-
class CommandInfo(DataNode):
|
|
22
|
-
name: str
|
|
23
|
-
arguments: str
|
|
24
|
-
mention: Option[str] = field(default_factory=Nothing)
|
|
25
|
-
|
|
26
|
-
@classmethod
|
|
27
|
-
def compose(cls, text: Text) -> typing.Self:
|
|
28
|
-
name, arguments = single_split(text, separator=" ")
|
|
29
|
-
name, mention = cut_mention(name)
|
|
30
|
-
return cls(name, arguments, mention)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
__all__ = ("CommandInfo", "cut_mention", "single_split")
|
|
1
|
+
import typing
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
from fntypes.option import Nothing, Option, Some
|
|
5
|
+
|
|
6
|
+
from telegrinder.node.base import DataNode
|
|
7
|
+
from telegrinder.node.text import Text
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def single_split(s: str, separator: str) -> tuple[str, str]:
|
|
11
|
+
left, *right = s.split(separator, 1)
|
|
12
|
+
return left, (right[0] if right else "")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def cut_mention(text: str) -> tuple[str, Option[str]]:
|
|
16
|
+
left, right = single_split(text, "@")
|
|
17
|
+
return left, Some(right) if right else Nothing()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class CommandInfo(DataNode):
|
|
22
|
+
name: str
|
|
23
|
+
arguments: str
|
|
24
|
+
mention: Option[str] = field(default_factory=Nothing)
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def compose(cls, text: Text) -> typing.Self:
|
|
28
|
+
name, arguments = single_split(text, separator=" ")
|
|
29
|
+
name, mention = cut_mention(name)
|
|
30
|
+
return cls(name, arguments, mention)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ("CommandInfo", "cut_mention", "single_split")
|
telegrinder/node/composer.py
CHANGED
|
@@ -1,198 +1,198 @@
|
|
|
1
|
-
import dataclasses
|
|
2
|
-
import inspect
|
|
3
|
-
import typing
|
|
4
|
-
|
|
5
|
-
from fntypes.error import UnwrapError
|
|
6
|
-
from fntypes.result import Error, Ok, Result
|
|
7
|
-
|
|
8
|
-
from telegrinder.api.api import API
|
|
9
|
-
from telegrinder.bot.cute_types.update import Update, UpdateCute
|
|
10
|
-
from telegrinder.bot.dispatch.context import Context
|
|
11
|
-
from telegrinder.modules import logger
|
|
12
|
-
from telegrinder.node.base import (
|
|
13
|
-
ComposeError,
|
|
14
|
-
Name,
|
|
15
|
-
Node,
|
|
16
|
-
NodeScope,
|
|
17
|
-
get_node_calc_lst,
|
|
18
|
-
get_nodes,
|
|
19
|
-
)
|
|
20
|
-
from telegrinder.tools.magic import magic_bundle
|
|
21
|
-
|
|
22
|
-
CONTEXT_STORE_NODES_KEY = "_node_ctx"
|
|
23
|
-
GLOBAL_VALUE_KEY = "_value"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
async def compose_node(
|
|
27
|
-
_node: type[Node],
|
|
28
|
-
linked: dict[type, typing.Any],
|
|
29
|
-
) -> "NodeSession":
|
|
30
|
-
node = _node.as_node()
|
|
31
|
-
kwargs = magic_bundle(node.compose, linked, typebundle=True)
|
|
32
|
-
|
|
33
|
-
if node.is_generator():
|
|
34
|
-
generator = typing.cast(typing.AsyncGenerator[typing.Any, None], node.compose(**kwargs))
|
|
35
|
-
value = await generator.asend(None)
|
|
36
|
-
else:
|
|
37
|
-
generator = None
|
|
38
|
-
value = typing.cast(typing.Awaitable[typing.Any] | typing.Any, node.compose(**kwargs))
|
|
39
|
-
if inspect.isawaitable(value):
|
|
40
|
-
value = await value
|
|
41
|
-
|
|
42
|
-
return NodeSession(_node, value, {}, generator)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
async def compose_nodes(
|
|
46
|
-
nodes: dict[str, type[Node]],
|
|
47
|
-
ctx: Context,
|
|
48
|
-
data: dict[type[typing.Any], typing.Any] | None = None,
|
|
49
|
-
) -> Result["NodeCollection", ComposeError]:
|
|
50
|
-
logger.debug("Composing nodes: {!r}...", nodes)
|
|
51
|
-
|
|
52
|
-
local_nodes: dict[type[Node], NodeSession]
|
|
53
|
-
data = {Context: ctx} | (data or {})
|
|
54
|
-
parent_nodes: dict[type[Node], NodeSession] = {}
|
|
55
|
-
event_nodes: dict[type[Node], NodeSession] = ctx.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
56
|
-
# TODO: optimize flattened list calculation via caching key = tuple of node types
|
|
57
|
-
calculation_nodes: dict[tuple[str, type[Node]], tuple[type[Node], ...]] = {
|
|
58
|
-
(node_name, node_t): tuple(get_node_calc_lst(node_t)) for node_name, node_t in nodes.items()
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
for (parent_node_name, parent_node_t), linked_nodes in calculation_nodes.items():
|
|
62
|
-
local_nodes = {}
|
|
63
|
-
subnodes = {}
|
|
64
|
-
data[Name] = parent_node_name
|
|
65
|
-
|
|
66
|
-
for node_t in linked_nodes:
|
|
67
|
-
scope = getattr(node_t, "scope", None)
|
|
68
|
-
|
|
69
|
-
if scope is NodeScope.PER_EVENT and node_t in event_nodes:
|
|
70
|
-
local_nodes[node_t] = event_nodes[node_t]
|
|
71
|
-
continue
|
|
72
|
-
elif scope is NodeScope.GLOBAL and hasattr(node_t, GLOBAL_VALUE_KEY):
|
|
73
|
-
local_nodes[node_t] = getattr(node_t, GLOBAL_VALUE_KEY)
|
|
74
|
-
continue
|
|
75
|
-
|
|
76
|
-
subnodes |= {
|
|
77
|
-
k: session.value for k, session in (local_nodes | event_nodes).items() if k not in subnodes
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
try:
|
|
81
|
-
local_nodes[node_t] = await compose_node(node_t, subnodes | data)
|
|
82
|
-
except (ComposeError, UnwrapError) as exc:
|
|
83
|
-
for t, local_node in local_nodes.items():
|
|
84
|
-
if t.scope is NodeScope.PER_CALL:
|
|
85
|
-
await local_node.close()
|
|
86
|
-
return Error(ComposeError(f"Cannot compose {node_t}. Error: {exc}"))
|
|
87
|
-
|
|
88
|
-
if scope is NodeScope.PER_EVENT:
|
|
89
|
-
event_nodes[node_t] = local_nodes[node_t]
|
|
90
|
-
elif scope is NodeScope.GLOBAL:
|
|
91
|
-
setattr(node_t, GLOBAL_VALUE_KEY, local_nodes[node_t])
|
|
92
|
-
|
|
93
|
-
parent_nodes[parent_node_t] = local_nodes[parent_node_t]
|
|
94
|
-
|
|
95
|
-
node_sessions = {k: parent_nodes[t] for k, t in nodes.items()}
|
|
96
|
-
return Ok(NodeCollection(node_sessions))
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@dataclasses.dataclass(slots=True, repr=False)
|
|
100
|
-
class NodeSession:
|
|
101
|
-
node_type: type[Node] | None
|
|
102
|
-
value: typing.Any
|
|
103
|
-
subnodes: dict[str, typing.Self]
|
|
104
|
-
generator: typing.AsyncGenerator[typing.Any, typing.Any | None] | None = None
|
|
105
|
-
|
|
106
|
-
def __repr__(self) -> str:
|
|
107
|
-
return f"<{self.__class__.__name__}: {self.value!r}" + (" (ACTIVE)>" if self.generator else ">")
|
|
108
|
-
|
|
109
|
-
async def close(
|
|
110
|
-
self,
|
|
111
|
-
with_value: typing.Any | None = None,
|
|
112
|
-
scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
|
|
113
|
-
) -> None:
|
|
114
|
-
if self.node_type and getattr(self.node_type, "scope", None) not in scopes:
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
for subnode in self.subnodes.values():
|
|
118
|
-
await subnode.close(scopes=scopes)
|
|
119
|
-
|
|
120
|
-
if self.generator is None:
|
|
121
|
-
return
|
|
122
|
-
try:
|
|
123
|
-
logger.debug("Closing session for node {!r}...", self.node_type)
|
|
124
|
-
await self.generator.asend(with_value)
|
|
125
|
-
except StopAsyncIteration:
|
|
126
|
-
self.generator = None
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
class NodeCollection:
|
|
130
|
-
__slots__ = ("sessions", "_values")
|
|
131
|
-
|
|
132
|
-
def __init__(self, sessions: dict[str, NodeSession]) -> None:
|
|
133
|
-
self.sessions = sessions
|
|
134
|
-
self._values: dict[str, typing.Any] = {}
|
|
135
|
-
|
|
136
|
-
def __repr__(self) -> str:
|
|
137
|
-
return "<{}: sessions={!r}>".format(self.__class__.__name__, self.sessions)
|
|
138
|
-
|
|
139
|
-
@property
|
|
140
|
-
def values(self) -> dict[str, typing.Any]:
|
|
141
|
-
if self._values.keys() == self.sessions.keys():
|
|
142
|
-
return self._values
|
|
143
|
-
|
|
144
|
-
for name, session in self.sessions.items():
|
|
145
|
-
if name not in self._values:
|
|
146
|
-
self._values[name] = session.value
|
|
147
|
-
|
|
148
|
-
return self._values
|
|
149
|
-
|
|
150
|
-
async def close_all(
|
|
151
|
-
self,
|
|
152
|
-
with_value: typing.Any | None = None,
|
|
153
|
-
scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
|
|
154
|
-
) -> None:
|
|
155
|
-
for session in self.sessions.values():
|
|
156
|
-
await session.close(with_value, scopes=scopes)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
@dataclasses.dataclass(slots=True, repr=False)
|
|
160
|
-
class Composition:
|
|
161
|
-
func: typing.Callable[..., typing.Any]
|
|
162
|
-
is_blocking: bool
|
|
163
|
-
nodes: dict[str, type[Node]] = dataclasses.field(init=False)
|
|
164
|
-
|
|
165
|
-
def __post_init__(self) -> None:
|
|
166
|
-
self.nodes = get_nodes(self.func)
|
|
167
|
-
|
|
168
|
-
def __repr__(self) -> str:
|
|
169
|
-
return "<{}: for function={!r} with nodes={!r}>".format(
|
|
170
|
-
("blocking " if self.is_blocking else "") + self.__class__.__name__,
|
|
171
|
-
self.func.__qualname__,
|
|
172
|
-
self.nodes,
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
async def compose_nodes(
|
|
176
|
-
self,
|
|
177
|
-
update: UpdateCute,
|
|
178
|
-
context: Context,
|
|
179
|
-
) -> NodeCollection | None:
|
|
180
|
-
match await compose_nodes(
|
|
181
|
-
nodes=self.nodes,
|
|
182
|
-
ctx=context,
|
|
183
|
-
data={Update: update, API: update.api},
|
|
184
|
-
):
|
|
185
|
-
case Ok(col):
|
|
186
|
-
return col
|
|
187
|
-
case Error(err):
|
|
188
|
-
logger.debug(f"Composition failed with error: {err!r}")
|
|
189
|
-
return None
|
|
190
|
-
|
|
191
|
-
async def __call__(self, node_cls: type[Node], **kwargs: typing.Any) -> typing.Any:
|
|
192
|
-
result = self.func(node_cls, **magic_bundle(self.func, kwargs, start_idx=0, bundle_ctx=False))
|
|
193
|
-
if inspect.isawaitable(result):
|
|
194
|
-
result = await result
|
|
195
|
-
return result
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
__all__ = ("Composition", "NodeCollection", "NodeSession", "compose_node", "compose_nodes")
|
|
1
|
+
import dataclasses
|
|
2
|
+
import inspect
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from fntypes.error import UnwrapError
|
|
6
|
+
from fntypes.result import Error, Ok, Result
|
|
7
|
+
|
|
8
|
+
from telegrinder.api.api import API
|
|
9
|
+
from telegrinder.bot.cute_types.update import Update, UpdateCute
|
|
10
|
+
from telegrinder.bot.dispatch.context import Context
|
|
11
|
+
from telegrinder.modules import logger
|
|
12
|
+
from telegrinder.node.base import (
|
|
13
|
+
ComposeError,
|
|
14
|
+
Name,
|
|
15
|
+
Node,
|
|
16
|
+
NodeScope,
|
|
17
|
+
get_node_calc_lst,
|
|
18
|
+
get_nodes,
|
|
19
|
+
)
|
|
20
|
+
from telegrinder.tools.magic import magic_bundle
|
|
21
|
+
|
|
22
|
+
CONTEXT_STORE_NODES_KEY = "_node_ctx"
|
|
23
|
+
GLOBAL_VALUE_KEY = "_value"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def compose_node(
|
|
27
|
+
_node: type[Node],
|
|
28
|
+
linked: dict[type, typing.Any],
|
|
29
|
+
) -> "NodeSession":
|
|
30
|
+
node = _node.as_node()
|
|
31
|
+
kwargs = magic_bundle(node.compose, linked, typebundle=True)
|
|
32
|
+
|
|
33
|
+
if node.is_generator():
|
|
34
|
+
generator = typing.cast(typing.AsyncGenerator[typing.Any, None], node.compose(**kwargs))
|
|
35
|
+
value = await generator.asend(None)
|
|
36
|
+
else:
|
|
37
|
+
generator = None
|
|
38
|
+
value = typing.cast(typing.Awaitable[typing.Any] | typing.Any, node.compose(**kwargs))
|
|
39
|
+
if inspect.isawaitable(value):
|
|
40
|
+
value = await value
|
|
41
|
+
|
|
42
|
+
return NodeSession(_node, value, {}, generator)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def compose_nodes(
|
|
46
|
+
nodes: dict[str, type[Node]],
|
|
47
|
+
ctx: Context,
|
|
48
|
+
data: dict[type[typing.Any], typing.Any] | None = None,
|
|
49
|
+
) -> Result["NodeCollection", ComposeError]:
|
|
50
|
+
logger.debug("Composing nodes: {!r}...", nodes)
|
|
51
|
+
|
|
52
|
+
local_nodes: dict[type[Node], NodeSession]
|
|
53
|
+
data = {Context: ctx} | (data or {})
|
|
54
|
+
parent_nodes: dict[type[Node], NodeSession] = {}
|
|
55
|
+
event_nodes: dict[type[Node], NodeSession] = ctx.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
56
|
+
# TODO: optimize flattened list calculation via caching key = tuple of node types
|
|
57
|
+
calculation_nodes: dict[tuple[str, type[Node]], tuple[type[Node], ...]] = {
|
|
58
|
+
(node_name, node_t): tuple(get_node_calc_lst(node_t)) for node_name, node_t in nodes.items()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (parent_node_name, parent_node_t), linked_nodes in calculation_nodes.items():
|
|
62
|
+
local_nodes = {}
|
|
63
|
+
subnodes = {}
|
|
64
|
+
data[Name] = parent_node_name
|
|
65
|
+
|
|
66
|
+
for node_t in linked_nodes:
|
|
67
|
+
scope = getattr(node_t, "scope", None)
|
|
68
|
+
|
|
69
|
+
if scope is NodeScope.PER_EVENT and node_t in event_nodes:
|
|
70
|
+
local_nodes[node_t] = event_nodes[node_t]
|
|
71
|
+
continue
|
|
72
|
+
elif scope is NodeScope.GLOBAL and hasattr(node_t, GLOBAL_VALUE_KEY):
|
|
73
|
+
local_nodes[node_t] = getattr(node_t, GLOBAL_VALUE_KEY)
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
subnodes |= {
|
|
77
|
+
k: session.value for k, session in (local_nodes | event_nodes).items() if k not in subnodes
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
local_nodes[node_t] = await compose_node(node_t, subnodes | data)
|
|
82
|
+
except (ComposeError, UnwrapError) as exc:
|
|
83
|
+
for t, local_node in local_nodes.items():
|
|
84
|
+
if t.scope is NodeScope.PER_CALL:
|
|
85
|
+
await local_node.close()
|
|
86
|
+
return Error(ComposeError(f"Cannot compose {node_t}. Error: {exc}"))
|
|
87
|
+
|
|
88
|
+
if scope is NodeScope.PER_EVENT:
|
|
89
|
+
event_nodes[node_t] = local_nodes[node_t]
|
|
90
|
+
elif scope is NodeScope.GLOBAL:
|
|
91
|
+
setattr(node_t, GLOBAL_VALUE_KEY, local_nodes[node_t])
|
|
92
|
+
|
|
93
|
+
parent_nodes[parent_node_t] = local_nodes[parent_node_t]
|
|
94
|
+
|
|
95
|
+
node_sessions = {k: parent_nodes[t] for k, t in nodes.items()}
|
|
96
|
+
return Ok(NodeCollection(node_sessions))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclasses.dataclass(slots=True, repr=False)
|
|
100
|
+
class NodeSession:
|
|
101
|
+
node_type: type[Node] | None
|
|
102
|
+
value: typing.Any
|
|
103
|
+
subnodes: dict[str, typing.Self]
|
|
104
|
+
generator: typing.AsyncGenerator[typing.Any, typing.Any | None] | None = None
|
|
105
|
+
|
|
106
|
+
def __repr__(self) -> str:
|
|
107
|
+
return f"<{self.__class__.__name__}: {self.value!r}" + (" (ACTIVE)>" if self.generator else ">")
|
|
108
|
+
|
|
109
|
+
async def close(
|
|
110
|
+
self,
|
|
111
|
+
with_value: typing.Any | None = None,
|
|
112
|
+
scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
|
|
113
|
+
) -> None:
|
|
114
|
+
if self.node_type and getattr(self.node_type, "scope", None) not in scopes:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
for subnode in self.subnodes.values():
|
|
118
|
+
await subnode.close(scopes=scopes)
|
|
119
|
+
|
|
120
|
+
if self.generator is None:
|
|
121
|
+
return
|
|
122
|
+
try:
|
|
123
|
+
logger.debug("Closing session for node {!r}...", self.node_type)
|
|
124
|
+
await self.generator.asend(with_value)
|
|
125
|
+
except StopAsyncIteration:
|
|
126
|
+
self.generator = None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class NodeCollection:
|
|
130
|
+
__slots__ = ("sessions", "_values")
|
|
131
|
+
|
|
132
|
+
def __init__(self, sessions: dict[str, NodeSession]) -> None:
|
|
133
|
+
self.sessions = sessions
|
|
134
|
+
self._values: dict[str, typing.Any] = {}
|
|
135
|
+
|
|
136
|
+
def __repr__(self) -> str:
|
|
137
|
+
return "<{}: sessions={!r}>".format(self.__class__.__name__, self.sessions)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def values(self) -> dict[str, typing.Any]:
|
|
141
|
+
if self._values.keys() == self.sessions.keys():
|
|
142
|
+
return self._values
|
|
143
|
+
|
|
144
|
+
for name, session in self.sessions.items():
|
|
145
|
+
if name not in self._values:
|
|
146
|
+
self._values[name] = session.value
|
|
147
|
+
|
|
148
|
+
return self._values
|
|
149
|
+
|
|
150
|
+
async def close_all(
|
|
151
|
+
self,
|
|
152
|
+
with_value: typing.Any | None = None,
|
|
153
|
+
scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
|
|
154
|
+
) -> None:
|
|
155
|
+
for session in self.sessions.values():
|
|
156
|
+
await session.close(with_value, scopes=scopes)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclasses.dataclass(slots=True, repr=False)
|
|
160
|
+
class Composition:
|
|
161
|
+
func: typing.Callable[..., typing.Any]
|
|
162
|
+
is_blocking: bool
|
|
163
|
+
nodes: dict[str, type[Node]] = dataclasses.field(init=False)
|
|
164
|
+
|
|
165
|
+
def __post_init__(self) -> None:
|
|
166
|
+
self.nodes = get_nodes(self.func)
|
|
167
|
+
|
|
168
|
+
def __repr__(self) -> str:
|
|
169
|
+
return "<{}: for function={!r} with nodes={!r}>".format(
|
|
170
|
+
("blocking " if self.is_blocking else "") + self.__class__.__name__,
|
|
171
|
+
self.func.__qualname__,
|
|
172
|
+
self.nodes,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
async def compose_nodes(
|
|
176
|
+
self,
|
|
177
|
+
update: UpdateCute,
|
|
178
|
+
context: Context,
|
|
179
|
+
) -> NodeCollection | None:
|
|
180
|
+
match await compose_nodes(
|
|
181
|
+
nodes=self.nodes,
|
|
182
|
+
ctx=context,
|
|
183
|
+
data={Update: update, API: update.api},
|
|
184
|
+
):
|
|
185
|
+
case Ok(col):
|
|
186
|
+
return col
|
|
187
|
+
case Error(err):
|
|
188
|
+
logger.debug(f"Composition failed with error: {err!r}")
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
async def __call__(self, node_cls: type[Node], **kwargs: typing.Any) -> typing.Any:
|
|
192
|
+
result = self.func(node_cls, **magic_bundle(self.func, kwargs, start_idx=0, bundle_ctx=False))
|
|
193
|
+
if inspect.isawaitable(result):
|
|
194
|
+
result = await result
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
__all__ = ("Composition", "NodeCollection", "NodeSession", "compose_node", "compose_nodes")
|
telegrinder/node/container.py
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import typing
|
|
2
|
-
|
|
3
|
-
from telegrinder.node.base import Node
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class ContainerNode(Node):
|
|
7
|
-
linked_nodes: typing.ClassVar[list[type[Node]]]
|
|
8
|
-
|
|
9
|
-
@classmethod
|
|
10
|
-
def compose(cls, **kw) -> tuple[Node, ...]:
|
|
11
|
-
return tuple(t[1] for t in sorted(kw.items(), key=lambda t: t[0]))
|
|
12
|
-
|
|
13
|
-
@classmethod
|
|
14
|
-
def get_subnodes(cls) -> dict[str, type[Node]]:
|
|
15
|
-
subnodes = getattr(cls, "subnodes", None)
|
|
16
|
-
if subnodes is None:
|
|
17
|
-
subnodes_dct = {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
|
|
18
|
-
setattr(cls, "subnodes", subnodes_dct)
|
|
19
|
-
return subnodes_dct
|
|
20
|
-
return subnodes
|
|
21
|
-
|
|
22
|
-
@classmethod
|
|
23
|
-
def link_nodes(cls, linked_nodes: list[type[Node]]) -> type["ContainerNode"]:
|
|
24
|
-
return type("_ContainerNode", (cls,), {"linked_nodes": linked_nodes})
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
__all__ = ("ContainerNode",)
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from telegrinder.node.base import Node
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ContainerNode(Node):
|
|
7
|
+
linked_nodes: typing.ClassVar[list[type[Node]]]
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def compose(cls, **kw) -> tuple[Node, ...]:
|
|
11
|
+
return tuple(t[1] for t in sorted(kw.items(), key=lambda t: t[0]))
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def get_subnodes(cls) -> dict[str, type[Node]]:
|
|
15
|
+
subnodes = getattr(cls, "subnodes", None)
|
|
16
|
+
if subnodes is None:
|
|
17
|
+
subnodes_dct = {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
|
|
18
|
+
setattr(cls, "subnodes", subnodes_dct)
|
|
19
|
+
return subnodes_dct
|
|
20
|
+
return subnodes
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def link_nodes(cls, linked_nodes: list[type[Node]]) -> type["ContainerNode"]:
|
|
24
|
+
return type("_ContainerNode", (cls,), {"linked_nodes": linked_nodes})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = ("ContainerNode",)
|
telegrinder/node/event.py
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
import dataclasses
|
|
2
|
-
import typing
|
|
3
|
-
|
|
4
|
-
import msgspec
|
|
5
|
-
|
|
6
|
-
from telegrinder.api.api import API
|
|
7
|
-
from telegrinder.bot.cute_types import BaseCute
|
|
8
|
-
from telegrinder.bot.dispatch.context import Context
|
|
9
|
-
from telegrinder.msgspec_utils import DataclassInstance, decoder
|
|
10
|
-
from telegrinder.node.base import ComposeError, FactoryNode
|
|
11
|
-
from telegrinder.node.update import UpdateNode
|
|
12
|
-
|
|
13
|
-
if typing.TYPE_CHECKING:
|
|
14
|
-
Dataclass = typing.TypeVar("Dataclass", bound="DataclassType")
|
|
15
|
-
|
|
16
|
-
DataclassType: typing.TypeAlias = DataclassInstance | msgspec.Struct | dict[str, typing.Any]
|
|
17
|
-
|
|
18
|
-
EVENT_NODE_KEY = "_event_node"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class _EventNode(FactoryNode):
|
|
22
|
-
dataclass: type["DataclassType"]
|
|
23
|
-
|
|
24
|
-
def __class_getitem__(cls, dataclass: type["DataclassType"], /) -> typing.Self:
|
|
25
|
-
return cls(dataclass=dataclass)
|
|
26
|
-
|
|
27
|
-
@classmethod
|
|
28
|
-
def compose(cls, raw_update: UpdateNode, ctx: Context, api: API) -> "DataclassType":
|
|
29
|
-
dataclass_type = typing.get_origin(cls.dataclass) or cls.dataclass
|
|
30
|
-
|
|
31
|
-
try:
|
|
32
|
-
if issubclass(dataclass_type, BaseCute):
|
|
33
|
-
if isinstance(raw_update.incoming_update, dataclass_type):
|
|
34
|
-
dataclass = raw_update.incoming_update
|
|
35
|
-
else:
|
|
36
|
-
dataclass = dataclass_type.from_update(raw_update.incoming_update, bound_api=api)
|
|
37
|
-
|
|
38
|
-
elif issubclass(dataclass_type, msgspec.Struct | dict) or dataclasses.is_dataclass(
|
|
39
|
-
dataclass_type
|
|
40
|
-
):
|
|
41
|
-
dataclass = decoder.convert(
|
|
42
|
-
raw_update.incoming_update.to_full_dict(),
|
|
43
|
-
type=cls.dataclass,
|
|
44
|
-
str_keys=True,
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
else:
|
|
48
|
-
dataclass = cls.dataclass(**raw_update.incoming_update.to_dict())
|
|
49
|
-
|
|
50
|
-
ctx[EVENT_NODE_KEY] = cls
|
|
51
|
-
return dataclass
|
|
52
|
-
except Exception as exc:
|
|
53
|
-
raise ComposeError(f"Cannot parse update into {cls.dataclass.__name__!r}, error: {str(exc)!r}")
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if typing.TYPE_CHECKING:
|
|
57
|
-
EventNode: typing.TypeAlias = typing.Annotated["Dataclass", ...]
|
|
58
|
-
|
|
59
|
-
else:
|
|
60
|
-
|
|
61
|
-
class EventNode(_EventNode):
|
|
62
|
-
pass
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
__all__ = ("EventNode",)
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import msgspec
|
|
5
|
+
|
|
6
|
+
from telegrinder.api.api import API
|
|
7
|
+
from telegrinder.bot.cute_types import BaseCute
|
|
8
|
+
from telegrinder.bot.dispatch.context import Context
|
|
9
|
+
from telegrinder.msgspec_utils import DataclassInstance, decoder
|
|
10
|
+
from telegrinder.node.base import ComposeError, FactoryNode
|
|
11
|
+
from telegrinder.node.update import UpdateNode
|
|
12
|
+
|
|
13
|
+
if typing.TYPE_CHECKING:
|
|
14
|
+
Dataclass = typing.TypeVar("Dataclass", bound="DataclassType")
|
|
15
|
+
|
|
16
|
+
DataclassType: typing.TypeAlias = DataclassInstance | msgspec.Struct | dict[str, typing.Any]
|
|
17
|
+
|
|
18
|
+
EVENT_NODE_KEY = "_event_node"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _EventNode(FactoryNode):
|
|
22
|
+
dataclass: type["DataclassType"]
|
|
23
|
+
|
|
24
|
+
def __class_getitem__(cls, dataclass: type["DataclassType"], /) -> typing.Self:
|
|
25
|
+
return cls(dataclass=dataclass)
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def compose(cls, raw_update: UpdateNode, ctx: Context, api: API) -> "DataclassType":
|
|
29
|
+
dataclass_type = typing.get_origin(cls.dataclass) or cls.dataclass
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
if issubclass(dataclass_type, BaseCute):
|
|
33
|
+
if isinstance(raw_update.incoming_update, dataclass_type):
|
|
34
|
+
dataclass = raw_update.incoming_update
|
|
35
|
+
else:
|
|
36
|
+
dataclass = dataclass_type.from_update(raw_update.incoming_update, bound_api=api)
|
|
37
|
+
|
|
38
|
+
elif issubclass(dataclass_type, msgspec.Struct | dict) or dataclasses.is_dataclass(
|
|
39
|
+
dataclass_type
|
|
40
|
+
):
|
|
41
|
+
dataclass = decoder.convert(
|
|
42
|
+
raw_update.incoming_update.to_full_dict(),
|
|
43
|
+
type=cls.dataclass,
|
|
44
|
+
str_keys=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
else:
|
|
48
|
+
dataclass = cls.dataclass(**raw_update.incoming_update.to_dict())
|
|
49
|
+
|
|
50
|
+
ctx[EVENT_NODE_KEY] = cls
|
|
51
|
+
return dataclass
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
raise ComposeError(f"Cannot parse update into {cls.dataclass.__name__!r}, error: {str(exc)!r}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if typing.TYPE_CHECKING:
|
|
57
|
+
EventNode: typing.TypeAlias = typing.Annotated["Dataclass", ...]
|
|
58
|
+
|
|
59
|
+
else:
|
|
60
|
+
|
|
61
|
+
class EventNode(_EventNode):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
__all__ = ("EventNode",)
|