telegrinder 0.3.1__py3-none-any.whl → 0.3.2__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.

Files changed (164) hide show
  1. telegrinder/__init__.py +144 -144
  2. telegrinder/api/__init__.py +8 -8
  3. telegrinder/api/api.py +93 -93
  4. telegrinder/api/error.py +16 -16
  5. telegrinder/api/response.py +20 -20
  6. telegrinder/api/token.py +36 -36
  7. telegrinder/bot/__init__.py +66 -66
  8. telegrinder/bot/bot.py +76 -76
  9. telegrinder/bot/cute_types/__init__.py +11 -11
  10. telegrinder/bot/cute_types/base.py +258 -234
  11. telegrinder/bot/cute_types/callback_query.py +382 -382
  12. telegrinder/bot/cute_types/chat_join_request.py +61 -61
  13. telegrinder/bot/cute_types/chat_member_updated.py +160 -160
  14. telegrinder/bot/cute_types/inline_query.py +53 -53
  15. telegrinder/bot/cute_types/message.py +2631 -2631
  16. telegrinder/bot/cute_types/update.py +75 -75
  17. telegrinder/bot/cute_types/utils.py +92 -92
  18. telegrinder/bot/dispatch/__init__.py +55 -55
  19. telegrinder/bot/dispatch/abc.py +77 -77
  20. telegrinder/bot/dispatch/context.py +92 -92
  21. telegrinder/bot/dispatch/dispatch.py +202 -201
  22. telegrinder/bot/dispatch/handler/__init__.py +13 -13
  23. telegrinder/bot/dispatch/handler/abc.py +24 -24
  24. telegrinder/bot/dispatch/handler/audio_reply.py +44 -44
  25. telegrinder/bot/dispatch/handler/base.py +57 -57
  26. telegrinder/bot/dispatch/handler/document_reply.py +44 -44
  27. telegrinder/bot/dispatch/handler/func.py +128 -123
  28. telegrinder/bot/dispatch/handler/media_group_reply.py +43 -43
  29. telegrinder/bot/dispatch/handler/message_reply.py +36 -36
  30. telegrinder/bot/dispatch/handler/photo_reply.py +44 -44
  31. telegrinder/bot/dispatch/handler/sticker_reply.py +37 -37
  32. telegrinder/bot/dispatch/handler/video_reply.py +44 -44
  33. telegrinder/bot/dispatch/middleware/__init__.py +3 -3
  34. telegrinder/bot/dispatch/middleware/abc.py +16 -16
  35. telegrinder/bot/dispatch/process.py +132 -132
  36. telegrinder/bot/dispatch/return_manager/__init__.py +13 -13
  37. telegrinder/bot/dispatch/return_manager/abc.py +108 -108
  38. telegrinder/bot/dispatch/return_manager/callback_query.py +20 -20
  39. telegrinder/bot/dispatch/return_manager/inline_query.py +15 -15
  40. telegrinder/bot/dispatch/return_manager/message.py +36 -36
  41. telegrinder/bot/dispatch/view/__init__.py +13 -13
  42. telegrinder/bot/dispatch/view/abc.py +41 -41
  43. telegrinder/bot/dispatch/view/base.py +200 -211
  44. telegrinder/bot/dispatch/view/box.py +129 -129
  45. telegrinder/bot/dispatch/view/callback_query.py +17 -17
  46. telegrinder/bot/dispatch/view/chat_join_request.py +16 -16
  47. telegrinder/bot/dispatch/view/chat_member.py +39 -39
  48. telegrinder/bot/dispatch/view/inline_query.py +17 -17
  49. telegrinder/bot/dispatch/view/message.py +44 -44
  50. telegrinder/bot/dispatch/view/raw.py +114 -118
  51. telegrinder/bot/dispatch/waiter_machine/__init__.py +17 -17
  52. telegrinder/bot/dispatch/waiter_machine/actions.py +13 -13
  53. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +8 -8
  54. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +57 -57
  55. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +57 -57
  56. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +53 -53
  57. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +19 -19
  58. telegrinder/bot/dispatch/waiter_machine/machine.py +168 -170
  59. telegrinder/bot/dispatch/waiter_machine/middleware.py +89 -89
  60. telegrinder/bot/dispatch/waiter_machine/short_state.py +65 -65
  61. telegrinder/bot/polling/__init__.py +4 -4
  62. telegrinder/bot/polling/abc.py +25 -25
  63. telegrinder/bot/polling/polling.py +131 -131
  64. telegrinder/bot/rules/__init__.py +62 -62
  65. telegrinder/bot/rules/abc.py +238 -238
  66. telegrinder/bot/rules/adapter/__init__.py +9 -9
  67. telegrinder/bot/rules/adapter/abc.py +29 -29
  68. telegrinder/bot/rules/adapter/errors.py +5 -5
  69. telegrinder/bot/rules/adapter/event.py +76 -76
  70. telegrinder/bot/rules/adapter/node.py +48 -48
  71. telegrinder/bot/rules/adapter/raw_update.py +30 -30
  72. telegrinder/bot/rules/callback_data.py +171 -171
  73. telegrinder/bot/rules/chat_join.py +48 -48
  74. telegrinder/bot/rules/command.py +126 -126
  75. telegrinder/bot/rules/enum_text.py +36 -36
  76. telegrinder/bot/rules/func.py +26 -26
  77. telegrinder/bot/rules/fuzzy.py +24 -24
  78. telegrinder/bot/rules/inline.py +60 -60
  79. telegrinder/bot/rules/integer.py +20 -20
  80. telegrinder/bot/rules/is_from.py +146 -146
  81. telegrinder/bot/rules/markup.py +43 -43
  82. telegrinder/bot/rules/mention.py +14 -14
  83. telegrinder/bot/rules/message.py +17 -17
  84. telegrinder/bot/rules/message_entities.py +35 -35
  85. telegrinder/bot/rules/node.py +27 -27
  86. telegrinder/bot/rules/regex.py +37 -37
  87. telegrinder/bot/rules/rule_enum.py +72 -72
  88. telegrinder/bot/rules/start.py +42 -42
  89. telegrinder/bot/rules/state.py +37 -37
  90. telegrinder/bot/rules/text.py +33 -33
  91. telegrinder/bot/rules/update.py +15 -15
  92. telegrinder/bot/scenario/__init__.py +5 -5
  93. telegrinder/bot/scenario/abc.py +19 -19
  94. telegrinder/bot/scenario/checkbox.py +167 -147
  95. telegrinder/bot/scenario/choice.py +46 -44
  96. telegrinder/client/__init__.py +4 -4
  97. telegrinder/client/abc.py +75 -75
  98. telegrinder/client/aiohttp.py +130 -130
  99. telegrinder/model.py +244 -244
  100. telegrinder/modules.py +237 -237
  101. telegrinder/msgspec_json.py +14 -14
  102. telegrinder/msgspec_utils.py +410 -410
  103. telegrinder/node/__init__.py +20 -20
  104. telegrinder/node/attachment.py +92 -92
  105. telegrinder/node/base.py +143 -144
  106. telegrinder/node/callback_query.py +14 -14
  107. telegrinder/node/command.py +33 -33
  108. telegrinder/node/composer.py +196 -184
  109. telegrinder/node/container.py +27 -27
  110. telegrinder/node/event.py +71 -73
  111. telegrinder/node/me.py +16 -16
  112. telegrinder/node/message.py +14 -14
  113. telegrinder/node/polymorphic.py +48 -52
  114. telegrinder/node/rule.py +76 -76
  115. telegrinder/node/scope.py +38 -38
  116. telegrinder/node/source.py +71 -71
  117. telegrinder/node/text.py +21 -21
  118. telegrinder/node/tools/__init__.py +3 -3
  119. telegrinder/node/tools/generator.py +40 -40
  120. telegrinder/node/update.py +15 -15
  121. telegrinder/rules.py +0 -0
  122. telegrinder/tools/__init__.py +74 -74
  123. telegrinder/tools/buttons.py +79 -79
  124. telegrinder/tools/error_handler/__init__.py +7 -7
  125. telegrinder/tools/error_handler/abc.py +33 -33
  126. telegrinder/tools/error_handler/error.py +9 -9
  127. telegrinder/tools/error_handler/error_handler.py +193 -193
  128. telegrinder/tools/formatting/__init__.py +46 -46
  129. telegrinder/tools/formatting/html.py +308 -308
  130. telegrinder/tools/formatting/links.py +33 -33
  131. telegrinder/tools/formatting/spec_html_formats.py +111 -111
  132. telegrinder/tools/functional.py +12 -12
  133. telegrinder/tools/global_context/__init__.py +7 -7
  134. telegrinder/tools/global_context/abc.py +63 -63
  135. telegrinder/tools/global_context/global_context.py +412 -412
  136. telegrinder/tools/global_context/telegrinder_ctx.py +27 -27
  137. telegrinder/tools/i18n/__init__.py +12 -12
  138. telegrinder/tools/i18n/abc.py +32 -32
  139. telegrinder/tools/i18n/middleware/__init__.py +3 -3
  140. telegrinder/tools/i18n/middleware/abc.py +25 -25
  141. telegrinder/tools/i18n/simple.py +43 -43
  142. telegrinder/tools/kb_set/__init__.py +4 -4
  143. telegrinder/tools/kb_set/base.py +15 -15
  144. telegrinder/tools/kb_set/yaml.py +63 -63
  145. telegrinder/tools/keyboard.py +128 -128
  146. telegrinder/tools/limited_dict.py +37 -37
  147. telegrinder/tools/loop_wrapper/__init__.py +4 -4
  148. telegrinder/tools/loop_wrapper/abc.py +15 -15
  149. telegrinder/tools/loop_wrapper/loop_wrapper.py +216 -216
  150. telegrinder/tools/magic.py +168 -168
  151. telegrinder/tools/parse_mode.py +6 -6
  152. telegrinder/tools/state_storage/__init__.py +4 -4
  153. telegrinder/tools/state_storage/abc.py +35 -35
  154. telegrinder/tools/state_storage/memory.py +25 -25
  155. telegrinder/types/__init__.py +6 -6
  156. telegrinder/types/enums.py +672 -672
  157. telegrinder/types/methods.py +4633 -4633
  158. telegrinder/types/objects.py +6317 -6317
  159. telegrinder/verification_utils.py +32 -32
  160. {telegrinder-0.3.1.dist-info → telegrinder-0.3.2.dist-info}/LICENSE +22 -22
  161. {telegrinder-0.3.1.dist-info → telegrinder-0.3.2.dist-info}/METADATA +1 -1
  162. telegrinder-0.3.2.dist-info/RECORD +164 -0
  163. telegrinder-0.3.1.dist-info/RECORD +0 -164
  164. {telegrinder-0.3.1.dist-info → telegrinder-0.3.2.dist-info}/WHEEL +0 -0
