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/attachment.py
CHANGED
|
@@ -1,42 +1,74 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
|
-
from fntypes.
|
|
5
|
-
from fntypes.option import Nothing
|
|
4
|
+
from fntypes.option import Nothing, Option, Some
|
|
6
5
|
|
|
7
6
|
import telegrinder.types
|
|
8
|
-
from telegrinder.
|
|
9
|
-
from telegrinder.node.
|
|
7
|
+
from telegrinder.bot.cute_types.message import MessageCute
|
|
8
|
+
from telegrinder.node.base import ComposeError, DataNode, scalar_node
|
|
9
|
+
|
|
10
|
+
type AttachmentType = typing.Literal[
|
|
11
|
+
"audio",
|
|
12
|
+
"animation",
|
|
13
|
+
"document",
|
|
14
|
+
"photo",
|
|
15
|
+
"poll",
|
|
16
|
+
"voice",
|
|
17
|
+
"video",
|
|
18
|
+
"video_note",
|
|
19
|
+
"successful_payment",
|
|
20
|
+
]
|
|
10
21
|
|
|
11
22
|
|
|
12
23
|
@dataclasses.dataclass(slots=True)
|
|
13
24
|
class Attachment(DataNode):
|
|
14
|
-
attachment_type:
|
|
25
|
+
attachment_type: AttachmentType
|
|
26
|
+
|
|
27
|
+
animation: Option[telegrinder.types.Animation] = dataclasses.field(
|
|
28
|
+
default_factory=Nothing,
|
|
29
|
+
kw_only=True,
|
|
30
|
+
)
|
|
15
31
|
audio: Option[telegrinder.types.Audio] = dataclasses.field(
|
|
16
|
-
default_factory=
|
|
32
|
+
default_factory=Nothing,
|
|
17
33
|
kw_only=True,
|
|
18
34
|
)
|
|
19
35
|
document: Option[telegrinder.types.Document] = dataclasses.field(
|
|
20
|
-
default_factory=
|
|
36
|
+
default_factory=Nothing,
|
|
21
37
|
kw_only=True,
|
|
22
38
|
)
|
|
23
39
|
photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
|
|
24
|
-
default_factory=
|
|
40
|
+
default_factory=Nothing,
|
|
25
41
|
kw_only=True,
|
|
26
42
|
)
|
|
27
43
|
poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing(), kw_only=True)
|
|
44
|
+
voice: Option[telegrinder.types.Voice] = dataclasses.field(
|
|
45
|
+
default_factory=Nothing,
|
|
46
|
+
kw_only=True,
|
|
47
|
+
)
|
|
28
48
|
video: Option[telegrinder.types.Video] = dataclasses.field(
|
|
29
|
-
default_factory=
|
|
49
|
+
default_factory=Nothing,
|
|
50
|
+
kw_only=True,
|
|
51
|
+
)
|
|
52
|
+
video_note: Option[telegrinder.types.VideoNote] = dataclasses.field(
|
|
53
|
+
default_factory=Nothing,
|
|
54
|
+
kw_only=True,
|
|
55
|
+
)
|
|
56
|
+
successful_payment: Option[telegrinder.types.SuccessfulPayment] = dataclasses.field(
|
|
57
|
+
default_factory=Nothing,
|
|
30
58
|
kw_only=True,
|
|
31
59
|
)
|
|
32
60
|
|
|
33
61
|
@classmethod
|
|
34
|
-
def
|
|
35
|
-
|
|
62
|
+
def get_attachment_types(cls) -> tuple[AttachmentType, ...]:
|
|
63
|
+
return typing.get_args(AttachmentType.__value__)
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def compose(cls, message: MessageCute) -> typing.Self:
|
|
67
|
+
for attachment_type in cls.get_attachment_types():
|
|
36
68
|
match getattr(message, attachment_type, Nothing()):
|
|
37
69
|
case Some(attachment):
|
|
38
70
|
return cls(attachment_type, **{attachment_type: Some(attachment)})
|
|
39
|
-
|
|
71
|
+
raise ComposeError("No attachment found in message.")
|
|
40
72
|
|
|
41
73
|
|
|
42
74
|
@dataclasses.dataclass(slots=True)
|
|
@@ -50,7 +82,8 @@ class Photo(DataNode):
|
|
|
50
82
|
return cls(attachment.photo.unwrap())
|
|
51
83
|
|
|
52
84
|
|
|
53
|
-
|
|
85
|
+
@scalar_node
|
|
86
|
+
class Video:
|
|
54
87
|
@classmethod
|
|
55
88
|
def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
|
|
56
89
|
if not attachment.video:
|
|
@@ -58,7 +91,17 @@ class Video(ScalarNode, telegrinder.types.Video):
|
|
|
58
91
|
return attachment.video.unwrap()
|
|
59
92
|
|
|
60
93
|
|
|
61
|
-
|
|
94
|
+
@scalar_node
|
|
95
|
+
class VideoNote:
|
|
96
|
+
@classmethod
|
|
97
|
+
def compose(cls, attachment: Attachment) -> telegrinder.types.VideoNote:
|
|
98
|
+
if not attachment.video_note:
|
|
99
|
+
raise ComposeError("Attachment is not a video note.")
|
|
100
|
+
return attachment.video_note.unwrap()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@scalar_node
|
|
104
|
+
class Audio:
|
|
62
105
|
@classmethod
|
|
63
106
|
def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
|
|
64
107
|
if not attachment.audio:
|
|
@@ -66,7 +109,26 @@ class Audio(ScalarNode, telegrinder.types.Audio):
|
|
|
66
109
|
return attachment.audio.unwrap()
|
|
67
110
|
|
|
68
111
|
|
|
69
|
-
|
|
112
|
+
@scalar_node
|
|
113
|
+
class Animation:
|
|
114
|
+
@classmethod
|
|
115
|
+
def compose(cls, attachment: Attachment) -> telegrinder.types.Animation:
|
|
116
|
+
if not attachment.animation:
|
|
117
|
+
raise ComposeError("Attachment is not an animation.")
|
|
118
|
+
return attachment.animation.unwrap()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@scalar_node
|
|
122
|
+
class Voice:
|
|
123
|
+
@classmethod
|
|
124
|
+
def compose(cls, attachment: Attachment) -> telegrinder.types.Voice:
|
|
125
|
+
if not attachment.voice:
|
|
126
|
+
raise ComposeError("Attachment is not a voice.")
|
|
127
|
+
return attachment.voice.unwrap()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@scalar_node
|
|
131
|
+
class Document:
|
|
70
132
|
@classmethod
|
|
71
133
|
def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
|
|
72
134
|
if not attachment.document:
|
|
@@ -74,7 +136,8 @@ class Document(ScalarNode, telegrinder.types.Document):
|
|
|
74
136
|
return attachment.document.unwrap()
|
|
75
137
|
|
|
76
138
|
|
|
77
|
-
|
|
139
|
+
@scalar_node
|
|
140
|
+
class Poll:
|
|
78
141
|
@classmethod
|
|
79
142
|
def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
|
|
80
143
|
if not attachment.poll:
|
|
@@ -82,11 +145,24 @@ class Poll(ScalarNode, telegrinder.types.Poll):
|
|
|
82
145
|
return attachment.poll.unwrap()
|
|
83
146
|
|
|
84
147
|
|
|
148
|
+
@scalar_node
|
|
149
|
+
class SuccessfulPayment:
|
|
150
|
+
@classmethod
|
|
151
|
+
def compose(cls, attachment: Attachment) -> telegrinder.types.SuccessfulPayment:
|
|
152
|
+
if not attachment.successful_payment:
|
|
153
|
+
raise ComposeError("Attachment is not a successful payment.")
|
|
154
|
+
return attachment.successful_payment.unwrap()
|
|
155
|
+
|
|
156
|
+
|
|
85
157
|
__all__ = (
|
|
158
|
+
"Animation",
|
|
86
159
|
"Attachment",
|
|
87
160
|
"Audio",
|
|
88
161
|
"Document",
|
|
89
162
|
"Photo",
|
|
90
163
|
"Poll",
|
|
164
|
+
"SuccessfulPayment",
|
|
91
165
|
"Video",
|
|
166
|
+
"VideoNote",
|
|
167
|
+
"Voice",
|
|
92
168
|
)
|
telegrinder/node/base.py
CHANGED
|
@@ -1,57 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import abc
|
|
2
4
|
import inspect
|
|
3
|
-
from
|
|
5
|
+
from collections import deque
|
|
6
|
+
from types import AsyncGeneratorType, CodeType, resolve_bases
|
|
4
7
|
|
|
5
8
|
import typing_extensions as typing
|
|
6
9
|
|
|
7
10
|
from telegrinder.node.scope import NodeScope
|
|
8
11
|
from telegrinder.tools.magic import cache_magic_value, get_annotations
|
|
12
|
+
from telegrinder.tools.strings import to_pascal_case
|
|
13
|
+
|
|
14
|
+
if typing.TYPE_CHECKING:
|
|
15
|
+
from telegrinder.node.tools.generator import generate_node
|
|
16
|
+
else:
|
|
17
|
+
|
|
18
|
+
def generate_node(*args, **kwargs):
|
|
19
|
+
globalns = globals()
|
|
20
|
+
if "__generate_node" not in globalns:
|
|
21
|
+
import telegrinder.node.tools.generator
|
|
22
|
+
|
|
23
|
+
globals()["__generate_node"] = telegrinder.node.tools.generator.generate_node
|
|
24
|
+
|
|
25
|
+
return globals()["__generate_node"](*args, **kwargs)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
type NodeType = Node | NodeProto[typing.Any]
|
|
29
|
+
type IsNode = NodeType | type[NodeType]
|
|
9
30
|
|
|
10
31
|
T = typing.TypeVar("T", default=typing.Any)
|
|
11
32
|
|
|
12
33
|
ComposeResult: typing.TypeAlias = T | typing.Awaitable[T] | typing.AsyncGenerator[T, None]
|
|
13
34
|
|
|
35
|
+
UNWRAPPED_NODE_KEY = "__unwrapped_node__"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@typing.overload
|
|
39
|
+
def is_node(maybe_node: type[typing.Any], /) -> typing.TypeIs[type[NodeType]]: ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@typing.overload
|
|
43
|
+
def is_node(maybe_node: typing.Any, /) -> typing.TypeIs[NodeType]: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_node(maybe_node: typing.Any, /) -> bool:
|
|
47
|
+
if isinstance(maybe_node, typing.TypeAliasType):
|
|
48
|
+
maybe_node = maybe_node.__value__
|
|
49
|
+
if not isinstance(maybe_node, type):
|
|
50
|
+
maybe_node = typing.get_origin(maybe_node) or maybe_node
|
|
14
51
|
|
|
15
|
-
def is_node(maybe_node: typing.Any) -> typing.TypeGuard[type["Node"]]:
|
|
16
|
-
maybe_node = maybe_node if isinstance(maybe_node, type) else typing.get_origin(maybe_node)
|
|
17
52
|
return (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
or
|
|
53
|
+
hasattr(maybe_node, "as_node")
|
|
54
|
+
or isinstance(maybe_node, type)
|
|
55
|
+
and issubclass(maybe_node, (Node, NodeProto))
|
|
56
|
+
or not isinstance(maybe_node, type)
|
|
57
|
+
and isinstance(maybe_node, (Node, NodeProto))
|
|
22
58
|
)
|
|
23
59
|
|
|
24
60
|
|
|
61
|
+
@typing.overload
|
|
62
|
+
def as_node(maybe_node: type[typing.Any], /) -> type[NodeType]: ...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@typing.overload
|
|
66
|
+
def as_node(maybe_node: typing.Any, /) -> NodeType: ...
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@typing.overload
|
|
70
|
+
def as_node(*maybe_nodes: type[typing.Any]) -> tuple[type[NodeType], ...]: ...
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@typing.overload
|
|
74
|
+
def as_node(*maybe_nodes: typing.Any) -> tuple[NodeType, ...]: ...
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@typing.overload
|
|
78
|
+
def as_node(*maybe_nodes: type[typing.Any] | typing.Any) -> tuple[IsNode, ...]: ...
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def as_node(*maybe_nodes: typing.Any) -> typing.Any | tuple[typing.Any, ...]:
|
|
82
|
+
for maybe_node in maybe_nodes:
|
|
83
|
+
if not is_node(maybe_node):
|
|
84
|
+
is_type = isinstance(maybe_node, type)
|
|
85
|
+
raise LookupError(
|
|
86
|
+
f"{'Type of' if is_type else 'Object of type'} "
|
|
87
|
+
f"{maybe_node.__name__ if is_type else maybe_node.__class__.__name__!r} "
|
|
88
|
+
"cannot be resolved as Node."
|
|
89
|
+
)
|
|
90
|
+
return maybe_nodes[0] if len(maybe_nodes) == 1 else maybe_nodes
|
|
91
|
+
|
|
92
|
+
|
|
25
93
|
@cache_magic_value("__nodes__")
|
|
26
|
-
def get_nodes(function: typing.Callable[..., typing.Any]) -> dict[str, type[
|
|
27
|
-
return {k: v for k, v in get_annotations(function).items() if is_node(v)}
|
|
94
|
+
def get_nodes(function: typing.Callable[..., typing.Any], /) -> dict[str, type[NodeType]]:
|
|
95
|
+
return {k: v.as_node() for k, v in get_annotations(function).items() if is_node(v)}
|
|
28
96
|
|
|
29
97
|
|
|
30
98
|
@cache_magic_value("__is_generator__")
|
|
31
99
|
def is_generator(
|
|
32
100
|
function: typing.Callable[..., typing.Any],
|
|
101
|
+
/,
|
|
33
102
|
) -> typing.TypeGuard[AsyncGeneratorType[typing.Any, None]]:
|
|
34
103
|
return inspect.isasyncgenfunction(function)
|
|
35
104
|
|
|
36
105
|
|
|
37
|
-
def
|
|
38
|
-
"""
|
|
39
|
-
Provides caching for passed node type."""
|
|
106
|
+
def unwrap_node(node: type[NodeType], /) -> tuple[type[NodeType], ...]:
|
|
107
|
+
"""Unwrap node as flattened tuple of node types in ordering required to calculate given node.
|
|
40
108
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
109
|
+
Provides caching for passed node type.
|
|
110
|
+
"""
|
|
111
|
+
if (unwrapped := getattr(node, UNWRAPPED_NODE_KEY, None)) is not None:
|
|
112
|
+
return unwrapped
|
|
113
|
+
|
|
114
|
+
stack = deque([(node, node.get_subnodes().values())])
|
|
115
|
+
visited = list[type[NodeType]]()
|
|
116
|
+
|
|
117
|
+
while stack:
|
|
118
|
+
parent, child_nodes = stack.pop()
|
|
119
|
+
|
|
120
|
+
if parent not in visited:
|
|
121
|
+
visited.insert(0, parent)
|
|
122
|
+
|
|
123
|
+
for child in child_nodes:
|
|
124
|
+
stack.append((child, child.get_subnodes().values()))
|
|
125
|
+
|
|
126
|
+
unwrapped = tuple(visited)
|
|
127
|
+
setattr(node, UNWRAPPED_NODE_KEY, unwrapped)
|
|
128
|
+
return unwrapped
|
|
49
129
|
|
|
50
130
|
|
|
51
131
|
class ComposeError(BaseException):
|
|
52
132
|
pass
|
|
53
133
|
|
|
54
134
|
|
|
135
|
+
@typing.runtime_checkable
|
|
136
|
+
class Composable[R](typing.Protocol):
|
|
137
|
+
@classmethod
|
|
138
|
+
def compose(cls, *args: typing.Any, **kwargs: typing.Any) -> ComposeResult[R]: ...
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class NodeImpersonation(typing.Protocol):
|
|
142
|
+
@classmethod
|
|
143
|
+
def as_node(cls) -> type[NodeProto[typing.Any]]: ...
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class NodeComposeFunction[R](typing.Protocol):
|
|
147
|
+
__name__: str
|
|
148
|
+
__code__: CodeType
|
|
149
|
+
|
|
150
|
+
def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> ComposeResult[R]: ...
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@typing.runtime_checkable
|
|
154
|
+
class NodeProto[R](Composable[R], NodeImpersonation, typing.Protocol):
|
|
155
|
+
@classmethod
|
|
156
|
+
def get_subnodes(cls) -> dict[str, type[NodeType]]: ...
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def is_generator(cls) -> bool: ...
|
|
160
|
+
|
|
161
|
+
|
|
55
162
|
class Node(abc.ABC):
|
|
56
163
|
node: str = "node"
|
|
57
164
|
scope: NodeScope = NodeScope.PER_EVENT
|
|
@@ -62,11 +169,7 @@ class Node(abc.ABC):
|
|
|
62
169
|
pass
|
|
63
170
|
|
|
64
171
|
@classmethod
|
|
65
|
-
def
|
|
66
|
-
raise ComposeError(error)
|
|
67
|
-
|
|
68
|
-
@classmethod
|
|
69
|
-
def get_subnodes(cls) -> dict[str, type["Node"]]:
|
|
172
|
+
def get_subnodes(cls) -> dict[str, type[NodeType]]:
|
|
70
173
|
return get_nodes(cls.compose)
|
|
71
174
|
|
|
72
175
|
@classmethod
|
|
@@ -78,6 +181,41 @@ class Node(abc.ABC):
|
|
|
78
181
|
return is_generator(cls.compose)
|
|
79
182
|
|
|
80
183
|
|
|
184
|
+
class scalar_node[T]: # noqa: N801
|
|
185
|
+
@typing.overload
|
|
186
|
+
def __new__(cls, x: NodeComposeFunction[Composable[T]], /) -> type[T]: ...
|
|
187
|
+
|
|
188
|
+
@typing.overload
|
|
189
|
+
def __new__(cls, x: NodeComposeFunction[T], /) -> type[T]: ...
|
|
190
|
+
|
|
191
|
+
@typing.overload
|
|
192
|
+
def __new__(
|
|
193
|
+
cls,
|
|
194
|
+
/,
|
|
195
|
+
*,
|
|
196
|
+
scope: NodeScope,
|
|
197
|
+
) -> typing.Callable[[NodeComposeFunction[Composable[T]] | NodeComposeFunction[T]], type[T]]: ...
|
|
198
|
+
|
|
199
|
+
def __new__(cls, x=None, /, *, scope=NodeScope.PER_EVENT) -> typing.Any:
|
|
200
|
+
def inner(node_or_func, /) -> typing.Any:
|
|
201
|
+
namespace = {"node": "scalar", "scope": scope, "__module__": node_or_func.__module__}
|
|
202
|
+
|
|
203
|
+
if isinstance(node_or_func, type):
|
|
204
|
+
bases: list[type[typing.Any]] = [node_or_func]
|
|
205
|
+
node_bases = resolve_bases(node_or_func.__bases__)
|
|
206
|
+
if not any(issubclass(base, Node) for base in node_bases if isinstance(base, type)):
|
|
207
|
+
bases.append(Node)
|
|
208
|
+
return type(node_or_func.__name__, tuple(bases), namespace)
|
|
209
|
+
else:
|
|
210
|
+
base_node = generate_node(
|
|
211
|
+
func=node_or_func,
|
|
212
|
+
subnodes=tuple(get_nodes(node_or_func).values()),
|
|
213
|
+
)
|
|
214
|
+
return type(to_pascal_case(node_or_func.__name__), (base_node,), namespace)
|
|
215
|
+
|
|
216
|
+
return inner if x is None else inner(x)
|
|
217
|
+
|
|
218
|
+
|
|
81
219
|
@typing.dataclass_transform(kw_only_default=True)
|
|
82
220
|
class FactoryNode(Node, abc.ABC):
|
|
83
221
|
node = "factory"
|
|
@@ -87,10 +225,10 @@ class FactoryNode(Node, abc.ABC):
|
|
|
87
225
|
def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
88
226
|
pass
|
|
89
227
|
|
|
90
|
-
def __new__(cls, **context: typing.Any) -> typing.Self:
|
|
228
|
+
def __new__(cls, **context: typing.Any) -> type[typing.Self]:
|
|
91
229
|
namespace = dict(**cls.__dict__)
|
|
92
230
|
namespace.pop("__new__", None)
|
|
93
|
-
return type(cls.__name__, (cls,),
|
|
231
|
+
return type(cls.__name__, (cls,), namespace | context) # type: ignore
|
|
94
232
|
|
|
95
233
|
|
|
96
234
|
@typing.dataclass_transform()
|
|
@@ -103,64 +241,54 @@ class DataNode(Node, abc.ABC):
|
|
|
103
241
|
pass
|
|
104
242
|
|
|
105
243
|
|
|
106
|
-
|
|
244
|
+
@scalar_node(scope=NodeScope.PER_CALL)
|
|
245
|
+
class Name:
|
|
107
246
|
@classmethod
|
|
108
|
-
|
|
109
|
-
def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
110
|
-
pass
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
SCALAR_NODE = type("ScalarNode", (), {"node": "scalar"})
|
|
247
|
+
def compose(cls) -> str: ...
|
|
114
248
|
|
|
115
249
|
|
|
116
|
-
|
|
250
|
+
class GlobalNode[Value](Node):
|
|
251
|
+
scope = NodeScope.GLOBAL
|
|
117
252
|
|
|
118
|
-
|
|
119
|
-
|
|
253
|
+
@classmethod
|
|
254
|
+
def set(cls, value: Value, /) -> None:
|
|
255
|
+
setattr(cls, "_value", value)
|
|
120
256
|
|
|
121
|
-
|
|
257
|
+
@typing.overload
|
|
258
|
+
@classmethod
|
|
259
|
+
def get(cls) -> Value: ...
|
|
122
260
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _as_node(cls, bases, dct):
|
|
128
|
-
if not hasattr(cls, "_scalar_node_type"):
|
|
129
|
-
dct.update(cls.__dict__)
|
|
130
|
-
scalar_node_type = type(cls.__name__, bases, dct)
|
|
131
|
-
setattr(cls, "_scalar_node_type", scalar_node_type)
|
|
132
|
-
return scalar_node_type
|
|
133
|
-
return getattr(cls, "_scalar_node_type")
|
|
134
|
-
|
|
135
|
-
def create_class(name, bases, dct):
|
|
136
|
-
return type(
|
|
137
|
-
"Scalar",
|
|
138
|
-
(SCALAR_NODE,),
|
|
139
|
-
{
|
|
140
|
-
"as_node": classmethod(lambda cls: _as_node(cls, bases, dct)),
|
|
141
|
-
"scope": Node.scope,
|
|
142
|
-
"__init_subclass__": __init_subclass__,
|
|
143
|
-
},
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
class ScalarNode(ScalarNodeProto, abc.ABC, metaclass=create_class):
|
|
147
|
-
pass
|
|
261
|
+
@typing.overload
|
|
262
|
+
@classmethod
|
|
263
|
+
def get[Default](cls, *, default: Default) -> Value | Default: ...
|
|
148
264
|
|
|
265
|
+
@classmethod
|
|
266
|
+
def get(cls, **kwargs: typing.Any) -> typing.Any:
|
|
267
|
+
sentinel = object()
|
|
268
|
+
default = kwargs.pop("default", sentinel)
|
|
269
|
+
return getattr(cls, "_value") if default is sentinel else getattr(cls, "_value", default)
|
|
149
270
|
|
|
150
|
-
class Name(ScalarNode, str):
|
|
151
271
|
@classmethod
|
|
152
|
-
def
|
|
272
|
+
def unset(cls) -> None:
|
|
273
|
+
if hasattr(cls, "_value"):
|
|
274
|
+
delattr(cls, "_value")
|
|
153
275
|
|
|
154
276
|
|
|
155
277
|
__all__ = (
|
|
278
|
+
"Composable",
|
|
156
279
|
"ComposeError",
|
|
157
280
|
"DataNode",
|
|
158
281
|
"FactoryNode",
|
|
282
|
+
"GlobalNode",
|
|
283
|
+
"IsNode",
|
|
159
284
|
"Name",
|
|
160
285
|
"Node",
|
|
161
|
-
"
|
|
162
|
-
"
|
|
163
|
-
"
|
|
286
|
+
"NodeImpersonation",
|
|
287
|
+
"NodeProto",
|
|
288
|
+
"NodeType",
|
|
289
|
+
"as_node",
|
|
164
290
|
"get_nodes",
|
|
165
291
|
"is_node",
|
|
292
|
+
"scalar_node",
|
|
293
|
+
"unwrap_node",
|
|
166
294
|
)
|
|
@@ -4,25 +4,22 @@ from fntypes.result import Error, Ok
|
|
|
4
4
|
|
|
5
5
|
from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
|
|
6
6
|
from telegrinder.msgspec_utils import msgspec_convert
|
|
7
|
-
from telegrinder.node.base import ComposeError, FactoryNode, Name,
|
|
8
|
-
from telegrinder.node.update import UpdateNode
|
|
7
|
+
from telegrinder.node.base import ComposeError, FactoryNode, Name, scalar_node
|
|
9
8
|
|
|
10
|
-
FieldType = typing.TypeVar("FieldType")
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
class
|
|
10
|
+
@scalar_node
|
|
11
|
+
class CallbackQueryData:
|
|
14
12
|
@classmethod
|
|
15
|
-
def compose(cls,
|
|
16
|
-
|
|
17
|
-
raise ComposeError("Update is not a callback_query.")
|
|
18
|
-
return update.callback_query.unwrap()
|
|
13
|
+
def compose(cls, callback_query: CallbackQueryCute) -> str:
|
|
14
|
+
return callback_query.data.expect(ComposeError("Cannot complete decode callback query data."))
|
|
19
15
|
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
@scalar_node
|
|
18
|
+
class CallbackQueryDataJson:
|
|
22
19
|
@classmethod
|
|
23
|
-
def compose(cls, callback_query:
|
|
24
|
-
return callback_query.
|
|
25
|
-
ComposeError("Cannot complete decode callback query data.")
|
|
20
|
+
def compose(cls, callback_query: CallbackQueryCute) -> dict:
|
|
21
|
+
return callback_query.decode_data().expect(
|
|
22
|
+
ComposeError("Cannot complete decode callback query data."),
|
|
26
23
|
)
|
|
27
24
|
|
|
28
25
|
|
|
@@ -33,7 +30,7 @@ class _Field(FactoryNode):
|
|
|
33
30
|
return cls(field_type=field_type)
|
|
34
31
|
|
|
35
32
|
@classmethod
|
|
36
|
-
def compose(cls, callback_query_data:
|
|
33
|
+
def compose(cls, callback_query_data: CallbackQueryDataJson, data_name: Name) -> typing.Any:
|
|
37
34
|
if data := callback_query_data.get(data_name):
|
|
38
35
|
match msgspec_convert(data, cls.field_type):
|
|
39
36
|
case Ok(value):
|
|
@@ -45,9 +42,13 @@ class _Field(FactoryNode):
|
|
|
45
42
|
|
|
46
43
|
|
|
47
44
|
if typing.TYPE_CHECKING:
|
|
48
|
-
Field = typing.Annotated[FieldType, ...]
|
|
45
|
+
type Field[FieldType] = typing.Annotated[FieldType, ...]
|
|
49
46
|
else:
|
|
50
47
|
Field = _Field
|
|
51
48
|
|
|
52
49
|
|
|
53
|
-
__all__ = (
|
|
50
|
+
__all__ = (
|
|
51
|
+
"CallbackQueryData",
|
|
52
|
+
"CallbackQueryDataJson",
|
|
53
|
+
"Field",
|
|
54
|
+
)
|
telegrinder/node/command.py
CHANGED
|
@@ -4,7 +4,8 @@ from dataclasses import dataclass, field
|
|
|
4
4
|
from fntypes.option import Nothing, Option, Some
|
|
5
5
|
|
|
6
6
|
from telegrinder.node.base import DataNode
|
|
7
|
-
from telegrinder.node.
|
|
7
|
+
from telegrinder.node.either import Either
|
|
8
|
+
from telegrinder.node.text import Caption, Text
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def single_split(s: str, separator: str) -> tuple[str, str]:
|
|
@@ -24,7 +25,7 @@ class CommandInfo(DataNode):
|
|
|
24
25
|
mention: Option[str] = field(default_factory=Nothing)
|
|
25
26
|
|
|
26
27
|
@classmethod
|
|
27
|
-
def compose(cls, text: Text) -> typing.Self:
|
|
28
|
+
def compose(cls, text: Either[Text, Caption]) -> typing.Self:
|
|
28
29
|
name, arguments = single_split(text, separator=" ")
|
|
29
30
|
name, mention = cut_mention(name)
|
|
30
31
|
return cls(name, arguments, mention)
|