telegrinder 0.3.3.post1__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.

Files changed (165) 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 +17 -17
  10. telegrinder/bot/cute_types/base.py +258 -258
  11. telegrinder/bot/cute_types/callback_query.py +385 -385
  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 +43 -43
  15. telegrinder/bot/cute_types/message.py +2637 -2637
  16. telegrinder/bot/cute_types/update.py +104 -109
  17. telegrinder/bot/cute_types/utils.py +95 -95
  18. telegrinder/bot/dispatch/__init__.py +55 -55
  19. telegrinder/bot/dispatch/abc.py +77 -77
  20. telegrinder/bot/dispatch/context.py +98 -98
  21. telegrinder/bot/dispatch/dispatch.py +202 -202
  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 +135 -135
  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 +22 -16
  35. telegrinder/bot/dispatch/process.py +157 -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 -200
  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 -114
  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 +55 -55
  55. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +57 -57
  56. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +51 -51
  57. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +19 -19
  58. telegrinder/bot/dispatch/waiter_machine/machine.py +172 -167
  59. telegrinder/bot/dispatch/waiter_machine/middleware.py +89 -89
  60. telegrinder/bot/dispatch/waiter_machine/short_state.py +68 -68
  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 +213 -213
  66. telegrinder/bot/rules/adapter/__init__.py +12 -9
  67. telegrinder/bot/rules/adapter/abc.py +31 -29
  68. telegrinder/bot/rules/adapter/errors.py +5 -5
  69. telegrinder/bot/rules/adapter/event.py +65 -67
  70. telegrinder/bot/rules/adapter/node.py +48 -48
  71. telegrinder/bot/rules/adapter/raw_event.py +27 -0
  72. telegrinder/bot/rules/adapter/raw_update.py +30 -30
  73. telegrinder/bot/rules/callback_data.py +170 -170
  74. telegrinder/bot/rules/chat_join.py +46 -46
  75. telegrinder/bot/rules/command.py +126 -126
  76. telegrinder/bot/rules/enum_text.py +36 -36
  77. telegrinder/bot/rules/func.py +26 -26
  78. telegrinder/bot/rules/fuzzy.py +24 -24
  79. telegrinder/bot/rules/inline.py +60 -60
  80. telegrinder/bot/rules/integer.py +20 -20
  81. telegrinder/bot/rules/is_from.py +127 -127
  82. telegrinder/bot/rules/markup.py +43 -43
  83. telegrinder/bot/rules/mention.py +14 -14
  84. telegrinder/bot/rules/message.py +17 -17
  85. telegrinder/bot/rules/message_entities.py +35 -35
  86. telegrinder/bot/rules/node.py +27 -27
  87. telegrinder/bot/rules/regex.py +37 -37
  88. telegrinder/bot/rules/rule_enum.py +72 -72
  89. telegrinder/bot/rules/start.py +42 -42
  90. telegrinder/bot/rules/state.py +37 -37
  91. telegrinder/bot/rules/text.py +33 -33
  92. telegrinder/bot/rules/update.py +15 -15
  93. telegrinder/bot/scenario/__init__.py +5 -5
  94. telegrinder/bot/scenario/abc.py +19 -19
  95. telegrinder/bot/scenario/checkbox.py +176 -167
  96. telegrinder/bot/scenario/choice.py +51 -46
  97. telegrinder/client/__init__.py +4 -4
  98. telegrinder/client/abc.py +75 -75
  99. telegrinder/client/aiohttp.py +130 -130
  100. telegrinder/model.py +320 -295
  101. telegrinder/modules.py +237 -237
  102. telegrinder/msgspec_json.py +14 -14
  103. telegrinder/msgspec_utils.py +410 -410
  104. telegrinder/node/__init__.py +0 -0
  105. telegrinder/node/attachment.py +87 -87
  106. telegrinder/node/base.py +166 -166
  107. telegrinder/node/callback_query.py +53 -53
  108. telegrinder/node/command.py +33 -33
  109. telegrinder/node/composer.py +198 -198
  110. telegrinder/node/container.py +27 -27
  111. telegrinder/node/event.py +65 -65
  112. telegrinder/node/me.py +16 -16
  113. telegrinder/node/message.py +14 -14
  114. telegrinder/node/polymorphic.py +48 -48
  115. telegrinder/node/rule.py +76 -76
  116. telegrinder/node/scope.py +38 -38
  117. telegrinder/node/source.py +71 -71
  118. telegrinder/node/text.py +41 -41
  119. telegrinder/node/tools/__init__.py +3 -3
  120. telegrinder/node/tools/generator.py +40 -40
  121. telegrinder/node/update.py +15 -15
  122. telegrinder/rules.py +0 -0
  123. telegrinder/tools/__init__.py +74 -74
  124. telegrinder/tools/buttons.py +79 -79
  125. telegrinder/tools/error_handler/__init__.py +7 -7
  126. telegrinder/tools/error_handler/abc.py +33 -33
  127. telegrinder/tools/error_handler/error.py +9 -9
  128. telegrinder/tools/error_handler/error_handler.py +193 -193
  129. telegrinder/tools/formatting/__init__.py +46 -46
  130. telegrinder/tools/formatting/html.py +283 -283
  131. telegrinder/tools/formatting/links.py +33 -33
  132. telegrinder/tools/formatting/spec_html_formats.py +111 -111
  133. telegrinder/tools/functional.py +12 -12
  134. telegrinder/tools/global_context/__init__.py +7 -7
  135. telegrinder/tools/global_context/abc.py +63 -63
  136. telegrinder/tools/global_context/global_context.py +412 -412
  137. telegrinder/tools/global_context/telegrinder_ctx.py +27 -27
  138. telegrinder/tools/i18n/__init__.py +7 -7
  139. telegrinder/tools/i18n/abc.py +30 -30
  140. telegrinder/tools/i18n/middleware/__init__.py +3 -3
  141. telegrinder/tools/i18n/middleware/abc.py +25 -25
  142. telegrinder/tools/i18n/simple.py +43 -43
  143. telegrinder/tools/kb_set/__init__.py +4 -4
  144. telegrinder/tools/kb_set/base.py +15 -15
  145. telegrinder/tools/kb_set/yaml.py +63 -63
  146. telegrinder/tools/keyboard.py +132 -132
  147. telegrinder/tools/limited_dict.py +37 -37
  148. telegrinder/tools/loop_wrapper/__init__.py +4 -4
  149. telegrinder/tools/loop_wrapper/abc.py +15 -15
  150. telegrinder/tools/loop_wrapper/loop_wrapper.py +224 -224
  151. telegrinder/tools/magic.py +157 -157
  152. telegrinder/tools/parse_mode.py +6 -6
  153. telegrinder/tools/state_storage/__init__.py +4 -4
  154. telegrinder/tools/state_storage/abc.py +35 -35
  155. telegrinder/tools/state_storage/memory.py +25 -25
  156. telegrinder/types/__init__.py +260 -260
  157. telegrinder/types/enums.py +701 -701
  158. telegrinder/types/methods.py +4633 -4633
  159. telegrinder/types/objects.py +6950 -8561
  160. telegrinder/verification_utils.py +32 -32
  161. {telegrinder-0.3.3.post1.dist-info → telegrinder-0.3.4.post1.dist-info}/LICENSE +22 -22
  162. {telegrinder-0.3.3.post1.dist-info → telegrinder-0.3.4.post1.dist-info}/METADATA +1 -1
  163. telegrinder-0.3.4.post1.dist-info/RECORD +165 -0
  164. telegrinder-0.3.3.post1.dist-info/RECORD +0 -164
  165. {telegrinder-0.3.3.post1.dist-info → telegrinder-0.3.4.post1.dist-info}/WHEEL +0 -0
File without changes
@@ -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,166 +1,166 @@
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
- "ComposeError",
157
- "FactoryNode",
158
- "DataNode",
159
- "Name",
160
- "Node",
161
- "SCALAR_NODE",
162
- "ScalarNode",
163
- "ScalarNodeProto",
164
- "get_nodes",
165
- "is_node",
166
- )
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
+ "ComposeError",
157
+ "DataNode",
158
+ "FactoryNode",
159
+ "Name",
160
+ "Node",
161
+ "SCALAR_NODE",
162
+ "ScalarNode",
163
+ "ScalarNodeProto",
164
+ "get_nodes",
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")