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
telegrinder/node/composer.py
CHANGED
|
@@ -5,100 +5,104 @@ import typing
|
|
|
5
5
|
from fntypes.error import UnwrapError
|
|
6
6
|
from fntypes.result import Error, Ok, Result
|
|
7
7
|
|
|
8
|
-
from telegrinder.api.api import API
|
|
9
|
-
from telegrinder.bot.cute_types.update import Update, UpdateCute
|
|
10
8
|
from telegrinder.bot.dispatch.context import Context
|
|
11
9
|
from telegrinder.modules import logger
|
|
12
10
|
from telegrinder.node.base import (
|
|
13
11
|
ComposeError,
|
|
12
|
+
IsNode,
|
|
14
13
|
Name,
|
|
15
|
-
|
|
14
|
+
NodeImpersonation,
|
|
16
15
|
NodeScope,
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
NodeType,
|
|
17
|
+
unwrap_node,
|
|
19
18
|
)
|
|
20
|
-
from telegrinder.tools.magic import magic_bundle
|
|
19
|
+
from telegrinder.tools.magic import join_dicts, magic_bundle
|
|
20
|
+
|
|
21
|
+
type AsyncGenerator = typing.AsyncGenerator[typing.Any, None]
|
|
21
22
|
|
|
22
23
|
CONTEXT_STORE_NODES_KEY = "_node_ctx"
|
|
23
24
|
GLOBAL_VALUE_KEY = "_value"
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
def get_scope(node: type[NodeType], /) -> NodeScope | None:
|
|
28
|
+
return getattr(node, "scope", None)
|
|
29
|
+
|
|
30
|
+
|
|
26
31
|
async def compose_node(
|
|
27
|
-
|
|
28
|
-
linked: dict[type, typing.Any],
|
|
32
|
+
node: type[NodeType],
|
|
33
|
+
linked: dict[type[typing.Any], typing.Any],
|
|
34
|
+
data: dict[type[typing.Any], typing.Any] | None = None,
|
|
29
35
|
) -> "NodeSession":
|
|
30
|
-
|
|
31
|
-
kwargs = magic_bundle(node.compose,
|
|
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))
|
|
32
42
|
|
|
33
43
|
if node.is_generator():
|
|
34
|
-
generator = typing.cast(
|
|
44
|
+
generator = typing.cast(AsyncGenerator, node.compose(**kwargs))
|
|
35
45
|
value = await generator.asend(None)
|
|
36
46
|
else:
|
|
37
47
|
generator = None
|
|
38
|
-
value =
|
|
48
|
+
value = node.compose(**kwargs)
|
|
39
49
|
if inspect.isawaitable(value):
|
|
40
50
|
value = await value
|
|
41
51
|
|
|
42
|
-
return NodeSession(
|
|
52
|
+
return NodeSession(node, value, subnodes={}, generator=generator)
|
|
43
53
|
|
|
44
54
|
|
|
45
55
|
async def compose_nodes(
|
|
46
|
-
nodes:
|
|
56
|
+
nodes: typing.Mapping[str, IsNode | NodeImpersonation],
|
|
47
57
|
ctx: Context,
|
|
48
58
|
data: dict[type[typing.Any], typing.Any] | None = None,
|
|
49
59
|
) -> Result["NodeCollection", ComposeError]:
|
|
50
|
-
logger.debug("Composing nodes: {!r}
|
|
60
|
+
logger.debug("Composing nodes: ({})...", " ".join(f"{k}={v!r}" for k, v in nodes.items()))
|
|
51
61
|
|
|
52
|
-
local_nodes: dict[type[Node], NodeSession]
|
|
53
62
|
data = {Context: ctx} | (data or {})
|
|
54
|
-
parent_nodes
|
|
55
|
-
event_nodes: dict[
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
for (parent_node_name, parent_node_t), linked_nodes in calculation_nodes.items():
|
|
62
|
-
local_nodes = {}
|
|
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]()
|
|
63
69
|
subnodes = {}
|
|
64
70
|
data[Name] = parent_node_name
|
|
65
71
|
|
|
66
72
|
for node_t in linked_nodes:
|
|
67
|
-
scope =
|
|
73
|
+
scope = get_scope(node_t)
|
|
68
74
|
|
|
69
75
|
if scope is NodeScope.PER_EVENT and node_t in event_nodes:
|
|
70
76
|
local_nodes[node_t] = event_nodes[node_t]
|
|
71
77
|
continue
|
|
72
78
|
elif scope is NodeScope.GLOBAL and hasattr(node_t, GLOBAL_VALUE_KEY):
|
|
73
|
-
local_nodes[node_t] = getattr(node_t, GLOBAL_VALUE_KEY)
|
|
79
|
+
local_nodes[node_t] = NodeSession(node_t, getattr(node_t, GLOBAL_VALUE_KEY), {})
|
|
74
80
|
continue
|
|
75
81
|
|
|
76
82
|
subnodes |= {
|
|
77
83
|
k: session.value for k, session in (local_nodes | event_nodes).items() if k not in subnodes
|
|
78
84
|
}
|
|
79
|
-
|
|
80
85
|
try:
|
|
81
|
-
local_nodes[node_t] = await compose_node(node_t, subnodes
|
|
86
|
+
local_nodes[node_t] = await compose_node(node_t, linked=subnodes, data=data)
|
|
82
87
|
except (ComposeError, UnwrapError) as exc:
|
|
83
88
|
for t, local_node in local_nodes.items():
|
|
84
|
-
if t
|
|
89
|
+
if get_scope(t) is NodeScope.PER_CALL:
|
|
85
90
|
await local_node.close()
|
|
86
|
-
return Error(ComposeError(f"Cannot compose {node_t}
|
|
91
|
+
return Error(ComposeError(f"Cannot compose {node_t!r}, error: {str(exc)}"))
|
|
87
92
|
|
|
88
93
|
if scope is NodeScope.PER_EVENT:
|
|
89
94
|
event_nodes[node_t] = local_nodes[node_t]
|
|
90
95
|
elif scope is NodeScope.GLOBAL:
|
|
91
|
-
setattr(node_t, GLOBAL_VALUE_KEY, local_nodes[node_t])
|
|
96
|
+
setattr(node_t, GLOBAL_VALUE_KEY, local_nodes[node_t].value)
|
|
92
97
|
|
|
93
98
|
parent_nodes[parent_node_t] = local_nodes[parent_node_t]
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
return Ok(NodeCollection(node_sessions))
|
|
100
|
+
return Ok(NodeCollection({k: parent_nodes[t] for k, t in unwrapped_nodes}))
|
|
97
101
|
|
|
98
102
|
|
|
99
103
|
@dataclasses.dataclass(slots=True, repr=False)
|
|
100
104
|
class NodeSession:
|
|
101
|
-
node_type: type[
|
|
105
|
+
node_type: type[NodeType] | None
|
|
102
106
|
value: typing.Any
|
|
103
107
|
subnodes: dict[str, typing.Self]
|
|
104
108
|
generator: typing.AsyncGenerator[typing.Any, typing.Any | None] | None = None
|
|
@@ -156,43 +160,4 @@ class NodeCollection:
|
|
|
156
160
|
await session.close(with_value, scopes=scopes)
|
|
157
161
|
|
|
158
162
|
|
|
159
|
-
|
|
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")
|
|
163
|
+
__all__ = ("NodeCollection", "NodeSession", "compose_node", "compose_nodes")
|
telegrinder/node/container.py
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
|
-
from telegrinder.node.base import Node
|
|
3
|
+
from telegrinder.node.base import IsNode, Node
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class ContainerNode(Node):
|
|
7
|
-
linked_nodes: typing.ClassVar[list[
|
|
7
|
+
linked_nodes: typing.ClassVar[list[IsNode]]
|
|
8
|
+
composer: typing.Callable[..., typing.Awaitable[typing.Any]]
|
|
8
9
|
|
|
9
10
|
@classmethod
|
|
10
|
-
def compose(cls, **kw) ->
|
|
11
|
-
|
|
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))
|
|
12
14
|
|
|
13
15
|
@classmethod
|
|
14
|
-
def get_subnodes(cls) -> dict[str,
|
|
16
|
+
def get_subnodes(cls) -> dict[str, IsNode]:
|
|
15
17
|
subnodes = getattr(cls, "subnodes", None)
|
|
16
18
|
if subnodes is None:
|
|
17
19
|
subnodes_dct = {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
|
|
@@ -20,8 +22,12 @@ class ContainerNode(Node):
|
|
|
20
22
|
return subnodes
|
|
21
23
|
|
|
22
24
|
@classmethod
|
|
23
|
-
def link_nodes(
|
|
24
|
-
|
|
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)})
|
|
25
31
|
|
|
26
32
|
|
|
27
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
|
@@ -5,61 +5,50 @@ import msgspec
|
|
|
5
5
|
|
|
6
6
|
from telegrinder.api.api import API
|
|
7
7
|
from telegrinder.bot.cute_types import BaseCute
|
|
8
|
-
from telegrinder.
|
|
9
|
-
from telegrinder.msgspec_utils import DataclassInstance, decoder
|
|
8
|
+
from telegrinder.msgspec_utils import decoder
|
|
10
9
|
from telegrinder.node.base import ComposeError, FactoryNode
|
|
11
|
-
from telegrinder.
|
|
10
|
+
from telegrinder.types.objects import Update
|
|
12
11
|
|
|
13
12
|
if typing.TYPE_CHECKING:
|
|
14
|
-
|
|
13
|
+
from _typeshed import DataclassInstance
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
EVENT_NODE_KEY = "_event_node"
|
|
15
|
+
type DataclassType = DataclassInstance | msgspec.Struct | dict[str, typing.Any]
|
|
19
16
|
|
|
20
17
|
|
|
21
18
|
class _EventNode(FactoryNode):
|
|
22
|
-
dataclass: type[
|
|
19
|
+
dataclass: type[DataclassType]
|
|
20
|
+
orig_dataclass: type[DataclassType]
|
|
23
21
|
|
|
24
|
-
def __class_getitem__(cls, dataclass: type[
|
|
25
|
-
return cls(dataclass=dataclass)
|
|
22
|
+
def __class_getitem__(cls, dataclass: type[DataclassType], /) -> typing.Self:
|
|
23
|
+
return cls(dataclass=dataclass, orig_dataclass=typing.get_origin(dataclass) or dataclass)
|
|
26
24
|
|
|
27
25
|
@classmethod
|
|
28
|
-
def compose(cls, raw_update:
|
|
29
|
-
dataclass_type = typing.get_origin(cls.dataclass) or cls.dataclass
|
|
30
|
-
|
|
26
|
+
def compose(cls, raw_update: Update, api: API) -> DataclassType:
|
|
31
27
|
try:
|
|
32
|
-
if issubclass(
|
|
33
|
-
if
|
|
34
|
-
|
|
35
|
-
else:
|
|
36
|
-
dataclass = dataclass_type.from_update(raw_update.incoming_update, bound_api=api)
|
|
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)
|
|
37
31
|
|
|
38
|
-
elif issubclass(
|
|
39
|
-
|
|
32
|
+
elif issubclass(cls.orig_dataclass, msgspec.Struct) or dataclasses.is_dataclass(
|
|
33
|
+
cls.orig_dataclass,
|
|
40
34
|
):
|
|
41
35
|
dataclass = decoder.convert(
|
|
42
|
-
raw_update.incoming_update
|
|
36
|
+
obj=raw_update.incoming_update,
|
|
43
37
|
type=cls.dataclass,
|
|
44
|
-
|
|
38
|
+
from_attributes=True,
|
|
45
39
|
)
|
|
46
|
-
|
|
47
40
|
else:
|
|
48
|
-
dataclass = cls.dataclass(**raw_update.incoming_update.
|
|
41
|
+
dataclass = cls.dataclass(**raw_update.incoming_update.to_full_dict())
|
|
49
42
|
|
|
50
|
-
ctx[EVENT_NODE_KEY] = cls
|
|
51
43
|
return dataclass
|
|
52
44
|
except Exception as exc:
|
|
53
|
-
raise ComposeError(f"Cannot parse update into {cls.dataclass
|
|
45
|
+
raise ComposeError(f"Cannot parse an update object into {cls.dataclass!r}, error: {str(exc)}")
|
|
54
46
|
|
|
55
47
|
|
|
56
48
|
if typing.TYPE_CHECKING:
|
|
57
|
-
EventNode:
|
|
58
|
-
|
|
49
|
+
type EventNode[Dataclass: DataclassType] = Dataclass
|
|
59
50
|
else:
|
|
60
|
-
|
|
61
|
-
class EventNode(_EventNode):
|
|
62
|
-
pass
|
|
51
|
+
EventNode = _EventNode
|
|
63
52
|
|
|
64
53
|
|
|
65
54
|
__all__ = ("EventNode",)
|
telegrinder/node/file.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
import telegrinder.types as tg_types
|
|
4
|
+
from telegrinder.api.api import API
|
|
5
|
+
from telegrinder.node.attachment import Animation, Audio, Document, Photo, Video, VideoNote, Voice
|
|
6
|
+
from telegrinder.node.base import FactoryNode
|
|
7
|
+
|
|
8
|
+
type Attachment = Animation | Audio | Document | Photo | Video | VideoNote | Voice
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _FileId(FactoryNode):
|
|
12
|
+
attachment_node: type[Attachment]
|
|
13
|
+
|
|
14
|
+
def __class_getitem__(cls, attachment_node: type[Attachment], /):
|
|
15
|
+
return cls(attachment_node=attachment_node)
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def get_subnodes(cls):
|
|
19
|
+
return {"attach": cls.attachment_node}
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def compose(cls, attach: Attachment) -> str:
|
|
23
|
+
if isinstance(attach, Photo):
|
|
24
|
+
return attach.sizes[-1].file_id
|
|
25
|
+
return attach.file_id
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _File(FactoryNode):
|
|
29
|
+
attachment_node: type[Attachment]
|
|
30
|
+
|
|
31
|
+
def __class_getitem__(cls, attachment_node: type[Attachment], /):
|
|
32
|
+
return cls(attachment_node=attachment_node)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def get_subnodes(cls):
|
|
36
|
+
return {"file_id": _FileId[cls.attachment_node]}
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
async def compose(cls, file_id: str, api: API) -> tg_types.File:
|
|
40
|
+
return (await api.get_file(file_id=file_id)).expect("File can't be downloaded.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if typing.TYPE_CHECKING:
|
|
44
|
+
type FileId[T: Attachment] = str
|
|
45
|
+
type File[T: Attachment] = tg_types.File
|
|
46
|
+
else:
|
|
47
|
+
FileId = _FileId
|
|
48
|
+
File = _File
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = ("File", "FileId")
|
telegrinder/node/me.py
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
from telegrinder.api.api import API
|
|
2
|
-
from telegrinder.node.base import ComposeError,
|
|
2
|
+
from telegrinder.node.base import ComposeError, scalar_node
|
|
3
3
|
from telegrinder.node.scope import GLOBAL
|
|
4
4
|
from telegrinder.types.objects import User
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
@scalar_node(scope=GLOBAL)
|
|
8
|
+
class Me:
|
|
10
9
|
@classmethod
|
|
11
10
|
async def compose(cls, api: API) -> User:
|
|
12
11
|
me = await api.get_me()
|
|
13
|
-
return me.expect(ComposeError("Can't complete get_me request"))
|
|
12
|
+
return me.expect(ComposeError("Can't complete api.get_me() request."))
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
__all__ = ("Me",)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from fntypes.result import Error, Ok
|
|
5
|
+
|
|
6
|
+
from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
|
|
7
|
+
from telegrinder.bot.cute_types.message import MessageCute
|
|
8
|
+
from telegrinder.bot.cute_types.pre_checkout_query import PreCheckoutQueryCute
|
|
9
|
+
from telegrinder.node.base import ComposeError, DataNode, FactoryNode, GlobalNode, scalar_node
|
|
10
|
+
from telegrinder.node.polymorphic import Polymorphic, impl
|
|
11
|
+
from telegrinder.tools.callback_data_serilization import ABCDataSerializer, JSONSerializer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@scalar_node[str]
|
|
15
|
+
class Payload(Polymorphic):
|
|
16
|
+
@impl
|
|
17
|
+
def compose_pre_checkout_query(cls, event: PreCheckoutQueryCute) -> str:
|
|
18
|
+
return event.invoice_payload
|
|
19
|
+
|
|
20
|
+
@impl
|
|
21
|
+
def compose_callback_query(cls, event: CallbackQueryCute) -> str:
|
|
22
|
+
return event.data.expect("CallbackQuery has no data.")
|
|
23
|
+
|
|
24
|
+
@impl
|
|
25
|
+
def compose_message(cls, event: MessageCute) -> str:
|
|
26
|
+
return event.successful_payment.map(
|
|
27
|
+
lambda payment: payment.invoice_payload,
|
|
28
|
+
).expect("Message has no successful payment.")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
32
|
+
class PayloadSerializer[T: type[ABCDataSerializer[typing.Any]]](DataNode, GlobalNode[T]):
|
|
33
|
+
serializer: type[ABCDataSerializer[typing.Any]]
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def compose(cls) -> typing.Self:
|
|
37
|
+
return cls(serializer=cls.get(default=JSONSerializer))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _PayloadData(FactoryNode):
|
|
41
|
+
data_type: type[typing.Any]
|
|
42
|
+
serializer: type[ABCDataSerializer[typing.Any]] | None = None
|
|
43
|
+
|
|
44
|
+
def __class_getitem__(
|
|
45
|
+
cls,
|
|
46
|
+
data_type: type[typing.Any] | tuple[type[typing.Any], type[ABCDataSerializer[typing.Any]]],
|
|
47
|
+
/,
|
|
48
|
+
):
|
|
49
|
+
data_type, serializer = (data_type, None) if not isinstance(data_type, tuple) else data_type
|
|
50
|
+
return cls(data_type=data_type, serializer=serializer)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def compose(cls, payload: Payload, payload_serializer: PayloadSerializer) -> typing.Any:
|
|
54
|
+
serializer = cls.serializer or payload_serializer.serializer
|
|
55
|
+
match serializer(cls.data_type).deserialize(payload):
|
|
56
|
+
case Ok(value):
|
|
57
|
+
return value
|
|
58
|
+
case Error(err):
|
|
59
|
+
raise ComposeError(err)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if typing.TYPE_CHECKING:
|
|
63
|
+
import typing_extensions
|
|
64
|
+
|
|
65
|
+
DataType = typing.TypeVar("DataType")
|
|
66
|
+
Serializer = typing_extensions.TypeVar(
|
|
67
|
+
"Serializer",
|
|
68
|
+
bound=ABCDataSerializer,
|
|
69
|
+
default=JSONSerializer[typing.Any],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
type PayloadDataType[DataType, Serializer] = typing.Annotated[DataType, Serializer]
|
|
73
|
+
PayloadData: typing.TypeAlias = PayloadDataType[DataType, Serializer]
|
|
74
|
+
else:
|
|
75
|
+
PayloadData = _PayloadData
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
__all__ = ("Payload", "PayloadData", "PayloadSerializer")
|
telegrinder/node/polymorphic.py
CHANGED
|
@@ -1,25 +1,41 @@
|
|
|
1
|
+
import inspect
|
|
1
2
|
import typing
|
|
2
3
|
|
|
4
|
+
from fntypes.result import Error, Ok
|
|
5
|
+
|
|
6
|
+
from telegrinder.api.api import API
|
|
7
|
+
from telegrinder.bot.cute_types.update import UpdateCute
|
|
3
8
|
from telegrinder.bot.dispatch.context import Context
|
|
4
9
|
from telegrinder.modules import logger
|
|
5
|
-
from telegrinder.node.base import ComposeError, Node
|
|
6
|
-
from telegrinder.node.composer import CONTEXT_STORE_NODES_KEY,
|
|
10
|
+
from telegrinder.node.base import ComposeError, Node, get_nodes
|
|
11
|
+
from telegrinder.node.composer import CONTEXT_STORE_NODES_KEY, NodeSession, compose_nodes
|
|
7
12
|
from telegrinder.node.scope import NodeScope
|
|
8
|
-
from telegrinder.
|
|
9
|
-
from telegrinder.
|
|
13
|
+
from telegrinder.tools.magic import get_impls, impl, magic_bundle
|
|
14
|
+
from telegrinder.types.objects import Update
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
class Polymorphic(Node):
|
|
13
18
|
@classmethod
|
|
14
|
-
async def compose(cls, update:
|
|
19
|
+
async def compose(cls, raw_update: Update, update: UpdateCute, context: Context) -> typing.Any:
|
|
15
20
|
logger.debug(f"Composing polymorphic node {cls.__name__!r}...")
|
|
16
21
|
scope = getattr(cls, "scope", None)
|
|
17
22
|
node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
23
|
+
data = {
|
|
24
|
+
API: update.ctx_api,
|
|
25
|
+
Context: context,
|
|
26
|
+
Update: raw_update,
|
|
27
|
+
}
|
|
18
28
|
|
|
19
29
|
for i, impl_ in enumerate(get_impls(cls)):
|
|
20
30
|
logger.debug("Checking impl {!r}...", impl_.__name__)
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
node_collection = None
|
|
32
|
+
|
|
33
|
+
match await compose_nodes(get_nodes(impl_), context, data=data):
|
|
34
|
+
case Ok(col):
|
|
35
|
+
node_collection = col
|
|
36
|
+
case Error(err):
|
|
37
|
+
logger.debug(f"Composition failed with error: {err!r}")
|
|
38
|
+
|
|
23
39
|
if node_collection is None:
|
|
24
40
|
logger.debug("Impl {!r} composition failed!", impl_.__name__)
|
|
25
41
|
continue
|
|
@@ -34,7 +50,10 @@ class Polymorphic(Node):
|
|
|
34
50
|
await node_collection.close_all()
|
|
35
51
|
return res.value
|
|
36
52
|
|
|
37
|
-
result =
|
|
53
|
+
result = impl_(cls, **node_collection.values | magic_bundle(impl_, data, typebundle=True))
|
|
54
|
+
if inspect.isawaitable(result):
|
|
55
|
+
result = await result
|
|
56
|
+
|
|
38
57
|
if scope is NodeScope.PER_EVENT:
|
|
39
58
|
node_ctx[(cls, i)] = NodeSession(cls, result, {})
|
|
40
59
|
|
telegrinder/node/rule.py
CHANGED
|
@@ -2,9 +2,9 @@ import dataclasses
|
|
|
2
2
|
import importlib
|
|
3
3
|
import typing
|
|
4
4
|
|
|
5
|
+
from telegrinder.bot.cute_types.update import UpdateCute
|
|
5
6
|
from telegrinder.bot.dispatch.context import Context
|
|
6
7
|
from telegrinder.node.base import ComposeError, Node
|
|
7
|
-
from telegrinder.node.update import UpdateNode
|
|
8
8
|
|
|
9
9
|
if typing.TYPE_CHECKING:
|
|
10
10
|
from telegrinder.bot.dispatch.process import check_rule
|
|
@@ -35,7 +35,7 @@ class RuleChain(dict[str, typing.Any], Node):
|
|
|
35
35
|
return dataclasses.dataclass(type(cls_.__name__, (object,), dict(cls_.__dict__)))
|
|
36
36
|
|
|
37
37
|
@classmethod
|
|
38
|
-
async def compose(cls, update:
|
|
38
|
+
async def compose(cls, update: UpdateCute) -> typing.Any:
|
|
39
39
|
# Hack to avoid circular import
|
|
40
40
|
globalns = globals()
|
|
41
41
|
if "check_rule" not in globalns:
|
|
@@ -64,10 +64,6 @@ class RuleChain(dict[str, typing.Any], Node):
|
|
|
64
64
|
def as_node(cls) -> type[typing.Self]:
|
|
65
65
|
return cls
|
|
66
66
|
|
|
67
|
-
@classmethod
|
|
68
|
-
def get_subnodes(cls) -> dict[typing.Literal["update"], type[UpdateNode]]:
|
|
69
|
-
return {"update": UpdateNode}
|
|
70
|
-
|
|
71
67
|
@classmethod
|
|
72
68
|
def is_generator(cls) -> typing.Literal[False]:
|
|
73
69
|
return False
|
telegrinder/node/scope.py
CHANGED
|
@@ -2,9 +2,7 @@ import enum
|
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
4
|
if typing.TYPE_CHECKING:
|
|
5
|
-
from .base import
|
|
6
|
-
|
|
7
|
-
T = typing.TypeVar("T", bound=type["Node"])
|
|
5
|
+
from .base import IsNode
|
|
8
6
|
|
|
9
7
|
|
|
10
8
|
class NodeScope(enum.Enum):
|
|
@@ -18,17 +16,17 @@ PER_CALL = NodeScope.PER_CALL
|
|
|
18
16
|
GLOBAL = NodeScope.GLOBAL
|
|
19
17
|
|
|
20
18
|
|
|
21
|
-
def per_call(node: T) -> T:
|
|
19
|
+
def per_call[T: IsNode](node: type[T]) -> type[T]:
|
|
22
20
|
setattr(node, "scope", PER_CALL)
|
|
23
21
|
return node
|
|
24
22
|
|
|
25
23
|
|
|
26
|
-
def per_event(node: T) -> T:
|
|
24
|
+
def per_event[T: IsNode](node: type[T]) -> type[T]:
|
|
27
25
|
setattr(node, "scope", PER_EVENT)
|
|
28
26
|
return node
|
|
29
27
|
|
|
30
28
|
|
|
31
|
-
def global_node(node: T) -> T:
|
|
29
|
+
def global_node[T: IsNode](node: type[T]) -> type[T]:
|
|
32
30
|
setattr(node, "scope", GLOBAL)
|
|
33
31
|
return node
|
|
34
32
|
|