telegrinder 0.3.4__py3-none-any.whl → 0.3.4.post1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of telegrinder might be problematic. Click here for more details.
- telegrinder/__init__.py +144 -144
- telegrinder/api/__init__.py +8 -8
- telegrinder/api/api.py +93 -93
- telegrinder/api/error.py +16 -16
- telegrinder/api/response.py +20 -20
- telegrinder/api/token.py +36 -36
- telegrinder/bot/__init__.py +66 -66
- telegrinder/bot/bot.py +76 -76
- telegrinder/bot/cute_types/__init__.py +17 -17
- telegrinder/bot/cute_types/base.py +258 -258
- telegrinder/bot/cute_types/callback_query.py +385 -385
- telegrinder/bot/cute_types/chat_join_request.py +61 -61
- telegrinder/bot/cute_types/chat_member_updated.py +160 -160
- telegrinder/bot/cute_types/inline_query.py +43 -43
- telegrinder/bot/cute_types/message.py +2637 -2637
- telegrinder/bot/cute_types/update.py +104 -104
- telegrinder/bot/cute_types/utils.py +95 -95
- telegrinder/bot/dispatch/__init__.py +55 -55
- telegrinder/bot/dispatch/abc.py +77 -77
- telegrinder/bot/dispatch/context.py +98 -98
- telegrinder/bot/dispatch/dispatch.py +202 -202
- telegrinder/bot/dispatch/handler/__init__.py +13 -13
- telegrinder/bot/dispatch/handler/abc.py +24 -24
- telegrinder/bot/dispatch/handler/audio_reply.py +44 -44
- telegrinder/bot/dispatch/handler/base.py +57 -57
- telegrinder/bot/dispatch/handler/document_reply.py +44 -44
- telegrinder/bot/dispatch/handler/func.py +135 -135
- telegrinder/bot/dispatch/handler/media_group_reply.py +43 -43
- telegrinder/bot/dispatch/handler/message_reply.py +36 -36
- telegrinder/bot/dispatch/handler/photo_reply.py +44 -44
- telegrinder/bot/dispatch/handler/sticker_reply.py +37 -37
- telegrinder/bot/dispatch/handler/video_reply.py +44 -44
- telegrinder/bot/dispatch/middleware/__init__.py +3 -3
- telegrinder/bot/dispatch/middleware/abc.py +22 -22
- telegrinder/bot/dispatch/process.py +157 -157
- telegrinder/bot/dispatch/return_manager/__init__.py +13 -13
- telegrinder/bot/dispatch/return_manager/abc.py +108 -108
- telegrinder/bot/dispatch/return_manager/callback_query.py +20 -20
- telegrinder/bot/dispatch/return_manager/inline_query.py +15 -15
- telegrinder/bot/dispatch/return_manager/message.py +36 -36
- telegrinder/bot/dispatch/view/__init__.py +13 -13
- telegrinder/bot/dispatch/view/abc.py +41 -41
- telegrinder/bot/dispatch/view/base.py +200 -200
- telegrinder/bot/dispatch/view/box.py +129 -129
- telegrinder/bot/dispatch/view/callback_query.py +17 -17
- telegrinder/bot/dispatch/view/chat_join_request.py +16 -16
- telegrinder/bot/dispatch/view/chat_member.py +39 -39
- telegrinder/bot/dispatch/view/inline_query.py +17 -17
- telegrinder/bot/dispatch/view/message.py +44 -44
- telegrinder/bot/dispatch/view/raw.py +114 -114
- telegrinder/bot/dispatch/waiter_machine/__init__.py +17 -17
- telegrinder/bot/dispatch/waiter_machine/actions.py +13 -13
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +8 -8
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +55 -55
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +57 -57
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +51 -51
- telegrinder/bot/dispatch/waiter_machine/hasher/state.py +19 -19
- telegrinder/bot/dispatch/waiter_machine/machine.py +172 -172
- telegrinder/bot/dispatch/waiter_machine/middleware.py +89 -89
- telegrinder/bot/dispatch/waiter_machine/short_state.py +68 -68
- telegrinder/bot/polling/__init__.py +4 -4
- telegrinder/bot/polling/abc.py +25 -25
- telegrinder/bot/polling/polling.py +131 -131
- telegrinder/bot/rules/__init__.py +62 -62
- telegrinder/bot/rules/abc.py +206 -206
- telegrinder/bot/rules/adapter/__init__.py +17 -17
- telegrinder/bot/rules/adapter/abc.py +31 -31
- telegrinder/bot/rules/adapter/errors.py +5 -5
- telegrinder/bot/rules/adapter/event.py +65 -65
- telegrinder/bot/rules/adapter/node.py +48 -48
- telegrinder/bot/rules/adapter/raw_event.py +27 -27
- telegrinder/bot/rules/adapter/raw_update.py +30 -30
- telegrinder/bot/rules/callback_data.py +163 -163
- telegrinder/bot/rules/chat_join.py +43 -43
- telegrinder/bot/rules/command.py +126 -126
- telegrinder/bot/rules/enum_text.py +36 -36
- telegrinder/bot/rules/func.py +26 -26
- telegrinder/bot/rules/fuzzy.py +24 -24
- telegrinder/bot/rules/inline.py +56 -56
- telegrinder/bot/rules/integer.py +20 -20
- telegrinder/bot/rules/is_from.py +127 -127
- telegrinder/bot/rules/markup.py +43 -43
- telegrinder/bot/rules/mention.py +14 -14
- telegrinder/bot/rules/message.py +17 -17
- telegrinder/bot/rules/message_entities.py +35 -35
- telegrinder/bot/rules/node.py +27 -27
- telegrinder/bot/rules/regex.py +37 -37
- telegrinder/bot/rules/rule_enum.py +72 -72
- telegrinder/bot/rules/start.py +42 -42
- telegrinder/bot/rules/state.py +37 -37
- telegrinder/bot/rules/text.py +33 -33
- telegrinder/bot/rules/update.py +15 -15
- telegrinder/bot/scenario/__init__.py +5 -5
- telegrinder/bot/scenario/abc.py +19 -19
- telegrinder/bot/scenario/checkbox.py +176 -176
- telegrinder/bot/scenario/choice.py +51 -51
- telegrinder/client/__init__.py +4 -4
- telegrinder/client/abc.py +75 -75
- telegrinder/client/aiohttp.py +130 -130
- telegrinder/model.py +313 -313
- telegrinder/modules.py +237 -237
- telegrinder/msgspec_json.py +14 -14
- telegrinder/msgspec_utils.py +410 -410
- telegrinder/node/__init__.py +20 -20
- telegrinder/node/attachment.py +87 -87
- telegrinder/node/base.py +157 -157
- telegrinder/node/callback_query.py +53 -53
- telegrinder/node/command.py +33 -33
- telegrinder/node/composer.py +198 -198
- telegrinder/node/container.py +27 -27
- telegrinder/node/event.py +65 -65
- telegrinder/node/me.py +16 -16
- telegrinder/node/message.py +14 -14
- telegrinder/node/polymorphic.py +48 -48
- telegrinder/node/rule.py +76 -76
- telegrinder/node/scope.py +38 -38
- telegrinder/node/source.py +71 -71
- telegrinder/node/text.py +41 -41
- telegrinder/node/tools/__init__.py +3 -3
- telegrinder/node/tools/generator.py +40 -40
- telegrinder/node/update.py +15 -15
- telegrinder/rules.py +5 -5
- telegrinder/tools/__init__.py +74 -74
- telegrinder/tools/buttons.py +79 -79
- telegrinder/tools/error_handler/__init__.py +7 -7
- telegrinder/tools/error_handler/abc.py +33 -33
- telegrinder/tools/error_handler/error.py +9 -9
- telegrinder/tools/error_handler/error_handler.py +193 -193
- telegrinder/tools/formatting/__init__.py +46 -46
- telegrinder/tools/formatting/html.py +283 -283
- telegrinder/tools/formatting/links.py +33 -33
- telegrinder/tools/formatting/spec_html_formats.py +111 -111
- telegrinder/tools/functional.py +12 -12
- telegrinder/tools/global_context/__init__.py +7 -7
- telegrinder/tools/global_context/abc.py +63 -63
- telegrinder/tools/global_context/global_context.py +412 -412
- telegrinder/tools/global_context/telegrinder_ctx.py +27 -27
- telegrinder/tools/i18n/__init__.py +7 -7
- telegrinder/tools/i18n/abc.py +30 -30
- telegrinder/tools/i18n/middleware/__init__.py +3 -3
- telegrinder/tools/i18n/middleware/abc.py +25 -25
- telegrinder/tools/i18n/simple.py +43 -43
- telegrinder/tools/kb_set/__init__.py +4 -4
- telegrinder/tools/kb_set/base.py +15 -15
- telegrinder/tools/kb_set/yaml.py +63 -63
- telegrinder/tools/keyboard.py +128 -128
- telegrinder/tools/limited_dict.py +37 -37
- telegrinder/tools/loop_wrapper/__init__.py +4 -4
- telegrinder/tools/loop_wrapper/abc.py +15 -15
- telegrinder/tools/loop_wrapper/loop_wrapper.py +224 -224
- telegrinder/tools/magic.py +157 -157
- telegrinder/tools/parse_mode.py +6 -6
- telegrinder/tools/state_storage/__init__.py +4 -4
- telegrinder/tools/state_storage/abc.py +35 -35
- telegrinder/tools/state_storage/memory.py +25 -25
- telegrinder/types/__init__.py +260 -260
- telegrinder/types/enums.py +701 -701
- telegrinder/types/methods.py +4633 -4633
- telegrinder/types/objects.py +6950 -6950
- telegrinder/verification_utils.py +32 -32
- {telegrinder-0.3.4.dist-info → telegrinder-0.3.4.post1.dist-info}/LICENSE +22 -22
- {telegrinder-0.3.4.dist-info → telegrinder-0.3.4.post1.dist-info}/METADATA +1 -1
- telegrinder-0.3.4.post1.dist-info/RECORD +165 -0
- telegrinder-0.3.4.dist-info/RECORD +0 -165
- {telegrinder-0.3.4.dist-info → telegrinder-0.3.4.post1.dist-info}/WHEEL +0 -0
telegrinder/node/__init__.py
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
from .attachment import Attachment, Audio, Photo, Video
|
|
2
|
-
from .base import ComposeError, DataNode, Name, Node, ScalarNode, is_node
|
|
3
|
-
from .callback_query import CallbackQueryData, CallbackQueryNode, Field
|
|
4
|
-
from .command import CommandInfo
|
|
5
|
-
from .composer import Composition, NodeCollection, NodeSession, compose_node, compose_nodes
|
|
6
|
-
from .container import ContainerNode
|
|
7
|
-
from .event import EventNode
|
|
8
|
-
from .me import Me
|
|
9
|
-
from .message import MessageNode
|
|
10
|
-
from .polymorphic import Polymorphic, impl
|
|
11
|
-
from .rule import RuleChain
|
|
12
|
-
from .scope import GLOBAL, PER_CALL, PER_EVENT, NodeScope, global_node, per_call, per_event
|
|
13
|
-
from .source import ChatSource, Source, UserSource
|
|
14
|
-
from .text import Text, TextInteger, TextLiteral
|
|
15
|
-
from .tools import generate_node
|
|
16
|
-
from .update import UpdateNode
|
|
17
|
-
|
|
18
|
-
__all__ = (
|
|
1
|
+
from .attachment import Attachment, Audio, Photo, Video
|
|
2
|
+
from .base import ComposeError, DataNode, Name, Node, ScalarNode, is_node
|
|
3
|
+
from .callback_query import CallbackQueryData, CallbackQueryNode, Field
|
|
4
|
+
from .command import CommandInfo
|
|
5
|
+
from .composer import Composition, NodeCollection, NodeSession, compose_node, compose_nodes
|
|
6
|
+
from .container import ContainerNode
|
|
7
|
+
from .event import EventNode
|
|
8
|
+
from .me import Me
|
|
9
|
+
from .message import MessageNode
|
|
10
|
+
from .polymorphic import Polymorphic, impl
|
|
11
|
+
from .rule import RuleChain
|
|
12
|
+
from .scope import GLOBAL, PER_CALL, PER_EVENT, NodeScope, global_node, per_call, per_event
|
|
13
|
+
from .source import ChatSource, Source, UserSource
|
|
14
|
+
from .text import Text, TextInteger, TextLiteral
|
|
15
|
+
from .tools import generate_node
|
|
16
|
+
from .update import UpdateNode
|
|
17
|
+
|
|
18
|
+
__all__ = (
|
|
19
19
|
"Attachment",
|
|
20
20
|
"Audio",
|
|
21
21
|
"CallbackQueryData",
|
|
@@ -56,5 +56,5 @@ __all__ = (
|
|
|
56
56
|
"impl",
|
|
57
57
|
"is_node",
|
|
58
58
|
"per_call",
|
|
59
|
-
"per_event",
|
|
60
|
-
)
|
|
59
|
+
"per_event",
|
|
60
|
+
)
|
telegrinder/node/attachment.py
CHANGED
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
import dataclasses
|
|
2
|
-
import typing
|
|
3
|
-
|
|
4
|
-
from fntypes.co import Option, Some
|
|
5
|
-
from fntypes.option import Nothing
|
|
6
|
-
|
|
7
|
-
import telegrinder.types
|
|
8
|
-
from telegrinder.node.base import ComposeError, DataNode, ScalarNode
|
|
9
|
-
from telegrinder.node.message import MessageNode
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@dataclasses.dataclass(slots=True)
|
|
13
|
-
class Attachment(DataNode):
|
|
14
|
-
attachment_type: typing.Literal["audio", "document", "photo", "poll", "video"]
|
|
15
|
-
audio: Option[telegrinder.types.Audio] = dataclasses.field(
|
|
16
|
-
default_factory=lambda: Nothing(),
|
|
17
|
-
kw_only=True,
|
|
18
|
-
)
|
|
19
|
-
document: Option[telegrinder.types.Document] = dataclasses.field(
|
|
20
|
-
default_factory=lambda: Nothing(),
|
|
21
|
-
kw_only=True,
|
|
22
|
-
)
|
|
23
|
-
photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
|
|
24
|
-
default_factory=lambda: Nothing(),
|
|
25
|
-
kw_only=True,
|
|
26
|
-
)
|
|
27
|
-
poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing(), kw_only=True)
|
|
28
|
-
video: Option[telegrinder.types.Video] = dataclasses.field(
|
|
29
|
-
default_factory=lambda: Nothing(),
|
|
30
|
-
kw_only=True,
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
@classmethod
|
|
34
|
-
def compose(cls, message: MessageNode) -> "Attachment":
|
|
35
|
-
for attachment_type in ("audio", "document", "photo", "poll", "video"):
|
|
36
|
-
match getattr(message, attachment_type, Nothing()):
|
|
37
|
-
case Some(attachment):
|
|
38
|
-
return cls(attachment_type, **{attachment_type: Some(attachment)})
|
|
39
|
-
return cls.compose_error("No attachment found in message.")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@dataclasses.dataclass(slots=True)
|
|
43
|
-
class Photo(DataNode):
|
|
44
|
-
sizes: list[telegrinder.types.PhotoSize]
|
|
45
|
-
|
|
46
|
-
@classmethod
|
|
47
|
-
def compose(cls, attachment: Attachment) -> typing.Self:
|
|
48
|
-
if not attachment.photo:
|
|
49
|
-
raise ComposeError("Attachment is not a photo.")
|
|
50
|
-
return cls(attachment.photo.unwrap())
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class Video(ScalarNode, telegrinder.types.Video):
|
|
54
|
-
@classmethod
|
|
55
|
-
def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
|
|
56
|
-
if not attachment.video:
|
|
57
|
-
raise ComposeError("Attachment is not a video.")
|
|
58
|
-
return attachment.video.unwrap()
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class Audio(ScalarNode, telegrinder.types.Audio):
|
|
62
|
-
@classmethod
|
|
63
|
-
def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
|
|
64
|
-
if not attachment.audio:
|
|
65
|
-
raise ComposeError("Attachment is not an audio.")
|
|
66
|
-
return attachment.audio.unwrap()
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class Document(ScalarNode, telegrinder.types.Document):
|
|
70
|
-
@classmethod
|
|
71
|
-
def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
|
|
72
|
-
if not attachment.document:
|
|
73
|
-
raise ComposeError("Attachment is not a document.")
|
|
74
|
-
return attachment.document.unwrap()
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class Poll(ScalarNode, telegrinder.types.Poll):
|
|
78
|
-
@classmethod
|
|
79
|
-
def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
|
|
80
|
-
if not attachment.poll:
|
|
81
|
-
raise ComposeError("Attachment is not a poll.")
|
|
82
|
-
return attachment.poll.unwrap()
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
__all__ = (
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from fntypes.co import Option, Some
|
|
5
|
+
from fntypes.option import Nothing
|
|
6
|
+
|
|
7
|
+
import telegrinder.types
|
|
8
|
+
from telegrinder.node.base import ComposeError, DataNode, ScalarNode
|
|
9
|
+
from telegrinder.node.message import MessageNode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclasses.dataclass(slots=True)
|
|
13
|
+
class Attachment(DataNode):
|
|
14
|
+
attachment_type: typing.Literal["audio", "document", "photo", "poll", "video"]
|
|
15
|
+
audio: Option[telegrinder.types.Audio] = dataclasses.field(
|
|
16
|
+
default_factory=lambda: Nothing(),
|
|
17
|
+
kw_only=True,
|
|
18
|
+
)
|
|
19
|
+
document: Option[telegrinder.types.Document] = dataclasses.field(
|
|
20
|
+
default_factory=lambda: Nothing(),
|
|
21
|
+
kw_only=True,
|
|
22
|
+
)
|
|
23
|
+
photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
|
|
24
|
+
default_factory=lambda: Nothing(),
|
|
25
|
+
kw_only=True,
|
|
26
|
+
)
|
|
27
|
+
poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing(), kw_only=True)
|
|
28
|
+
video: Option[telegrinder.types.Video] = dataclasses.field(
|
|
29
|
+
default_factory=lambda: Nothing(),
|
|
30
|
+
kw_only=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def compose(cls, message: MessageNode) -> "Attachment":
|
|
35
|
+
for attachment_type in ("audio", "document", "photo", "poll", "video"):
|
|
36
|
+
match getattr(message, attachment_type, Nothing()):
|
|
37
|
+
case Some(attachment):
|
|
38
|
+
return cls(attachment_type, **{attachment_type: Some(attachment)})
|
|
39
|
+
return cls.compose_error("No attachment found in message.")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclasses.dataclass(slots=True)
|
|
43
|
+
class Photo(DataNode):
|
|
44
|
+
sizes: list[telegrinder.types.PhotoSize]
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def compose(cls, attachment: Attachment) -> typing.Self:
|
|
48
|
+
if not attachment.photo:
|
|
49
|
+
raise ComposeError("Attachment is not a photo.")
|
|
50
|
+
return cls(attachment.photo.unwrap())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Video(ScalarNode, telegrinder.types.Video):
|
|
54
|
+
@classmethod
|
|
55
|
+
def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
|
|
56
|
+
if not attachment.video:
|
|
57
|
+
raise ComposeError("Attachment is not a video.")
|
|
58
|
+
return attachment.video.unwrap()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Audio(ScalarNode, telegrinder.types.Audio):
|
|
62
|
+
@classmethod
|
|
63
|
+
def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
|
|
64
|
+
if not attachment.audio:
|
|
65
|
+
raise ComposeError("Attachment is not an audio.")
|
|
66
|
+
return attachment.audio.unwrap()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Document(ScalarNode, telegrinder.types.Document):
|
|
70
|
+
@classmethod
|
|
71
|
+
def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
|
|
72
|
+
if not attachment.document:
|
|
73
|
+
raise ComposeError("Attachment is not a document.")
|
|
74
|
+
return attachment.document.unwrap()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Poll(ScalarNode, telegrinder.types.Poll):
|
|
78
|
+
@classmethod
|
|
79
|
+
def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
|
|
80
|
+
if not attachment.poll:
|
|
81
|
+
raise ComposeError("Attachment is not a poll.")
|
|
82
|
+
return attachment.poll.unwrap()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
__all__ = (
|
|
86
86
|
"Attachment",
|
|
87
87
|
"Audio",
|
|
88
88
|
"Document",
|
|
89
89
|
"Photo",
|
|
90
90
|
"Poll",
|
|
91
|
-
"Video",
|
|
92
|
-
)
|
|
91
|
+
"Video",
|
|
92
|
+
)
|
telegrinder/node/base.py
CHANGED
|
@@ -1,158 +1,158 @@
|
|
|
1
|
-
import abc
|
|
2
|
-
import inspect
|
|
3
|
-
from types import AsyncGeneratorType
|
|
4
|
-
|
|
5
|
-
import typing_extensions as typing
|
|
6
|
-
|
|
7
|
-
from telegrinder.node.scope import NodeScope
|
|
8
|
-
from telegrinder.tools.magic import cache_magic_value, get_annotations
|
|
9
|
-
|
|
10
|
-
T = typing.TypeVar("T", default=typing.Any)
|
|
11
|
-
|
|
12
|
-
ComposeResult: typing.TypeAlias = T | typing.Awaitable[T] | typing.AsyncGenerator[T, None]
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
return (
|
|
18
|
-
isinstance(maybe_node, type)
|
|
19
|
-
and issubclass(maybe_node, Node)
|
|
20
|
-
or isinstance(maybe_node, Node)
|
|
21
|
-
or hasattr(maybe_node, "as_node")
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@cache_magic_value("__nodes__")
|
|
26
|
-
def get_nodes(function: typing.Callable[..., typing.Any]) -> dict[str, type["Node"]]:
|
|
27
|
-
return {k: v for k, v in get_annotations(function).items() if is_node(v)}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@cache_magic_value("__is_generator__")
|
|
31
|
-
def is_generator(
|
|
32
|
-
function: typing.Callable[..., typing.Any],
|
|
33
|
-
) -> typing.TypeGuard[AsyncGeneratorType[typing.Any, None]]:
|
|
34
|
-
return inspect.isasyncgenfunction(function)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def get_node_calc_lst(node: type["Node"]) -> list[type["Node"]]:
|
|
38
|
-
"""Returns flattened list of node types in ordering required to calculate given node.
|
|
39
|
-
Provides caching for passed node type."""
|
|
40
|
-
|
|
41
|
-
if calc_lst := getattr(node, "__nodes_calc_lst__", None):
|
|
42
|
-
return calc_lst
|
|
43
|
-
nodes_lst: list[type[Node]] = []
|
|
44
|
-
for node_type in node.as_node().get_subnodes().values():
|
|
45
|
-
nodes_lst.extend(get_node_calc_lst(node_type))
|
|
46
|
-
calc_lst = [*nodes_lst, node]
|
|
47
|
-
setattr(node, "__nodes_calc_lst__", calc_lst)
|
|
48
|
-
return calc_lst
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class ComposeError(BaseException):
|
|
52
|
-
pass
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class Node(abc.ABC):
|
|
56
|
-
node: str = "node"
|
|
57
|
-
scope: NodeScope = NodeScope.PER_EVENT
|
|
58
|
-
|
|
59
|
-
@classmethod
|
|
60
|
-
@abc.abstractmethod
|
|
61
|
-
def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
62
|
-
pass
|
|
63
|
-
|
|
64
|
-
@classmethod
|
|
65
|
-
def compose_error(cls, error: str | None = None) -> typing.NoReturn:
|
|
66
|
-
raise ComposeError(error)
|
|
67
|
-
|
|
68
|
-
@classmethod
|
|
69
|
-
def get_subnodes(cls) -> dict[str, type["Node"]]:
|
|
70
|
-
return get_nodes(cls.compose)
|
|
71
|
-
|
|
72
|
-
@classmethod
|
|
73
|
-
def as_node(cls) -> type[typing.Self]:
|
|
74
|
-
return cls
|
|
75
|
-
|
|
76
|
-
@classmethod
|
|
77
|
-
def is_generator(cls) -> bool:
|
|
78
|
-
return is_generator(cls.compose)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
@typing.dataclass_transform(kw_only_default=True)
|
|
82
|
-
class FactoryNode(Node, abc.ABC):
|
|
83
|
-
node = "factory"
|
|
84
|
-
|
|
85
|
-
@classmethod
|
|
86
|
-
@abc.abstractmethod
|
|
87
|
-
def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
88
|
-
pass
|
|
89
|
-
|
|
90
|
-
def __new__(cls, **context: typing.Any) -> typing.Self:
|
|
91
|
-
namespace = dict(**cls.__dict__)
|
|
92
|
-
namespace.pop("__new__", None)
|
|
93
|
-
return type(cls.__name__, (cls,), context | namespace) # type: ignore
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
@typing.dataclass_transform()
|
|
97
|
-
class DataNode(Node, abc.ABC):
|
|
98
|
-
node = "data"
|
|
99
|
-
|
|
100
|
-
@classmethod
|
|
101
|
-
@abc.abstractmethod
|
|
102
|
-
def compose(cls, *args, **kwargs) -> ComposeResult[typing.Self]:
|
|
103
|
-
pass
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
class ScalarNodeProto(Node, abc.ABC):
|
|
107
|
-
@classmethod
|
|
108
|
-
@abc.abstractmethod
|
|
109
|
-
def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
110
|
-
pass
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
SCALAR_NODE = type("ScalarNode", (), {"node": "scalar"})
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if typing.TYPE_CHECKING:
|
|
117
|
-
|
|
118
|
-
class ScalarNode(ScalarNodeProto, abc.ABC):
|
|
119
|
-
pass
|
|
120
|
-
|
|
121
|
-
else:
|
|
122
|
-
|
|
123
|
-
def __init_subclass__(cls, *args, **kwargs): # noqa: N807
|
|
124
|
-
if any(issubclass(base, ScalarNodeProto) for base in cls.__bases__ if base is not ScalarNode):
|
|
125
|
-
raise RuntimeError("Scalar nodes do not support inheritance.")
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
class Name(ScalarNode, str):
|
|
151
|
-
@classmethod
|
|
152
|
-
def compose(cls) -> str: ...
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
__all__ = (
|
|
1
|
+
import abc
|
|
2
|
+
import inspect
|
|
3
|
+
from types import AsyncGeneratorType
|
|
4
|
+
|
|
5
|
+
import typing_extensions as typing
|
|
6
|
+
|
|
7
|
+
from telegrinder.node.scope import NodeScope
|
|
8
|
+
from telegrinder.tools.magic import cache_magic_value, get_annotations
|
|
9
|
+
|
|
10
|
+
T = typing.TypeVar("T", default=typing.Any)
|
|
11
|
+
|
|
12
|
+
ComposeResult: typing.TypeAlias = T | typing.Awaitable[T] | typing.AsyncGenerator[T, None]
|
|
13
|
+
|
|
14
|
+
|
|
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
|
+
return (
|
|
18
|
+
isinstance(maybe_node, type)
|
|
19
|
+
and issubclass(maybe_node, Node)
|
|
20
|
+
or isinstance(maybe_node, Node)
|
|
21
|
+
or hasattr(maybe_node, "as_node")
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@cache_magic_value("__nodes__")
|
|
26
|
+
def get_nodes(function: typing.Callable[..., typing.Any]) -> dict[str, type["Node"]]:
|
|
27
|
+
return {k: v for k, v in get_annotations(function).items() if is_node(v)}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@cache_magic_value("__is_generator__")
|
|
31
|
+
def is_generator(
|
|
32
|
+
function: typing.Callable[..., typing.Any],
|
|
33
|
+
) -> typing.TypeGuard[AsyncGeneratorType[typing.Any, None]]:
|
|
34
|
+
return inspect.isasyncgenfunction(function)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_node_calc_lst(node: type["Node"]) -> list[type["Node"]]:
|
|
38
|
+
"""Returns flattened list of node types in ordering required to calculate given node.
|
|
39
|
+
Provides caching for passed node type."""
|
|
40
|
+
|
|
41
|
+
if calc_lst := getattr(node, "__nodes_calc_lst__", None):
|
|
42
|
+
return calc_lst
|
|
43
|
+
nodes_lst: list[type[Node]] = []
|
|
44
|
+
for node_type in node.as_node().get_subnodes().values():
|
|
45
|
+
nodes_lst.extend(get_node_calc_lst(node_type))
|
|
46
|
+
calc_lst = [*nodes_lst, node]
|
|
47
|
+
setattr(node, "__nodes_calc_lst__", calc_lst)
|
|
48
|
+
return calc_lst
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ComposeError(BaseException):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Node(abc.ABC):
|
|
56
|
+
node: str = "node"
|
|
57
|
+
scope: NodeScope = NodeScope.PER_EVENT
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
@abc.abstractmethod
|
|
61
|
+
def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def compose_error(cls, error: str | None = None) -> typing.NoReturn:
|
|
66
|
+
raise ComposeError(error)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def get_subnodes(cls) -> dict[str, type["Node"]]:
|
|
70
|
+
return get_nodes(cls.compose)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def as_node(cls) -> type[typing.Self]:
|
|
74
|
+
return cls
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def is_generator(cls) -> bool:
|
|
78
|
+
return is_generator(cls.compose)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@typing.dataclass_transform(kw_only_default=True)
|
|
82
|
+
class FactoryNode(Node, abc.ABC):
|
|
83
|
+
node = "factory"
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
@abc.abstractmethod
|
|
87
|
+
def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def __new__(cls, **context: typing.Any) -> typing.Self:
|
|
91
|
+
namespace = dict(**cls.__dict__)
|
|
92
|
+
namespace.pop("__new__", None)
|
|
93
|
+
return type(cls.__name__, (cls,), context | namespace) # type: ignore
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@typing.dataclass_transform()
|
|
97
|
+
class DataNode(Node, abc.ABC):
|
|
98
|
+
node = "data"
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
@abc.abstractmethod
|
|
102
|
+
def compose(cls, *args, **kwargs) -> ComposeResult[typing.Self]:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ScalarNodeProto(Node, abc.ABC):
|
|
107
|
+
@classmethod
|
|
108
|
+
@abc.abstractmethod
|
|
109
|
+
def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
SCALAR_NODE = type("ScalarNode", (), {"node": "scalar"})
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if typing.TYPE_CHECKING:
|
|
117
|
+
|
|
118
|
+
class ScalarNode(ScalarNodeProto, abc.ABC):
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
else:
|
|
122
|
+
|
|
123
|
+
def __init_subclass__(cls, *args, **kwargs): # noqa: N807
|
|
124
|
+
if any(issubclass(base, ScalarNodeProto) for base in cls.__bases__ if base is not ScalarNode):
|
|
125
|
+
raise RuntimeError("Scalar nodes do not support inheritance.")
|
|
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
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Name(ScalarNode, str):
|
|
151
|
+
@classmethod
|
|
152
|
+
def compose(cls) -> str: ...
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
__all__ = (
|
|
156
156
|
"ComposeError",
|
|
157
157
|
"DataNode",
|
|
158
158
|
"FactoryNode",
|
|
@@ -162,5 +162,5 @@ __all__ = (
|
|
|
162
162
|
"ScalarNode",
|
|
163
163
|
"ScalarNodeProto",
|
|
164
164
|
"get_nodes",
|
|
165
|
-
"is_node",
|
|
166
|
-
)
|
|
165
|
+
"is_node",
|
|
166
|
+
)
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
import typing
|
|
2
|
-
|
|
3
|
-
from fntypes.result import Error, Ok
|
|
4
|
-
|
|
5
|
-
from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
|
|
6
|
-
from telegrinder.msgspec_utils import msgspec_convert
|
|
7
|
-
from telegrinder.node.base import ComposeError, FactoryNode, Name, ScalarNode
|
|
8
|
-
from telegrinder.node.update import UpdateNode
|
|
9
|
-
|
|
10
|
-
FieldType = typing.TypeVar("FieldType")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class CallbackQueryNode(ScalarNode, CallbackQueryCute):
|
|
14
|
-
@classmethod
|
|
15
|
-
def compose(cls, update: UpdateNode) -> CallbackQueryCute:
|
|
16
|
-
if not update.callback_query:
|
|
17
|
-
raise ComposeError("Update is not a callback_query.")
|
|
18
|
-
return update.callback_query.unwrap()
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class CallbackQueryData(ScalarNode, dict[str, typing.Any]):
|
|
22
|
-
@classmethod
|
|
23
|
-
def compose(cls, callback_query: CallbackQueryNode) -> dict[str, typing.Any]:
|
|
24
|
-
return callback_query.decode_callback_data().expect(
|
|
25
|
-
ComposeError("Cannot complete decode callback query data.")
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class _Field(FactoryNode):
|
|
30
|
-
field_type: type[typing.Any]
|
|
31
|
-
|
|
32
|
-
def __class_getitem__(cls, field_type: type[typing.Any], /) -> typing.Self:
|
|
33
|
-
return cls(field_type=field_type)
|
|
34
|
-
|
|
35
|
-
@classmethod
|
|
36
|
-
def compose(cls, callback_query_data: CallbackQueryData, data_name: Name) -> typing.Any:
|
|
37
|
-
if data := callback_query_data.get(data_name):
|
|
38
|
-
match msgspec_convert(data, cls.field_type):
|
|
39
|
-
case Ok(value):
|
|
40
|
-
return value
|
|
41
|
-
case Error(err):
|
|
42
|
-
raise ComposeError(err)
|
|
43
|
-
|
|
44
|
-
raise ComposeError(f"Cannot find callback data with name {data_name!r}.")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if typing.TYPE_CHECKING:
|
|
48
|
-
Field = typing.Annotated[FieldType, ...]
|
|
49
|
-
else:
|
|
50
|
-
Field = _Field
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
__all__ = ("CallbackQueryData", "CallbackQueryNode", "Field")
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from fntypes.result import Error, Ok
|
|
4
|
+
|
|
5
|
+
from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
|
|
6
|
+
from telegrinder.msgspec_utils import msgspec_convert
|
|
7
|
+
from telegrinder.node.base import ComposeError, FactoryNode, Name, ScalarNode
|
|
8
|
+
from telegrinder.node.update import UpdateNode
|
|
9
|
+
|
|
10
|
+
FieldType = typing.TypeVar("FieldType")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CallbackQueryNode(ScalarNode, CallbackQueryCute):
|
|
14
|
+
@classmethod
|
|
15
|
+
def compose(cls, update: UpdateNode) -> CallbackQueryCute:
|
|
16
|
+
if not update.callback_query:
|
|
17
|
+
raise ComposeError("Update is not a callback_query.")
|
|
18
|
+
return update.callback_query.unwrap()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CallbackQueryData(ScalarNode, dict[str, typing.Any]):
|
|
22
|
+
@classmethod
|
|
23
|
+
def compose(cls, callback_query: CallbackQueryNode) -> dict[str, typing.Any]:
|
|
24
|
+
return callback_query.decode_callback_data().expect(
|
|
25
|
+
ComposeError("Cannot complete decode callback query data.")
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _Field(FactoryNode):
|
|
30
|
+
field_type: type[typing.Any]
|
|
31
|
+
|
|
32
|
+
def __class_getitem__(cls, field_type: type[typing.Any], /) -> typing.Self:
|
|
33
|
+
return cls(field_type=field_type)
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def compose(cls, callback_query_data: CallbackQueryData, data_name: Name) -> typing.Any:
|
|
37
|
+
if data := callback_query_data.get(data_name):
|
|
38
|
+
match msgspec_convert(data, cls.field_type):
|
|
39
|
+
case Ok(value):
|
|
40
|
+
return value
|
|
41
|
+
case Error(err):
|
|
42
|
+
raise ComposeError(err)
|
|
43
|
+
|
|
44
|
+
raise ComposeError(f"Cannot find callback data with name {data_name!r}.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if typing.TYPE_CHECKING:
|
|
48
|
+
Field = typing.Annotated[FieldType, ...]
|
|
49
|
+
else:
|
|
50
|
+
Field = _Field
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
__all__ = ("CallbackQueryData", "CallbackQueryNode", "Field")
|