@@ -1,21 +1,21 @@
1
- from .attachment import Attachment, Audio, Photo, Video
2
- from .base import ComposeError, DataNode, Node, ScalarNode, is_node
3
- from .callback_query import CallbackQueryNode
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
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, Node, ScalarNode, is_node
3
+ from .callback_query import CallbackQueryNode
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
15
+ from .tools import generate_node
16
+ from .update import UpdateNode
17
+
18
+ __all__ = (
19
19
  "Attachment",
20
20
  "Audio",
21
21
  "CallbackQueryNode",
@@ -52,5 +52,5 @@ __all__ = (
52
52
  "impl",
53
53
  "is_node",
54
54
  "per_call",
55
- "per_event",
56
- )
55
+ "per_event",
56
+ )
@@ -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__ = (
86
- "Attachment",
87
- "Audio",
88
- "Document",
89
- "Photo",
90
- "Poll",
91
- "Video",
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__ = (
86
+ "Attachment",
87
+ "Audio",
88
+ "Document",
89
+ "Photo",
90
+ "Poll",
91
+ "Video",
92
+ )
telegrinder/node/base.py CHANGED
@@ -1,144 +1,143 @@
1
- import abc
2
- import inspect
3
- import typing
4
- from types import AsyncGeneratorType
5
-
6
- from telegrinder.node.scope import NodeScope
7
- from telegrinder.tools.magic import cache_magic_value, get_annotations
8
-
9
- ComposeResult: typing.TypeAlias = (
10
- typing.Awaitable[typing.Any] | typing.AsyncGenerator[typing.Any, None] | typing.Any
11
- )
12
-
13
-
14
- def is_node(maybe_node: typing.Any) -> typing.TypeGuard[type["Node"]]:
15
- maybe_node = maybe_node if isinstance(maybe_node, type) else typing.get_origin(maybe_node)
16
- return (
17
- isinstance(maybe_node, type)
18
- and issubclass(maybe_node, Node)
19
- or isinstance(maybe_node, Node)
20
- or hasattr(maybe_node, "as_node")
21
- )
22
-
23
-
24
- @cache_magic_value("__nodes__")
25
- def get_nodes(function: typing.Callable[..., typing.Any]) -> dict[str, type["Node"]]:
26
- return {k: v for k, v in get_annotations(function).items() if is_node(v)}
27
-
28
-
29
- @cache_magic_value("__is_generator__")
30
- def is_generator(
31
- function: typing.Callable[..., typing.Any],
32
- ) -> typing.TypeGuard[AsyncGeneratorType[typing.Any, None]]:
33
- return inspect.isasyncgenfunction(function)
34
-
35
-
36
- def get_node_calc_lst(node: type["Node"]) -> list[type["Node"]]:
37
- """Returns flattened list of node types in ordering required to calculate given node.
38
- Provides caching for passed node type"""
39
-
40
- if calc_lst := getattr(node, "__nodes_calc_lst__", None):
41
- return calc_lst
42
- nodes_lst: list[type["Node"]] = []
43
- annotations = list(node.as_node().get_subnodes().values())
44
- for node_type in annotations:
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
- class DataNode(Node, abc.ABC):
82
- node = "data"
83
-
84
- @typing.dataclass_transform()
85
- @classmethod
86
- @abc.abstractmethod
87
- async def compose(cls, *args, **kwargs) -> ComposeResult:
88
- pass
89
-
90
-
91
- class ScalarNodeProto(Node, abc.ABC):
92
- @classmethod
93
- @abc.abstractmethod
94
- async def compose(cls, *args, **kwargs) -> ComposeResult:
95
- pass
96
-
97
-
98
- SCALAR_NODE = type("ScalarNode", (), {"node": "scalar"})
99
-
100
-
101
- if typing.TYPE_CHECKING:
102
-
103
- class ScalarNode(ScalarNodeProto, abc.ABC):
104
- pass
105
-
106
- else:
107
-
108
- def __init_subclass__(cls, *args, **kwargs): # noqa: N807
109
- if any(issubclass(base, ScalarNodeProto) for base in cls.__bases__ if base is not ScalarNode):
110
- raise RuntimeError("Scalar nodes do not support inheritance.")
111
-
112
- def _as_node(cls, bases, dct):
113
- if not hasattr(cls, "_scalar_node_type"):
114
- dct.update(cls.__dict__)
115
- scalar_node_type = type(cls.__name__, bases, dct)
116
- setattr(cls, "_scalar_node_type", scalar_node_type)
117
- return scalar_node_type
118
- return getattr(cls, "_scalar_node_type")
119
-
120
- def create_class(name, bases, dct):
121
- return type(
122
- "Scalar",
123
- (SCALAR_NODE,),
124
- {
125
- "as_node": classmethod(lambda cls: _as_node(cls, bases, dct)),
126
- "scope": Node.scope,
127
- "__init_subclass__": __init_subclass__,
128
- },
129
- )
130
-
131
- class ScalarNode(ScalarNodeProto, abc.ABC, metaclass=create_class):
132
- pass
133
-
134
-
135
- __all__ = (
136
- "ComposeError",
137
- "DataNode",
138
- "Node",
139
- "SCALAR_NODE",
140
- "ScalarNode",
141
- "ScalarNodeProto",
142
- "get_nodes",
143
- "is_node",
144
- )
1
+ import abc
2
+ import inspect
3
+ import typing
4
+ from types import AsyncGeneratorType
5
+
6
+ from telegrinder.node.scope import NodeScope
7
+ from telegrinder.tools.magic import cache_magic_value, get_annotations
8
+
9
+ ComposeResult: typing.TypeAlias = (
10
+ typing.Awaitable[typing.Any] | typing.AsyncGenerator[typing.Any, None] | typing.Any
11
+ )
12
+
13
+
14
+ def is_node(maybe_node: typing.Any) -> typing.TypeGuard[type["Node"]]:
15
+ maybe_node = maybe_node if isinstance(maybe_node, type) else typing.get_origin(maybe_node)
16
+ return (
17
+ isinstance(maybe_node, type)
18
+ and issubclass(maybe_node, Node)
19
+ or isinstance(maybe_node, Node)
20
+ or hasattr(maybe_node, "as_node")
21
+ )
22
+
23
+
24
+ @cache_magic_value("__nodes__")
25
+ def get_nodes(function: typing.Callable[..., typing.Any]) -> dict[str, type["Node"]]:
26
+ return {k: v for k, v in get_annotations(function).items() if is_node(v)}
27
+
28
+
29
+ @cache_magic_value("__is_generator__")
30
+ def is_generator(
31
+ function: typing.Callable[..., typing.Any],
32
+ ) -> typing.TypeGuard[AsyncGeneratorType[typing.Any, None]]:
33
+ return inspect.isasyncgenfunction(function)
34
+
35
+
36
+ def get_node_calc_lst(node: type["Node"]) -> list[type["Node"]]:
37
+ """Returns flattened list of node types in ordering required to calculate given node.
38
+ Provides caching for passed node type."""
39
+
40
+ if calc_lst := getattr(node, "__nodes_calc_lst__", None):
41
+ return calc_lst
42
+ nodes_lst: list[type[Node]] = []
43
+ for node_type in node.as_node().get_subnodes().values():
44
+ nodes_lst.extend(get_node_calc_lst(node_type))
45
+ calc_lst = [*nodes_lst, node]
46
+ setattr(node, "__nodes_calc_lst__", calc_lst)
47
+ return calc_lst
48
+
49
+
50
+ class ComposeError(BaseException):
51
+ pass
52
+
53
+
54
+ class Node(abc.ABC):
55
+ node: str = "node"
56
+ scope: NodeScope = NodeScope.PER_EVENT
57
+
58
+ @classmethod
59
+ @abc.abstractmethod
60
+ def compose(cls, *args, **kwargs) -> ComposeResult:
61
+ pass
62
+
63
+ @classmethod
64
+ def compose_error(cls, error: str | None = None) -> typing.NoReturn:
65
+ raise ComposeError(error)
66
+
67
+ @classmethod
68
+ def get_subnodes(cls) -> dict[str, type["Node"]]:
69
+ return get_nodes(cls.compose)
70
+
71
+ @classmethod
72
+ def as_node(cls) -> type[typing.Self]:
73
+ return cls
74
+
75
+ @classmethod
76
+ def is_generator(cls) -> bool:
77
+ return is_generator(cls.compose)
78
+
79
+
80
+ class DataNode(Node, abc.ABC):
81
+ node = "data"
82
+
83
+ @typing.dataclass_transform()
84
+ @classmethod
85
+ @abc.abstractmethod
86
+ async def compose(cls, *args, **kwargs) -> ComposeResult:
87
+ pass
88
+
89
+
90
+ class ScalarNodeProto(Node, abc.ABC):
91
+ @classmethod
92
+ @abc.abstractmethod
93
+ async def compose(cls, *args, **kwargs) -> ComposeResult:
94
+ pass
95
+
96
+
97
+ SCALAR_NODE = type("ScalarNode", (), {"node": "scalar"})
98
+
99
+
100
+ if typing.TYPE_CHECKING:
101
+
102
+ class ScalarNode(ScalarNodeProto, abc.ABC):
103
+ pass
104
+
105
+ else:
106
+
107
+ def __init_subclass__(cls, *args, **kwargs): # noqa: N807
108
+ if any(issubclass(base, ScalarNodeProto) for base in cls.__bases__ if base is not ScalarNode):
109
+ raise RuntimeError("Scalar nodes do not support inheritance.")
110
+
111
+ def _as_node(cls, bases, dct):
112
+ if not hasattr(cls, "_scalar_node_type"):
113
+ dct.update(cls.__dict__)
114
+ scalar_node_type = type(cls.__name__, bases, dct)
115
+ setattr(cls, "_scalar_node_type", scalar_node_type)
116
+ return scalar_node_type
117
+ return getattr(cls, "_scalar_node_type")
118
+
119
+ def create_class(name, bases, dct):
120
+ return type(
121
+ "Scalar",
122
+ (SCALAR_NODE,),
123
+ {
124
+ "as_node": classmethod(lambda cls: _as_node(cls, bases, dct)),
125
+ "scope": Node.scope,
126
+ "__init_subclass__": __init_subclass__,
127
+ },
128
+ )
129
+
130
+ class ScalarNode(ScalarNodeProto, abc.ABC, metaclass=create_class):
131
+ pass
132
+
133
+
134
+ __all__ = (
135
+ "ComposeError",
136
+ "DataNode",
137
+ "Node",
138
+ "SCALAR_NODE",
139
+ "ScalarNode",
140
+ "ScalarNodeProto",
141
+ "get_nodes",
142
+ "is_node",
143
+ )
@@ -1,14 +1,14 @@
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
4
-
5
-
6
- class CallbackQueryNode(ScalarNode, CallbackQueryCute):
7
- @classmethod
8
- def compose(cls, update: UpdateNode) -> CallbackQueryCute:
9
- if not update.callback_query:
10
- raise ComposeError("Update is not a callback_query.")
11
- return update.callback_query.unwrap()
12
-
13
-
14
- __all__ = ("CallbackQueryNode",)
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
4
+
5
+
6
+ class CallbackQueryNode(ScalarNode, CallbackQueryCute):
7
+ @classmethod
8
+ def compose(cls, update: UpdateNode) -> CallbackQueryCute:
9
+ if not update.callback_query:
10
+ raise ComposeError("Update is not a callback_query.")
11
+ return update.callback_query.unwrap()
12
+
13
+
14
+ __all__ = ("CallbackQueryNode",)
@@ -1,33 +1,33 @@
1
- import typing
2
- from dataclasses import dataclass, field
3
-
4
- from fntypes.option import Nothing, Option, Some
5
-
6
- from telegrinder.node.base import DataNode
7
- from telegrinder.node.text import Text
8
-
9
-
10
- def single_split(s: str, separator: str) -> tuple[str, str]:
11
- left, *right = s.split(separator, 1)
12
- return left, (right[0] if right else "")
13
-
14
-
15
- def cut_mention(text: str) -> tuple[str, Option[str]]:
16
- left, right = single_split(text, "@")
17
- return left, Some(right) if right else Nothing()
18
-
19
-
20
- @dataclass(slots=True)
21
- class CommandInfo(DataNode):
22
- name: str
23
- arguments: str
24
- mention: Option[str] = field(default_factory=Nothing)
25
-
26
- @classmethod
27
- def compose(cls, text: Text) -> typing.Self:
28
- name, arguments = single_split(text, separator=" ")
29
- name, mention = cut_mention(name)
30
- return cls(name, arguments, mention)
31
-
32
-
33
- __all__ = ("CommandInfo", "cut_mention", "single_split")
1
+ import typing
2
+ from dataclasses import dataclass, field
3
+
4
+ from fntypes.option import Nothing, Option, Some
5
+
6
+ from telegrinder.node.base import DataNode
7
+ from telegrinder.node.text import Text
8
+
9
+
10
+ def single_split(s: str, separator: str) -> tuple[str, str]:
11
+ left, *right = s.split(separator, 1)
12
+ return left, (right[0] if right else "")
13
+
14
+
15
+ def cut_mention(text: str) -> tuple[str, Option[str]]:
16
+ left, right = single_split(text, "@")
17
+ return left, Some(right) if right else Nothing()
18
+
19
+
20
+ @dataclass(slots=True)
21
+ class CommandInfo(DataNode):
22
+ name: str
23
+ arguments: str
24
+ mention: Option[str] = field(default_factory=Nothing)
25
+
26
+ @classmethod
27
+ def compose(cls, text: Text) -> typing.Self:
28
+ name, arguments = single_split(text, separator=" ")
29
+ name, mention = cut_mention(name)
30
+ return cls(name, arguments, mention)
31
+
32
+
33
+ __all__ = ("CommandInfo", "cut_mention", "single_split")