telegrinder 0.1.dev170__py3-none-any.whl → 0.1.dev171__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/api/abc.py +7 -1
- telegrinder/api/api.py +12 -3
- telegrinder/api/error.py +2 -1
- telegrinder/bot/bot.py +6 -1
- telegrinder/bot/cute_types/base.py +144 -17
- telegrinder/bot/cute_types/callback_query.py +6 -1
- telegrinder/bot/cute_types/chat_member_updated.py +1 -2
- telegrinder/bot/cute_types/message.py +23 -11
- telegrinder/bot/cute_types/update.py +48 -0
- telegrinder/bot/cute_types/utils.py +2 -465
- telegrinder/bot/dispatch/__init__.py +2 -3
- telegrinder/bot/dispatch/abc.py +6 -3
- telegrinder/bot/dispatch/context.py +6 -6
- telegrinder/bot/dispatch/dispatch.py +61 -23
- telegrinder/bot/dispatch/handler/abc.py +2 -2
- telegrinder/bot/dispatch/handler/func.py +36 -17
- telegrinder/bot/dispatch/handler/message_reply.py +2 -2
- telegrinder/bot/dispatch/middleware/abc.py +2 -2
- telegrinder/bot/dispatch/process.py +10 -10
- telegrinder/bot/dispatch/return_manager/abc.py +3 -3
- telegrinder/bot/dispatch/view/abc.py +12 -15
- telegrinder/bot/dispatch/view/box.py +73 -62
- telegrinder/bot/dispatch/view/message.py +11 -3
- telegrinder/bot/dispatch/view/raw.py +3 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +2 -2
- telegrinder/bot/dispatch/waiter_machine/middleware.py +1 -1
- telegrinder/bot/dispatch/waiter_machine/short_state.py +2 -1
- telegrinder/bot/polling/polling.py +3 -3
- telegrinder/bot/rules/abc.py +11 -7
- telegrinder/bot/rules/adapter/event.py +7 -4
- telegrinder/bot/rules/adapter/node.py +1 -1
- telegrinder/bot/rules/command.py +5 -7
- telegrinder/bot/rules/func.py +1 -1
- telegrinder/bot/rules/fuzzy.py +1 -1
- telegrinder/bot/rules/integer.py +1 -2
- telegrinder/bot/rules/markup.py +3 -3
- telegrinder/bot/rules/message_entities.py +1 -1
- telegrinder/bot/rules/node.py +2 -2
- telegrinder/bot/rules/regex.py +1 -1
- telegrinder/bot/rules/rule_enum.py +1 -1
- telegrinder/bot/scenario/checkbox.py +2 -2
- telegrinder/model.py +85 -46
- telegrinder/modules.py +3 -3
- telegrinder/msgspec_utils.py +33 -5
- telegrinder/node/__init__.py +20 -11
- telegrinder/node/attachment.py +19 -16
- telegrinder/node/base.py +120 -24
- telegrinder/node/callback_query.py +5 -9
- telegrinder/node/command.py +6 -2
- telegrinder/node/composer.py +82 -54
- telegrinder/node/container.py +4 -4
- telegrinder/node/event.py +59 -0
- telegrinder/node/me.py +3 -0
- telegrinder/node/message.py +6 -10
- telegrinder/node/polymorphic.py +11 -12
- telegrinder/node/rule.py +27 -5
- telegrinder/node/source.py +10 -11
- telegrinder/node/text.py +4 -4
- telegrinder/node/update.py +1 -2
- telegrinder/py.typed +0 -0
- telegrinder/tools/__init__.py +2 -2
- telegrinder/tools/buttons.py +5 -10
- telegrinder/tools/error_handler/error.py +2 -0
- telegrinder/tools/error_handler/error_handler.py +1 -1
- telegrinder/tools/formatting/spec_html_formats.py +10 -10
- telegrinder/tools/global_context/__init__.py +2 -2
- telegrinder/tools/global_context/global_context.py +2 -2
- telegrinder/tools/global_context/telegrinder_ctx.py +4 -4
- telegrinder/tools/keyboard.py +2 -2
- telegrinder/tools/loop_wrapper/loop_wrapper.py +39 -5
- telegrinder/tools/magic.py +48 -15
- telegrinder/types/enums.py +1 -0
- telegrinder/types/methods.py +14 -5
- telegrinder/types/objects.py +3 -0
- {telegrinder-0.1.dev170.dist-info → telegrinder-0.1.dev171.dist-info}/METADATA +2 -2
- telegrinder-0.1.dev171.dist-info/RECORD +145 -0
- telegrinder-0.1.dev170.dist-info/RECORD +0 -143
- {telegrinder-0.1.dev170.dist-info → telegrinder-0.1.dev171.dist-info}/LICENSE +0 -0
- {telegrinder-0.1.dev170.dist-info → telegrinder-0.1.dev171.dist-info}/WHEEL +0 -0
telegrinder/node/__init__.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from .attachment import Attachment, Audio, Photo, Video
|
|
2
|
-
from .base import ComposeError, DataNode, Node, ScalarNode, is_node
|
|
2
|
+
from .base import BaseNode, ComposeError, DataNode, Node, ScalarNode, is_node, node_impl
|
|
3
|
+
from .callback_query import CallbackQueryNode
|
|
3
4
|
from .command import CommandInfo
|
|
4
5
|
from .composer import Composition, NodeCollection, NodeSession, compose_node, compose_nodes
|
|
5
6
|
from .container import ContainerNode
|
|
7
|
+
from .event import EventNode
|
|
6
8
|
from .me import Me
|
|
7
9
|
from .message import MessageNode
|
|
10
|
+
from .polymorphic import Polymorphic, impl
|
|
8
11
|
from .rule import RuleChain
|
|
9
12
|
from .scope import GLOBAL, PER_CALL, PER_EVENT, NodeScope, global_node, per_call, per_event
|
|
10
13
|
from .source import ChatSource, Source, UserSource
|
|
@@ -15,35 +18,41 @@ from .update import UpdateNode
|
|
|
15
18
|
__all__ = (
|
|
16
19
|
"Attachment",
|
|
17
20
|
"Audio",
|
|
21
|
+
"BaseNode",
|
|
22
|
+
"CallbackQueryNode",
|
|
18
23
|
"ChatSource",
|
|
24
|
+
"CommandInfo",
|
|
19
25
|
"ComposeError",
|
|
26
|
+
"Composition",
|
|
20
27
|
"ContainerNode",
|
|
21
28
|
"DataNode",
|
|
29
|
+
"EventNode",
|
|
30
|
+
"GLOBAL",
|
|
31
|
+
"Me",
|
|
22
32
|
"MessageNode",
|
|
23
33
|
"Node",
|
|
24
34
|
"NodeCollection",
|
|
35
|
+
"NodeScope",
|
|
25
36
|
"NodeSession",
|
|
37
|
+
"PER_CALL",
|
|
38
|
+
"PER_EVENT",
|
|
26
39
|
"Photo",
|
|
40
|
+
"Polymorphic",
|
|
27
41
|
"RuleChain",
|
|
28
42
|
"ScalarNode",
|
|
29
43
|
"Source",
|
|
30
44
|
"Text",
|
|
31
45
|
"TextInteger",
|
|
32
|
-
"UserSource",
|
|
33
46
|
"UpdateNode",
|
|
47
|
+
"UserSource",
|
|
34
48
|
"Video",
|
|
35
49
|
"compose_node",
|
|
50
|
+
"compose_nodes",
|
|
36
51
|
"generate_node",
|
|
37
|
-
"
|
|
52
|
+
"global_node",
|
|
53
|
+
"impl",
|
|
38
54
|
"is_node",
|
|
39
|
-
"
|
|
40
|
-
"NodeScope",
|
|
41
|
-
"PER_CALL",
|
|
42
|
-
"PER_EVENT",
|
|
55
|
+
"node_impl",
|
|
43
56
|
"per_call",
|
|
44
57
|
"per_event",
|
|
45
|
-
"CommandInfo",
|
|
46
|
-
"GLOBAL",
|
|
47
|
-
"global_node",
|
|
48
|
-
"Me",
|
|
49
58
|
)
|
telegrinder/node/attachment.py
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
|
-
from fntypes import Option, Some
|
|
4
|
+
from fntypes.co import Option, Some
|
|
5
5
|
from fntypes.option import Nothing
|
|
6
6
|
|
|
7
7
|
import telegrinder.types
|
|
8
|
+
from telegrinder.node.base import ComposeError, DataNode, ScalarNode
|
|
9
|
+
from telegrinder.node.message import MessageNode
|
|
8
10
|
|
|
9
|
-
from .base import ComposeError, DataNode, ScalarNode
|
|
10
|
-
from .message import MessageNode
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
@dataclasses.dataclass
|
|
12
|
+
@dataclasses.dataclass(slots=True)
|
|
14
13
|
class Attachment(DataNode):
|
|
15
14
|
attachment_type: typing.Literal["audio", "document", "photo", "poll", "video"]
|
|
16
15
|
audio: Option[telegrinder.types.Audio] = dataclasses.field(
|
|
17
|
-
default_factory=lambda: Nothing(),
|
|
16
|
+
default_factory=lambda: Nothing(),
|
|
17
|
+
kw_only=True,
|
|
18
18
|
)
|
|
19
19
|
document: Option[telegrinder.types.Document] = dataclasses.field(
|
|
20
|
-
default_factory=lambda: Nothing(),
|
|
20
|
+
default_factory=lambda: Nothing(),
|
|
21
|
+
kw_only=True,
|
|
21
22
|
)
|
|
22
23
|
photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
|
|
23
|
-
default_factory=lambda: Nothing(),
|
|
24
|
+
default_factory=lambda: Nothing(),
|
|
25
|
+
kw_only=True,
|
|
24
26
|
)
|
|
25
27
|
poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing(), kw_only=True)
|
|
26
28
|
video: Option[telegrinder.types.Video] = dataclasses.field(
|
|
27
|
-
default_factory=lambda: Nothing(),
|
|
29
|
+
default_factory=lambda: Nothing(),
|
|
30
|
+
kw_only=True,
|
|
28
31
|
)
|
|
29
32
|
|
|
30
33
|
@classmethod
|
|
@@ -33,17 +36,17 @@ class Attachment(DataNode):
|
|
|
33
36
|
match getattr(message, attachment_type, Nothing()):
|
|
34
37
|
case Some(attachment):
|
|
35
38
|
return cls(attachment_type, **{attachment_type: Some(attachment)})
|
|
36
|
-
return cls.compose_error("No attachment found in message")
|
|
39
|
+
return cls.compose_error("No attachment found in message.")
|
|
37
40
|
|
|
38
41
|
|
|
39
|
-
@dataclasses.dataclass
|
|
42
|
+
@dataclasses.dataclass(slots=True)
|
|
40
43
|
class Photo(DataNode):
|
|
41
44
|
sizes: list[telegrinder.types.PhotoSize]
|
|
42
45
|
|
|
43
46
|
@classmethod
|
|
44
47
|
async def compose(cls, attachment: Attachment) -> typing.Self:
|
|
45
48
|
if not attachment.photo:
|
|
46
|
-
raise ComposeError("Attachment is not
|
|
49
|
+
raise ComposeError("Attachment is not a photo.")
|
|
47
50
|
return cls(attachment.photo.unwrap())
|
|
48
51
|
|
|
49
52
|
|
|
@@ -51,7 +54,7 @@ class Video(ScalarNode, telegrinder.types.Video):
|
|
|
51
54
|
@classmethod
|
|
52
55
|
async def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
|
|
53
56
|
if not attachment.video:
|
|
54
|
-
raise ComposeError("Attachment is not
|
|
57
|
+
raise ComposeError("Attachment is not a video.")
|
|
55
58
|
return attachment.video.unwrap()
|
|
56
59
|
|
|
57
60
|
|
|
@@ -59,7 +62,7 @@ class Audio(ScalarNode, telegrinder.types.Audio):
|
|
|
59
62
|
@classmethod
|
|
60
63
|
async def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
|
|
61
64
|
if not attachment.audio:
|
|
62
|
-
raise ComposeError("Attachment is not an audio")
|
|
65
|
+
raise ComposeError("Attachment is not an audio.")
|
|
63
66
|
return attachment.audio.unwrap()
|
|
64
67
|
|
|
65
68
|
|
|
@@ -67,7 +70,7 @@ class Document(ScalarNode, telegrinder.types.Document):
|
|
|
67
70
|
@classmethod
|
|
68
71
|
async def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
|
|
69
72
|
if not attachment.document:
|
|
70
|
-
raise ComposeError("Attachment is not
|
|
73
|
+
raise ComposeError("Attachment is not a document.")
|
|
71
74
|
return attachment.document.unwrap()
|
|
72
75
|
|
|
73
76
|
|
|
@@ -75,7 +78,7 @@ class Poll(ScalarNode, telegrinder.types.Poll):
|
|
|
75
78
|
@classmethod
|
|
76
79
|
async def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
|
|
77
80
|
if not attachment.poll:
|
|
78
|
-
raise ComposeError("Attachment is not
|
|
81
|
+
raise ComposeError("Attachment is not a poll.")
|
|
79
82
|
return attachment.poll.unwrap()
|
|
80
83
|
|
|
81
84
|
|
telegrinder/node/base.py
CHANGED
|
@@ -1,19 +1,70 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import inspect
|
|
3
3
|
import typing
|
|
4
|
+
from types import AsyncGeneratorType
|
|
5
|
+
|
|
6
|
+
from telegrinder.api.api import API
|
|
7
|
+
from telegrinder.bot.cute_types.update import UpdateCute
|
|
8
|
+
from telegrinder.bot.dispatch.context import Context
|
|
9
|
+
from telegrinder.node.scope import NodeScope
|
|
10
|
+
from telegrinder.tools.magic import (
|
|
11
|
+
NODE_IMPL_MARK,
|
|
12
|
+
cache_magic_value,
|
|
13
|
+
get_annotations,
|
|
14
|
+
get_impls_by_key,
|
|
15
|
+
magic_bundle,
|
|
16
|
+
node_impl,
|
|
17
|
+
)
|
|
4
18
|
|
|
5
|
-
|
|
19
|
+
ComposeResult: typing.TypeAlias = typing.Awaitable[typing.Any] | typing.AsyncGenerator[typing.Any, None]
|
|
6
20
|
|
|
7
|
-
from .scope import NodeScope
|
|
8
21
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
)
|
|
22
|
+
def is_node(maybe_node: type[typing.Any]) -> typing.TypeGuard[type["Node"]]:
|
|
23
|
+
maybe_node = typing.get_origin(maybe_node) or maybe_node
|
|
24
|
+
return (
|
|
25
|
+
isinstance(maybe_node, type)
|
|
26
|
+
and issubclass(maybe_node, Node)
|
|
27
|
+
or isinstance(maybe_node, Node)
|
|
28
|
+
or hasattr(maybe_node, "as_node")
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@cache_magic_value("__compose_annotations__")
|
|
33
|
+
def get_compose_annotations(function: typing.Callable[..., typing.Any]) -> dict[str, typing.Any]:
|
|
34
|
+
return {k: v for k, v in get_annotations(function).items() if not is_node(v)}
|
|
35
|
+
|
|
14
36
|
|
|
37
|
+
@cache_magic_value("__nodes__")
|
|
38
|
+
def get_nodes(function: typing.Callable[..., typing.Any]) -> dict[str, type["Node"]]:
|
|
39
|
+
return {k: v for k, v in get_annotations(function).items() if is_node(v)}
|
|
15
40
|
|
|
16
|
-
|
|
41
|
+
|
|
42
|
+
@cache_magic_value("__is_generator__")
|
|
43
|
+
def is_generator(function: typing.Callable[..., typing.Any]) -> typing.TypeGuard[AsyncGeneratorType[typing.Any, None]]:
|
|
44
|
+
return inspect.isasyncgenfunction(function)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_node_impls(node_cls: type["Node"]) -> dict[str, typing.Any]:
|
|
48
|
+
if not hasattr(node_cls, "__node_impls__"):
|
|
49
|
+
impls = get_impls_by_key(node_cls, NODE_IMPL_MARK)
|
|
50
|
+
if issubclass(node_cls, BaseNode):
|
|
51
|
+
impls |= get_impls_by_key(BaseNode, NODE_IMPL_MARK)
|
|
52
|
+
setattr(node_cls, "__node_impls__", impls)
|
|
53
|
+
return getattr(node_cls, "__node_impls__")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_node_impl(
|
|
57
|
+
node: type[typing.Any],
|
|
58
|
+
node_impls: dict[str, typing.Callable[..., typing.Any]],
|
|
59
|
+
) -> typing.Callable[..., typing.Any] | None:
|
|
60
|
+
for n_impl in node_impls.values():
|
|
61
|
+
if "return" in n_impl.__annotations__ and node is n_impl.__annotations__["return"]:
|
|
62
|
+
return n_impl
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ComposeError(BaseException):
|
|
67
|
+
pass
|
|
17
68
|
|
|
18
69
|
|
|
19
70
|
class Node(abc.ABC):
|
|
@@ -25,13 +76,46 @@ class Node(abc.ABC):
|
|
|
25
76
|
def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
26
77
|
pass
|
|
27
78
|
|
|
79
|
+
@classmethod
|
|
80
|
+
async def compose_annotation(
|
|
81
|
+
cls,
|
|
82
|
+
annotation: typing.Any,
|
|
83
|
+
update: UpdateCute,
|
|
84
|
+
ctx: Context,
|
|
85
|
+
) -> typing.Any:
|
|
86
|
+
orig_annotation: type[typing.Any] = typing.get_origin(annotation) or annotation
|
|
87
|
+
n_impl = get_node_impl(orig_annotation, cls.get_node_impls())
|
|
88
|
+
if n_impl is None:
|
|
89
|
+
raise ComposeError(f"Node implementation for {orig_annotation!r} not found.")
|
|
90
|
+
|
|
91
|
+
result = n_impl(
|
|
92
|
+
cls,
|
|
93
|
+
**magic_bundle(
|
|
94
|
+
n_impl,
|
|
95
|
+
{"update": update, "context": ctx},
|
|
96
|
+
start_idx=0,
|
|
97
|
+
bundle_ctx=False,
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
if inspect.isawaitable(result):
|
|
101
|
+
return await result
|
|
102
|
+
return result
|
|
103
|
+
|
|
28
104
|
@classmethod
|
|
29
105
|
def compose_error(cls, error: str | None = None) -> typing.NoReturn:
|
|
30
106
|
raise ComposeError(error)
|
|
31
107
|
|
|
32
108
|
@classmethod
|
|
33
|
-
def get_sub_nodes(cls) -> dict[str, type[
|
|
34
|
-
return
|
|
109
|
+
def get_sub_nodes(cls) -> dict[str, type["Node"]]:
|
|
110
|
+
return get_nodes(cls.compose)
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def get_compose_annotations(cls) -> dict[str, typing.Any]:
|
|
114
|
+
return get_compose_annotations(cls.compose)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def get_node_impls(cls) -> dict[str, typing.Callable[..., typing.Any]]:
|
|
118
|
+
return get_node_impls(cls)
|
|
35
119
|
|
|
36
120
|
@classmethod
|
|
37
121
|
def as_node(cls) -> type[typing.Self]:
|
|
@@ -39,10 +123,29 @@ class Node(abc.ABC):
|
|
|
39
123
|
|
|
40
124
|
@classmethod
|
|
41
125
|
def is_generator(cls) -> bool:
|
|
42
|
-
return
|
|
126
|
+
return is_generator(cls.compose)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class BaseNode(Node, abc.ABC):
|
|
130
|
+
@classmethod
|
|
131
|
+
@abc.abstractmethod
|
|
132
|
+
def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
@node_impl
|
|
136
|
+
def compose_api(cls, update: UpdateCute) -> API:
|
|
137
|
+
return update.ctx_api
|
|
138
|
+
|
|
139
|
+
@node_impl
|
|
140
|
+
def compose_context(cls, context: Context) -> Context:
|
|
141
|
+
return context
|
|
43
142
|
|
|
143
|
+
@node_impl
|
|
144
|
+
def compose_update(cls, update: UpdateCute) -> UpdateCute:
|
|
145
|
+
return update
|
|
44
146
|
|
|
45
|
-
|
|
147
|
+
|
|
148
|
+
class DataNode(BaseNode, abc.ABC):
|
|
46
149
|
node = "data"
|
|
47
150
|
|
|
48
151
|
@typing.dataclass_transform()
|
|
@@ -52,7 +155,7 @@ class DataNode(Node, abc.ABC):
|
|
|
52
155
|
pass
|
|
53
156
|
|
|
54
157
|
|
|
55
|
-
class ScalarNodeProto(
|
|
158
|
+
class ScalarNodeProto(BaseNode, abc.ABC):
|
|
56
159
|
@classmethod
|
|
57
160
|
@abc.abstractmethod
|
|
58
161
|
async def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
@@ -68,7 +171,6 @@ if typing.TYPE_CHECKING:
|
|
|
68
171
|
pass
|
|
69
172
|
|
|
70
173
|
else:
|
|
71
|
-
|
|
72
174
|
def create_node(cls, bases, dct):
|
|
73
175
|
dct.update(cls.__dict__)
|
|
74
176
|
return type(cls.__name__, bases, dct)
|
|
@@ -87,21 +189,15 @@ else:
|
|
|
87
189
|
pass
|
|
88
190
|
|
|
89
191
|
|
|
90
|
-
def is_node(maybe_node: type[typing.Any]) -> typing.TypeGuard[type[Node]]:
|
|
91
|
-
maybe_node = typing.get_origin(maybe_node) or maybe_node
|
|
92
|
-
return (
|
|
93
|
-
isinstance(maybe_node, type)
|
|
94
|
-
and issubclass(maybe_node, Node)
|
|
95
|
-
or isinstance(maybe_node, Node)
|
|
96
|
-
or hasattr(maybe_node, "as_node")
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
|
|
100
192
|
__all__ = (
|
|
193
|
+
"BaseNode",
|
|
101
194
|
"ComposeError",
|
|
102
195
|
"DataNode",
|
|
103
196
|
"Node",
|
|
104
197
|
"SCALAR_NODE",
|
|
105
198
|
"ScalarNode",
|
|
199
|
+
"ScalarNodeProto",
|
|
200
|
+
"get_compose_annotations",
|
|
201
|
+
"get_nodes",
|
|
106
202
|
"is_node",
|
|
107
203
|
)
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
from telegrinder.bot.cute_types import CallbackQueryCute
|
|
2
|
-
|
|
3
|
-
from .
|
|
4
|
-
from .update import UpdateNode
|
|
1
|
+
from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
|
|
2
|
+
from telegrinder.node.base import ComposeError, ScalarNode
|
|
3
|
+
from telegrinder.node.update import UpdateNode
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
class CallbackQueryNode(ScalarNode, CallbackQueryCute):
|
|
8
7
|
@classmethod
|
|
9
8
|
async def compose(cls, update: UpdateNode) -> CallbackQueryCute:
|
|
10
9
|
if not update.callback_query:
|
|
11
|
-
raise ComposeError
|
|
12
|
-
return
|
|
13
|
-
**update.callback_query.unwrap().to_dict(),
|
|
14
|
-
api=update.api,
|
|
15
|
-
)
|
|
10
|
+
raise ComposeError("Update is not a callback_query.")
|
|
11
|
+
return update.callback_query.unwrap()
|
|
16
12
|
|
|
17
13
|
|
|
18
14
|
__all__ = ("CallbackQueryNode",)
|
telegrinder/node/command.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import typing
|
|
1
2
|
from dataclasses import dataclass, field
|
|
2
3
|
|
|
3
4
|
from fntypes import Nothing, Option, Some
|
|
@@ -16,14 +17,17 @@ def cut_mention(text: str) -> tuple[str, Option[str]]:
|
|
|
16
17
|
return left, Some(right) if right else Nothing()
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
@dataclass
|
|
20
|
+
@dataclass(slots=True)
|
|
20
21
|
class CommandInfo(DataNode):
|
|
21
22
|
name: str
|
|
22
23
|
arguments: str
|
|
23
24
|
mention: Option[str] = field(default_factory=Nothing)
|
|
24
25
|
|
|
25
26
|
@classmethod
|
|
26
|
-
async def compose(cls, text: Text):
|
|
27
|
+
async def compose(cls, text: Text) -> typing.Self:
|
|
27
28
|
name, arguments = single_split(text, separator=" ")
|
|
28
29
|
name, mention = cut_mention(name)
|
|
29
30
|
return cls(name, arguments, mention)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ("CommandInfo", "cut_mention", "single_split")
|
telegrinder/node/composer.py
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
+
import dataclasses
|
|
1
2
|
import typing
|
|
2
3
|
|
|
3
|
-
from
|
|
4
|
-
|
|
4
|
+
from fntypes.error import UnwrapError
|
|
5
|
+
|
|
6
|
+
from telegrinder.bot.cute_types.update import UpdateCute
|
|
5
7
|
from telegrinder.bot.dispatch.context import Context
|
|
6
|
-
from telegrinder.
|
|
7
|
-
from telegrinder.node.
|
|
8
|
-
|
|
8
|
+
from telegrinder.modules import logger
|
|
9
|
+
from telegrinder.node.base import (
|
|
10
|
+
BaseNode,
|
|
11
|
+
ComposeError,
|
|
12
|
+
Node,
|
|
13
|
+
NodeScope,
|
|
14
|
+
get_compose_annotations,
|
|
15
|
+
get_nodes,
|
|
16
|
+
)
|
|
17
|
+
from telegrinder.tools.magic import magic_bundle
|
|
9
18
|
|
|
10
19
|
CONTEXT_STORE_NODES_KEY = "node_ctx"
|
|
11
20
|
|
|
@@ -22,80 +31,86 @@ async def compose_node(
|
|
|
22
31
|
for name, subnode in node.get_sub_nodes().items():
|
|
23
32
|
if subnode in node_ctx:
|
|
24
33
|
context.sessions[name] = node_ctx[subnode]
|
|
25
|
-
elif subnode is UpdateCute:
|
|
26
|
-
context.sessions[name] = NodeSession(None, update, {})
|
|
27
|
-
elif subnode is API:
|
|
28
|
-
context.sessions[name] = NodeSession(None, update.ctx_api, {})
|
|
29
|
-
elif subnode is Context:
|
|
30
|
-
context.sessions[name] = NodeSession(None, ctx, {})
|
|
31
34
|
else:
|
|
32
35
|
context.sessions[name] = await compose_node(subnode, update, ctx)
|
|
33
36
|
|
|
34
37
|
if getattr(subnode, "scope", None) is NodeScope.PER_EVENT:
|
|
35
38
|
node_ctx[subnode] = context.sessions[name]
|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
for name, annotation in node.get_compose_annotations().items():
|
|
41
|
+
context.sessions[name] = NodeSession(
|
|
42
|
+
None,
|
|
43
|
+
await node.compose_annotation(annotation, update, ctx),
|
|
44
|
+
{},
|
|
45
|
+
)
|
|
38
46
|
|
|
39
47
|
if node.is_generator():
|
|
40
|
-
generator = typing.cast(typing.AsyncGenerator, node.compose(**context.values()))
|
|
48
|
+
generator = typing.cast(typing.AsyncGenerator[typing.Any, None], node.compose(**context.values()))
|
|
41
49
|
value = await generator.asend(None)
|
|
42
50
|
else:
|
|
43
51
|
generator = None
|
|
44
|
-
value = await node.compose(**context.values())
|
|
52
|
+
value = await typing.cast(typing.Awaitable[typing.Any], node.compose(**context.values()))
|
|
45
53
|
|
|
46
54
|
return NodeSession(_node, value, context.sessions, generator)
|
|
47
55
|
|
|
48
56
|
|
|
49
57
|
async def compose_nodes(
|
|
50
|
-
node_types: dict[str, type[Node]],
|
|
51
58
|
update: UpdateCute,
|
|
52
59
|
ctx: Context,
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
nodes: dict[str, type[Node]],
|
|
61
|
+
node_class: type[Node] | None = None,
|
|
62
|
+
context_annotations: dict[str, typing.Any] | None = None,
|
|
63
|
+
) -> "NodeCollection | None":
|
|
64
|
+
node_sessions: dict[str, NodeSession] = {}
|
|
55
65
|
node_ctx: dict[type[Node], "NodeSession"] = ctx.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
56
66
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
try:
|
|
68
|
+
for name, node_t in nodes.items():
|
|
69
|
+
scope = getattr(node_t, "scope", None)
|
|
70
|
+
|
|
60
71
|
if scope is NodeScope.PER_EVENT and node_t in node_ctx:
|
|
61
|
-
|
|
72
|
+
node_sessions[name] = node_ctx[node_t]
|
|
62
73
|
continue
|
|
63
74
|
elif scope is NodeScope.GLOBAL and hasattr(node_t, "_value"):
|
|
64
|
-
|
|
75
|
+
node_sessions[name] = getattr(node_t, "_value")
|
|
65
76
|
continue
|
|
66
77
|
|
|
67
|
-
|
|
68
|
-
node_t,
|
|
69
|
-
update,
|
|
70
|
-
ctx,
|
|
71
|
-
)
|
|
78
|
+
node_sessions[name] = await compose_node(node_t, update, ctx)
|
|
72
79
|
|
|
73
80
|
if scope is NodeScope.PER_EVENT:
|
|
74
|
-
node_ctx[node_t] =
|
|
81
|
+
node_ctx[node_t] = node_sessions[name]
|
|
75
82
|
elif scope is NodeScope.GLOBAL:
|
|
76
|
-
setattr(node_t, "_value",
|
|
83
|
+
setattr(node_t, "_value", node_sessions[name])
|
|
84
|
+
except (ComposeError, UnwrapError) as exc:
|
|
85
|
+
logger.debug(f"Composing node (name={name!r}, node_class={node_t!r}) failed with error: {str(exc)!r}")
|
|
86
|
+
await NodeCollection(node_sessions).close_all()
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
if context_annotations:
|
|
90
|
+
node_class = node_class or BaseNode
|
|
77
91
|
|
|
78
|
-
|
|
79
|
-
|
|
92
|
+
try:
|
|
93
|
+
for name, annotation in context_annotations.items():
|
|
94
|
+
node_sessions[name] = await node_class.compose_annotation(annotation, update, ctx)
|
|
95
|
+
except (ComposeError, UnwrapError) as exc:
|
|
96
|
+
logger.debug(
|
|
97
|
+
f"Composing context annotation (name={name!r}, annotation={annotation!r}) failed with error: {str(exc)!r}",
|
|
98
|
+
)
|
|
99
|
+
await NodeCollection(node_sessions).close_all()
|
|
80
100
|
return None
|
|
81
|
-
|
|
101
|
+
|
|
102
|
+
return NodeCollection(node_sessions)
|
|
82
103
|
|
|
83
104
|
|
|
105
|
+
@dataclasses.dataclass(slots=True, repr=False)
|
|
84
106
|
class NodeSession:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
subnodes: dict[str, typing.Self],
|
|
90
|
-
generator: typing.AsyncGenerator[typing.Any, None] | None = None,
|
|
91
|
-
):
|
|
92
|
-
self.node_type = node_type
|
|
93
|
-
self.value = value
|
|
94
|
-
self.subnodes = subnodes
|
|
95
|
-
self.generator = generator
|
|
107
|
+
node_type: type[Node] | None
|
|
108
|
+
value: typing.Any
|
|
109
|
+
subnodes: dict[str, typing.Self]
|
|
110
|
+
generator: typing.AsyncGenerator[typing.Any, None] | None = None
|
|
96
111
|
|
|
97
112
|
def __repr__(self) -> str:
|
|
98
|
-
return f"<{self.__class__.__name__}: {self.value}" + ("ACTIVE>" if self.generator else ">")
|
|
113
|
+
return f"<{self.__class__.__name__}: {self.value!r}" + (" ACTIVE>" if self.generator else ">")
|
|
99
114
|
|
|
100
115
|
async def close(
|
|
101
116
|
self,
|
|
@@ -117,11 +132,13 @@ class NodeSession:
|
|
|
117
132
|
|
|
118
133
|
|
|
119
134
|
class NodeCollection:
|
|
135
|
+
__slots__ = ("sessions",)
|
|
136
|
+
|
|
120
137
|
def __init__(self, sessions: dict[str, NodeSession]) -> None:
|
|
121
138
|
self.sessions = sessions
|
|
122
139
|
|
|
123
140
|
def __repr__(self) -> str:
|
|
124
|
-
return "<{}: sessions={}>".format(self.__class__.__name__, self.sessions)
|
|
141
|
+
return "<{}: sessions={!r}>".format(self.__class__.__name__, self.sessions)
|
|
125
142
|
|
|
126
143
|
def values(self) -> dict[str, typing.Any]:
|
|
127
144
|
return {name: session.value for name, session in self.sessions.items()}
|
|
@@ -135,26 +152,37 @@ class NodeCollection:
|
|
|
135
152
|
await session.close(with_value, scopes=scopes)
|
|
136
153
|
|
|
137
154
|
|
|
155
|
+
@dataclasses.dataclass(slots=True, repr=False)
|
|
138
156
|
class Composition:
|
|
139
|
-
|
|
157
|
+
func: typing.Callable[..., typing.Any]
|
|
158
|
+
is_blocking: bool
|
|
159
|
+
node_class: type[Node] = dataclasses.field(default_factory=lambda: BaseNode)
|
|
160
|
+
nodes: dict[str, type[Node]] = dataclasses.field(init=False)
|
|
161
|
+
context_annotations: dict[str, typing.Any] = dataclasses.field(init=False)
|
|
140
162
|
|
|
141
|
-
def
|
|
142
|
-
self.
|
|
143
|
-
self.
|
|
144
|
-
self.is_blocking = is_blocking
|
|
163
|
+
def __post_init__(self) -> None:
|
|
164
|
+
self.nodes = get_nodes(self.func)
|
|
165
|
+
self.context_annotations = get_compose_annotations(self.func)
|
|
145
166
|
|
|
146
167
|
def __repr__(self) -> str:
|
|
147
|
-
return "<{}: for function={!r} with nodes={}>".format(
|
|
168
|
+
return "<{}: for function={!r} with nodes={!r}, context_annotations={!r}>".format(
|
|
148
169
|
("blocking " if self.is_blocking else "") + self.__class__.__name__,
|
|
149
|
-
self.func.
|
|
170
|
+
self.func.__qualname__,
|
|
150
171
|
self.nodes,
|
|
172
|
+
self.context_annotations,
|
|
151
173
|
)
|
|
152
174
|
|
|
153
175
|
async def compose_nodes(self, update: UpdateCute, context: Context) -> NodeCollection | None:
|
|
154
|
-
return await compose_nodes(
|
|
176
|
+
return await compose_nodes(
|
|
177
|
+
update=update,
|
|
178
|
+
ctx=context,
|
|
179
|
+
nodes=self.nodes,
|
|
180
|
+
node_class=self.node_class,
|
|
181
|
+
context_annotations=self.context_annotations,
|
|
182
|
+
)
|
|
155
183
|
|
|
156
184
|
async def __call__(self, **kwargs: typing.Any) -> typing.Any:
|
|
157
185
|
return await self.func(**magic_bundle(self.func, kwargs, start_idx=0, bundle_ctx=False)) # type: ignore
|
|
158
186
|
|
|
159
187
|
|
|
160
|
-
__all__ = ("
|
|
188
|
+
__all__ = ("Composition", "NodeCollection", "NodeSession", "compose_node", "compose_nodes")
|
telegrinder/node/container.py
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
|
-
from .base import Node
|
|
3
|
+
from telegrinder.node.base import BaseNode, Node
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
class ContainerNode(
|
|
6
|
+
class ContainerNode(BaseNode):
|
|
7
7
|
linked_nodes: typing.ClassVar[list[type[Node]]]
|
|
8
8
|
|
|
9
9
|
@classmethod
|
|
10
|
-
async def compose(cls, **kw) -> tuple[
|
|
10
|
+
async def compose(cls, **kw) -> tuple[Node, ...]:
|
|
11
11
|
return tuple(t[1] for t in sorted(kw.items(), key=lambda t: t[0]))
|
|
12
12
|
|
|
13
13
|
@classmethod
|
|
14
|
-
def get_sub_nodes(cls) -> dict[str, type[
|
|
14
|
+
def get_sub_nodes(cls) -> dict[str, type[Node]]:
|
|
15
15
|
return {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
|
|
16
16
|
|
|
17
17
|
@classmethod
|