telegrinder 0.3.4__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 +148 -149
- telegrinder/api/__init__.py +9 -8
- telegrinder/api/api.py +101 -93
- telegrinder/api/error.py +20 -16
- telegrinder/api/response.py +20 -20
- telegrinder/api/token.py +36 -36
- telegrinder/bot/__init__.py +72 -66
- telegrinder/bot/bot.py +83 -76
- telegrinder/bot/cute_types/__init__.py +19 -17
- telegrinder/bot/cute_types/base.py +184 -258
- telegrinder/bot/cute_types/callback_query.py +400 -385
- telegrinder/bot/cute_types/chat_join_request.py +62 -61
- telegrinder/bot/cute_types/chat_member_updated.py +157 -160
- telegrinder/bot/cute_types/inline_query.py +44 -43
- telegrinder/bot/cute_types/message.py +2590 -2637
- telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
- telegrinder/bot/cute_types/update.py +112 -104
- telegrinder/bot/cute_types/utils.py +62 -95
- telegrinder/bot/dispatch/__init__.py +59 -55
- telegrinder/bot/dispatch/abc.py +76 -77
- telegrinder/bot/dispatch/context.py +96 -98
- telegrinder/bot/dispatch/dispatch.py +254 -202
- telegrinder/bot/dispatch/handler/__init__.py +13 -13
- telegrinder/bot/dispatch/handler/abc.py +23 -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 +129 -135
- telegrinder/bot/dispatch/handler/media_group_reply.py +44 -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 +97 -22
- telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
- telegrinder/bot/dispatch/process.py +151 -157
- telegrinder/bot/dispatch/return_manager/__init__.py +15 -13
- telegrinder/bot/dispatch/return_manager/abc.py +104 -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/return_manager/pre_checkout_query.py +20 -0
- telegrinder/bot/dispatch/view/__init__.py +15 -13
- telegrinder/bot/dispatch/view/abc.py +45 -41
- telegrinder/bot/dispatch/view/base.py +231 -200
- telegrinder/bot/dispatch/view/box.py +140 -129
- telegrinder/bot/dispatch/view/callback_query.py +16 -17
- telegrinder/bot/dispatch/view/chat_join_request.py +11 -16
- telegrinder/bot/dispatch/view/chat_member.py +37 -39
- telegrinder/bot/dispatch/view/inline_query.py +16 -17
- telegrinder/bot/dispatch/view/message.py +43 -44
- telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
- telegrinder/bot/dispatch/view/raw.py +116 -114
- telegrinder/bot/dispatch/waiter_machine/__init__.py +17 -17
- telegrinder/bot/dispatch/waiter_machine/actions.py +14 -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 +59 -57
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +51 -51
- telegrinder/bot/dispatch/waiter_machine/hasher/state.py +20 -19
- telegrinder/bot/dispatch/waiter_machine/machine.py +251 -172
- telegrinder/bot/dispatch/waiter_machine/middleware.py +94 -89
- telegrinder/bot/dispatch/waiter_machine/short_state.py +57 -68
- telegrinder/bot/polling/__init__.py +4 -4
- telegrinder/bot/polling/abc.py +25 -25
- telegrinder/bot/polling/polling.py +139 -131
- telegrinder/bot/rules/__init__.py +85 -62
- telegrinder/bot/rules/abc.py +213 -206
- telegrinder/bot/rules/callback_data.py +122 -163
- telegrinder/bot/rules/chat_join.py +45 -43
- telegrinder/bot/rules/command.py +126 -126
- telegrinder/bot/rules/enum_text.py +33 -36
- telegrinder/bot/rules/func.py +28 -26
- telegrinder/bot/rules/fuzzy.py +24 -24
- telegrinder/bot/rules/id.py +24 -0
- telegrinder/bot/rules/inline.py +58 -56
- telegrinder/bot/rules/integer.py +21 -20
- telegrinder/bot/rules/is_from.py +127 -127
- telegrinder/bot/rules/logic.py +18 -0
- telegrinder/bot/rules/markup.py +42 -43
- telegrinder/bot/rules/mention.py +14 -14
- telegrinder/bot/rules/message.py +15 -17
- telegrinder/bot/rules/message_entities.py +33 -35
- telegrinder/bot/rules/node.py +33 -27
- telegrinder/bot/rules/payload.py +81 -0
- telegrinder/bot/rules/payment_invoice.py +29 -0
- telegrinder/bot/rules/regex.py +36 -37
- telegrinder/bot/rules/rule_enum.py +72 -72
- telegrinder/bot/rules/start.py +42 -42
- telegrinder/bot/rules/state.py +35 -37
- telegrinder/bot/rules/text.py +38 -33
- telegrinder/bot/rules/update.py +15 -15
- telegrinder/bot/scenario/__init__.py +5 -5
- telegrinder/bot/scenario/abc.py +17 -19
- telegrinder/bot/scenario/checkbox.py +174 -176
- telegrinder/bot/scenario/choice.py +48 -51
- telegrinder/client/__init__.py +12 -4
- telegrinder/client/abc.py +100 -75
- telegrinder/client/aiohttp.py +134 -130
- telegrinder/client/form_data.py +31 -0
- telegrinder/client/sonic.py +212 -0
- telegrinder/model.py +208 -315
- telegrinder/modules.py +239 -237
- telegrinder/msgspec_json.py +14 -14
- telegrinder/msgspec_utils.py +478 -410
- telegrinder/node/__init__.py +86 -25
- telegrinder/node/attachment.py +163 -87
- telegrinder/node/base.py +288 -160
- telegrinder/node/callback_query.py +54 -53
- telegrinder/node/command.py +34 -33
- telegrinder/node/composer.py +163 -198
- telegrinder/node/container.py +33 -27
- telegrinder/node/either.py +82 -0
- telegrinder/node/event.py +54 -65
- telegrinder/node/file.py +51 -0
- telegrinder/node/me.py +15 -16
- telegrinder/node/payload.py +78 -0
- telegrinder/node/polymorphic.py +67 -48
- telegrinder/node/rule.py +72 -76
- telegrinder/node/scope.py +36 -38
- telegrinder/node/source.py +87 -71
- telegrinder/node/text.py +53 -41
- telegrinder/node/tools/__init__.py +3 -3
- telegrinder/node/tools/generator.py +36 -40
- telegrinder/py.typed +0 -0
- telegrinder/rules.py +1 -62
- telegrinder/tools/__init__.py +152 -93
- 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/errors.py +5 -5
- telegrinder/{bot/rules → tools}/adapter/event.py +63 -65
- telegrinder/{bot/rules → tools}/adapter/node.py +46 -48
- telegrinder/{bot/rules → tools}/adapter/raw_event.py +27 -27
- telegrinder/{bot/rules → tools}/adapter/raw_update.py +30 -30
- telegrinder/tools/buttons.py +106 -80
- 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/__init__.py +7 -7
- telegrinder/tools/error_handler/abc.py +30 -33
- telegrinder/tools/error_handler/error.py +9 -9
- telegrinder/tools/error_handler/error_handler.py +179 -193
- telegrinder/tools/formatting/__init__.py +83 -63
- telegrinder/tools/formatting/deep_links.py +541 -0
- telegrinder/tools/formatting/{html.py → html_formatter.py} +266 -294
- telegrinder/tools/formatting/spec_html_formats.py +71 -117
- telegrinder/tools/functional.py +8 -12
- telegrinder/tools/global_context/__init__.py +7 -7
- telegrinder/tools/global_context/abc.py +63 -63
- telegrinder/tools/global_context/global_context.py +387 -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 +22 -25
- telegrinder/tools/i18n/simple.py +43 -43
- telegrinder/tools/input_file_directory.py +30 -0
- telegrinder/tools/keyboard.py +128 -128
- telegrinder/tools/lifespan.py +105 -0
- telegrinder/tools/limited_dict.py +32 -37
- telegrinder/tools/loop_wrapper/__init__.py +4 -4
- telegrinder/tools/loop_wrapper/abc.py +20 -15
- telegrinder/tools/loop_wrapper/loop_wrapper.py +169 -224
- telegrinder/tools/magic.py +307 -157
- telegrinder/tools/parse_mode.py +6 -6
- telegrinder/tools/state_storage/__init__.py +4 -4
- telegrinder/tools/state_storage/abc.py +31 -35
- telegrinder/tools/state_storage/memory.py +25 -25
- telegrinder/tools/strings.py +13 -0
- telegrinder/types/__init__.py +268 -260
- telegrinder/types/enums.py +711 -701
- telegrinder/types/input_file.py +51 -0
- telegrinder/types/methods.py +5055 -4633
- telegrinder/types/objects.py +7058 -6950
- telegrinder/verification_utils.py +30 -32
- {telegrinder-0.3.4.dist-info → telegrinder-0.4.0.dist-info}/LICENSE +22 -22
- telegrinder-0.4.0.dist-info/METADATA +144 -0
- telegrinder-0.4.0.dist-info/RECORD +182 -0
- {telegrinder-0.3.4.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.dist-info/METADATA +0 -110
- telegrinder-0.3.4.dist-info/RECORD +0 -165
telegrinder/node/composer.py
CHANGED
|
@@ -1,198 +1,163 @@
|
|
|
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.
|
|
9
|
-
from telegrinder.
|
|
10
|
-
from telegrinder.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
for
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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.bot.dispatch.context import Context
|
|
9
|
+
from telegrinder.modules import logger
|
|
10
|
+
from telegrinder.node.base import (
|
|
11
|
+
ComposeError,
|
|
12
|
+
IsNode,
|
|
13
|
+
Name,
|
|
14
|
+
NodeImpersonation,
|
|
15
|
+
NodeScope,
|
|
16
|
+
NodeType,
|
|
17
|
+
unwrap_node,
|
|
18
|
+
)
|
|
19
|
+
from telegrinder.tools.magic import join_dicts, magic_bundle
|
|
20
|
+
|
|
21
|
+
type AsyncGenerator = typing.AsyncGenerator[typing.Any, None]
|
|
22
|
+
|
|
23
|
+
CONTEXT_STORE_NODES_KEY = "_node_ctx"
|
|
24
|
+
GLOBAL_VALUE_KEY = "_value"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_scope(node: type[NodeType], /) -> NodeScope | None:
|
|
28
|
+
return getattr(node, "scope", None)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def compose_node(
|
|
32
|
+
node: type[NodeType],
|
|
33
|
+
linked: dict[type[typing.Any], typing.Any],
|
|
34
|
+
data: dict[type[typing.Any], typing.Any] | None = None,
|
|
35
|
+
) -> "NodeSession":
|
|
36
|
+
subnodes = node.get_subnodes()
|
|
37
|
+
kwargs = magic_bundle(node.compose, join_dicts(subnodes, linked))
|
|
38
|
+
|
|
39
|
+
# Linking data via typebundle
|
|
40
|
+
if data:
|
|
41
|
+
kwargs.update(magic_bundle(node.compose, data, typebundle=True))
|
|
42
|
+
|
|
43
|
+
if node.is_generator():
|
|
44
|
+
generator = typing.cast(AsyncGenerator, node.compose(**kwargs))
|
|
45
|
+
value = await generator.asend(None)
|
|
46
|
+
else:
|
|
47
|
+
generator = None
|
|
48
|
+
value = node.compose(**kwargs)
|
|
49
|
+
if inspect.isawaitable(value):
|
|
50
|
+
value = await value
|
|
51
|
+
|
|
52
|
+
return NodeSession(node, value, subnodes={}, generator=generator)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def compose_nodes(
|
|
56
|
+
nodes: typing.Mapping[str, IsNode | NodeImpersonation],
|
|
57
|
+
ctx: Context,
|
|
58
|
+
data: dict[type[typing.Any], typing.Any] | None = None,
|
|
59
|
+
) -> Result["NodeCollection", ComposeError]:
|
|
60
|
+
logger.debug("Composing nodes: ({})...", " ".join(f"{k}={v!r}" for k, v in nodes.items()))
|
|
61
|
+
|
|
62
|
+
data = {Context: ctx} | (data or {})
|
|
63
|
+
parent_nodes = dict[IsNode, NodeSession]()
|
|
64
|
+
event_nodes: dict[IsNode, NodeSession] = ctx.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
65
|
+
unwrapped_nodes = {(key, n := node.as_node()): unwrap_node(n) for key, node in nodes.items()}
|
|
66
|
+
|
|
67
|
+
for (parent_node_name, parent_node_t), linked_nodes in unwrapped_nodes.items():
|
|
68
|
+
local_nodes = dict[type[NodeType], NodeSession]()
|
|
69
|
+
subnodes = {}
|
|
70
|
+
data[Name] = parent_node_name
|
|
71
|
+
|
|
72
|
+
for node_t in linked_nodes:
|
|
73
|
+
scope = get_scope(node_t)
|
|
74
|
+
|
|
75
|
+
if scope is NodeScope.PER_EVENT and node_t in event_nodes:
|
|
76
|
+
local_nodes[node_t] = event_nodes[node_t]
|
|
77
|
+
continue
|
|
78
|
+
elif scope is NodeScope.GLOBAL and hasattr(node_t, GLOBAL_VALUE_KEY):
|
|
79
|
+
local_nodes[node_t] = NodeSession(node_t, getattr(node_t, GLOBAL_VALUE_KEY), {})
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
subnodes |= {
|
|
83
|
+
k: session.value for k, session in (local_nodes | event_nodes).items() if k not in subnodes
|
|
84
|
+
}
|
|
85
|
+
try:
|
|
86
|
+
local_nodes[node_t] = await compose_node(node_t, linked=subnodes, data=data)
|
|
87
|
+
except (ComposeError, UnwrapError) as exc:
|
|
88
|
+
for t, local_node in local_nodes.items():
|
|
89
|
+
if get_scope(t) is NodeScope.PER_CALL:
|
|
90
|
+
await local_node.close()
|
|
91
|
+
return Error(ComposeError(f"Cannot compose {node_t!r}, error: {str(exc)}"))
|
|
92
|
+
|
|
93
|
+
if scope is NodeScope.PER_EVENT:
|
|
94
|
+
event_nodes[node_t] = local_nodes[node_t]
|
|
95
|
+
elif scope is NodeScope.GLOBAL:
|
|
96
|
+
setattr(node_t, GLOBAL_VALUE_KEY, local_nodes[node_t].value)
|
|
97
|
+
|
|
98
|
+
parent_nodes[parent_node_t] = local_nodes[parent_node_t]
|
|
99
|
+
|
|
100
|
+
return Ok(NodeCollection({k: parent_nodes[t] for k, t in unwrapped_nodes}))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclasses.dataclass(slots=True, repr=False)
|
|
104
|
+
class NodeSession:
|
|
105
|
+
node_type: type[NodeType] | None
|
|
106
|
+
value: typing.Any
|
|
107
|
+
subnodes: dict[str, typing.Self]
|
|
108
|
+
generator: typing.AsyncGenerator[typing.Any, typing.Any | None] | None = None
|
|
109
|
+
|
|
110
|
+
def __repr__(self) -> str:
|
|
111
|
+
return f"<{self.__class__.__name__}: {self.value!r}" + (" (ACTIVE)>" if self.generator else ">")
|
|
112
|
+
|
|
113
|
+
async def close(
|
|
114
|
+
self,
|
|
115
|
+
with_value: typing.Any | None = None,
|
|
116
|
+
scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
|
|
117
|
+
) -> None:
|
|
118
|
+
if self.node_type and getattr(self.node_type, "scope", None) not in scopes:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
for subnode in self.subnodes.values():
|
|
122
|
+
await subnode.close(scopes=scopes)
|
|
123
|
+
|
|
124
|
+
if self.generator is None:
|
|
125
|
+
return
|
|
126
|
+
try:
|
|
127
|
+
logger.debug("Closing session for node {!r}...", self.node_type)
|
|
128
|
+
await self.generator.asend(with_value)
|
|
129
|
+
except StopAsyncIteration:
|
|
130
|
+
self.generator = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class NodeCollection:
|
|
134
|
+
__slots__ = ("sessions", "_values")
|
|
135
|
+
|
|
136
|
+
def __init__(self, sessions: dict[str, NodeSession]) -> None:
|
|
137
|
+
self.sessions = sessions
|
|
138
|
+
self._values: dict[str, typing.Any] = {}
|
|
139
|
+
|
|
140
|
+
def __repr__(self) -> str:
|
|
141
|
+
return "<{}: sessions={!r}>".format(self.__class__.__name__, self.sessions)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def values(self) -> dict[str, typing.Any]:
|
|
145
|
+
if self._values.keys() == self.sessions.keys():
|
|
146
|
+
return self._values
|
|
147
|
+
|
|
148
|
+
for name, session in self.sessions.items():
|
|
149
|
+
if name not in self._values:
|
|
150
|
+
self._values[name] = session.value
|
|
151
|
+
|
|
152
|
+
return self._values
|
|
153
|
+
|
|
154
|
+
async def close_all(
|
|
155
|
+
self,
|
|
156
|
+
with_value: typing.Any | None = None,
|
|
157
|
+
scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
|
|
158
|
+
) -> None:
|
|
159
|
+
for session in self.sessions.values():
|
|
160
|
+
await session.close(with_value, scopes=scopes)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
__all__ = ("NodeCollection", "NodeSession", "compose_node", "compose_nodes")
|
telegrinder/node/container.py
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
|
-
import typing
|
|
2
|
-
|
|
3
|
-
from telegrinder.node.base import Node
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class ContainerNode(Node):
|
|
7
|
-
linked_nodes: typing.ClassVar[list[
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from telegrinder.node.base import IsNode, Node
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ContainerNode(Node):
|
|
7
|
+
linked_nodes: typing.ClassVar[list[IsNode]]
|
|
8
|
+
composer: typing.Callable[..., typing.Awaitable[typing.Any]]
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
async def compose(cls, **kw: typing.Any) -> typing.Any:
|
|
12
|
+
subnodes = cls.get_subnodes().keys()
|
|
13
|
+
return await cls.composer(*tuple(t[1] for t in sorted(kw.items(), key=lambda t: t[0]) if t[0] in subnodes))
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def get_subnodes(cls) -> dict[str, IsNode]:
|
|
17
|
+
subnodes = getattr(cls, "subnodes", None)
|
|
18
|
+
if subnodes is None:
|
|
19
|
+
subnodes_dct = {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
|
|
20
|
+
setattr(cls, "subnodes", subnodes_dct)
|
|
21
|
+
return subnodes_dct
|
|
22
|
+
return subnodes
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def link_nodes(
|
|
26
|
+
cls,
|
|
27
|
+
linked_nodes: list[IsNode],
|
|
28
|
+
composer: typing.Callable[..., typing.Awaitable[typing.Any]],
|
|
29
|
+
) -> type["ContainerNode"]:
|
|
30
|
+
return type(cls.__name__, (cls,), {"linked_nodes": linked_nodes, "composer": classmethod(composer)})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ("ContainerNode",)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from fntypes.result import Ok
|
|
4
|
+
|
|
5
|
+
from telegrinder.api.api import API
|
|
6
|
+
from telegrinder.bot.dispatch.context import Context
|
|
7
|
+
from telegrinder.node.base import ComposeError, FactoryNode, Node
|
|
8
|
+
from telegrinder.node.composer import CONTEXT_STORE_NODES_KEY, GLOBAL_VALUE_KEY, compose_node, compose_nodes
|
|
9
|
+
from telegrinder.node.scope import NodeScope, per_call
|
|
10
|
+
from telegrinder.types.objects import Update
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@per_call
|
|
14
|
+
class _Either(FactoryNode):
|
|
15
|
+
"""Represents a node that either to compose `left` or `right` nodes.
|
|
16
|
+
|
|
17
|
+
For example:
|
|
18
|
+
```python
|
|
19
|
+
# ScalarNode `Integer` -> int
|
|
20
|
+
# ScalarNode `Float` -> float
|
|
21
|
+
|
|
22
|
+
Number = Either[Integer, Float] # using a type alias just as an example
|
|
23
|
+
|
|
24
|
+
def number_to_int(number: Number) -> int:
|
|
25
|
+
return int(number)
|
|
26
|
+
```
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
nodes: tuple[type[Node], type[Node] | None]
|
|
30
|
+
|
|
31
|
+
def __class_getitem__(cls, node: type[Node] | tuple[type[Node], type[Node]], /):
|
|
32
|
+
nodes = (node, None) if not isinstance(node, tuple) else node
|
|
33
|
+
assert len(nodes) == 2, "Node `Either` must have at least two nodes."
|
|
34
|
+
return cls(nodes=nodes)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
async def compose(cls, api: API, update: Update, context: Context) -> typing.Any | None:
|
|
38
|
+
data = {API: api, Update: update, Context: context}
|
|
39
|
+
node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
40
|
+
|
|
41
|
+
for node in cls.nodes:
|
|
42
|
+
if node is None:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
if node.scope is NodeScope.PER_EVENT and node in node_ctx:
|
|
46
|
+
return node_ctx[node].value
|
|
47
|
+
elif node.scope is NodeScope.GLOBAL and hasattr(node, GLOBAL_VALUE_KEY):
|
|
48
|
+
return getattr(node, GLOBAL_VALUE_KEY)
|
|
49
|
+
|
|
50
|
+
subnodes = node.as_node().get_subnodes()
|
|
51
|
+
match await compose_nodes(subnodes, context, data):
|
|
52
|
+
case Ok(col):
|
|
53
|
+
try:
|
|
54
|
+
session = await compose_node(
|
|
55
|
+
node=node,
|
|
56
|
+
linked={
|
|
57
|
+
typing.cast(type, n): col.sessions[name].value for name, n in subnodes.items()
|
|
58
|
+
},
|
|
59
|
+
data=data,
|
|
60
|
+
)
|
|
61
|
+
except ComposeError:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
if node.scope is NodeScope.PER_EVENT:
|
|
65
|
+
node_ctx[node] = session
|
|
66
|
+
elif node.scope is NodeScope.GLOBAL:
|
|
67
|
+
setattr(node, GLOBAL_VALUE_KEY, session.value)
|
|
68
|
+
|
|
69
|
+
return session.value
|
|
70
|
+
|
|
71
|
+
raise ComposeError("Cannot compose either nodes: {}.".format(", ".join(repr(n) for n in cls.nodes)))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if typing.TYPE_CHECKING:
|
|
75
|
+
type Either[Left, Right: typing.Any | None] = Left | Right
|
|
76
|
+
type Optional[Left] = Either[Left, None]
|
|
77
|
+
else:
|
|
78
|
+
Either = _Either
|
|
79
|
+
Optional = type("Optional", (Either,), {})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__all__ = ("Either", "Optional")
|
telegrinder/node/event.py
CHANGED
|
@@ -1,65 +1,54 @@
|
|
|
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.
|
|
9
|
-
from telegrinder.
|
|
10
|
-
from telegrinder.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
dataclass: type[
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
dataclass =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.msgspec_utils import decoder
|
|
9
|
+
from telegrinder.node.base import ComposeError, FactoryNode
|
|
10
|
+
from telegrinder.types.objects import Update
|
|
11
|
+
|
|
12
|
+
if typing.TYPE_CHECKING:
|
|
13
|
+
from _typeshed import DataclassInstance
|
|
14
|
+
|
|
15
|
+
type DataclassType = DataclassInstance | msgspec.Struct | dict[str, typing.Any]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _EventNode(FactoryNode):
|
|
19
|
+
dataclass: type[DataclassType]
|
|
20
|
+
orig_dataclass: type[DataclassType]
|
|
21
|
+
|
|
22
|
+
def __class_getitem__(cls, dataclass: type[DataclassType], /) -> typing.Self:
|
|
23
|
+
return cls(dataclass=dataclass, orig_dataclass=typing.get_origin(dataclass) or dataclass)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def compose(cls, raw_update: Update, api: API) -> DataclassType:
|
|
27
|
+
try:
|
|
28
|
+
if issubclass(cls.orig_dataclass, BaseCute):
|
|
29
|
+
update = raw_update if issubclass(cls.orig_dataclass, Update) else raw_update.incoming_update
|
|
30
|
+
dataclass = cls.orig_dataclass.from_update(update=update, bound_api=api)
|
|
31
|
+
|
|
32
|
+
elif issubclass(cls.orig_dataclass, msgspec.Struct) or dataclasses.is_dataclass(
|
|
33
|
+
cls.orig_dataclass,
|
|
34
|
+
):
|
|
35
|
+
dataclass = decoder.convert(
|
|
36
|
+
obj=raw_update.incoming_update,
|
|
37
|
+
type=cls.dataclass,
|
|
38
|
+
from_attributes=True,
|
|
39
|
+
)
|
|
40
|
+
else:
|
|
41
|
+
dataclass = cls.dataclass(**raw_update.incoming_update.to_full_dict())
|
|
42
|
+
|
|
43
|
+
return dataclass
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
raise ComposeError(f"Cannot parse an update object into {cls.dataclass!r}, error: {str(exc)}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if typing.TYPE_CHECKING:
|
|
49
|
+
type EventNode[Dataclass: DataclassType] = Dataclass
|
|
50
|
+
else:
|
|
51
|
+
EventNode = _EventNode
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = ("EventNode",)
|