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.

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 -104
  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 -22
  35. telegrinder/bot/dispatch/process.py +157 -157
  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 -172
  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 +206 -206
  66. telegrinder/bot/rules/adapter/__init__.py +17 -17
  67. telegrinder/bot/rules/adapter/abc.py +31 -31
  68. telegrinder/bot/rules/adapter/errors.py +5 -5
  69. telegrinder/bot/rules/adapter/event.py +65 -65
  70. telegrinder/bot/rules/adapter/node.py +48 -48
  71. telegrinder/bot/rules/adapter/raw_event.py +27 -27
  72. telegrinder/bot/rules/adapter/raw_update.py +30 -30
  73. telegrinder/bot/rules/callback_data.py +163 -163
  74. telegrinder/bot/rules/chat_join.py +43 -43
  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 +56 -56
  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 -176
  96. telegrinder/bot/scenario/choice.py +51 -51
  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 +313 -313
  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 +20 -20
  105. telegrinder/node/attachment.py +87 -87
  106. telegrinder/node/base.py +157 -157
  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 +5 -5
  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 +128 -128
  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 -6950
  160. telegrinder/verification_utils.py +32 -32
  161. {telegrinder-0.3.4.dist-info → telegrinder-0.3.4.post1.dist-info}/LICENSE +22 -22
  162. {telegrinder-0.3.4.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.4.dist-info/RECORD +0 -165
  165. {telegrinder-0.3.4.dist-info → telegrinder-0.3.4.post1.dist-info}/WHEEL +0 -0
@@ -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")
@@ -1,198 +1,198 @@
1
- import dataclasses
2
- import inspect
3
- import typing
4
-
5
- from fntypes.error import UnwrapError
6
- from fntypes.result import Error, Ok, Result
7
-
8
- from telegrinder.api.api import API
9
- from telegrinder.bot.cute_types.update import Update, UpdateCute
10
- from telegrinder.bot.dispatch.context import Context
11
- from telegrinder.modules import logger
12
- from telegrinder.node.base import (
13
- ComposeError,
14
- Name,
15
- Node,
16
- NodeScope,
17
- get_node_calc_lst,
18
- get_nodes,
19
- )
20
- from telegrinder.tools.magic import magic_bundle
21
-
22
- CONTEXT_STORE_NODES_KEY = "_node_ctx"
23
- GLOBAL_VALUE_KEY = "_value"
24
-
25
-
26
- async def compose_node(
27
- _node: type[Node],
28
- linked: dict[type, typing.Any],
29
- ) -> "NodeSession":
30
- node = _node.as_node()
31
- kwargs = magic_bundle(node.compose, linked, typebundle=True)
32
-
33
- if node.is_generator():
34
- generator = typing.cast(typing.AsyncGenerator[typing.Any, None], node.compose(**kwargs))
35
- value = await generator.asend(None)
36
- else:
37
- generator = None
38
- value = typing.cast(typing.Awaitable[typing.Any] | typing.Any, node.compose(**kwargs))
39
- if inspect.isawaitable(value):
40
- value = await value
41
-
42
- return NodeSession(_node, value, {}, generator)
43
-
44
-
45
- async def compose_nodes(
46
- nodes: dict[str, type[Node]],
47
- ctx: Context,
48
- data: dict[type[typing.Any], typing.Any] | None = None,
49
- ) -> Result["NodeCollection", ComposeError]:
50
- logger.debug("Composing nodes: {!r}...", nodes)
51
-
52
- local_nodes: dict[type[Node], NodeSession]
53
- data = {Context: ctx} | (data or {})
54
- parent_nodes: dict[type[Node], NodeSession] = {}
55
- event_nodes: dict[type[Node], NodeSession] = ctx.get_or_set(CONTEXT_STORE_NODES_KEY, {})
56
- # TODO: optimize flattened list calculation via caching key = tuple of node types
57
- calculation_nodes: dict[tuple[str, type[Node]], tuple[type[Node], ...]] = {
58
- (node_name, node_t): tuple(get_node_calc_lst(node_t)) for node_name, node_t in nodes.items()
59
- }
60
-
61
- for (parent_node_name, parent_node_t), linked_nodes in calculation_nodes.items():
62
- local_nodes = {}
63
- subnodes = {}
64
- data[Name] = parent_node_name
65
-
66
- for node_t in linked_nodes:
67
- scope = getattr(node_t, "scope", None)
68
-
69
- if scope is NodeScope.PER_EVENT and node_t in event_nodes:
70
- local_nodes[node_t] = event_nodes[node_t]
71
- continue
72
- elif scope is NodeScope.GLOBAL and hasattr(node_t, GLOBAL_VALUE_KEY):
73
- local_nodes[node_t] = getattr(node_t, GLOBAL_VALUE_KEY)
74
- continue
75
-
76
- subnodes |= {
77
- k: session.value for k, session in (local_nodes | event_nodes).items() if k not in subnodes
78
- }
79
-
80
- try:
81
- local_nodes[node_t] = await compose_node(node_t, subnodes | data)
82
- except (ComposeError, UnwrapError) as exc:
83
- for t, local_node in local_nodes.items():
84
- if t.scope is NodeScope.PER_CALL:
85
- await local_node.close()
86
- return Error(ComposeError(f"Cannot compose {node_t}. Error: {exc}"))
87
-
88
- if scope is NodeScope.PER_EVENT:
89
- event_nodes[node_t] = local_nodes[node_t]
90
- elif scope is NodeScope.GLOBAL:
91
- setattr(node_t, GLOBAL_VALUE_KEY, local_nodes[node_t])
92
-
93
- parent_nodes[parent_node_t] = local_nodes[parent_node_t]
94
-
95
- node_sessions = {k: parent_nodes[t] for k, t in nodes.items()}
96
- return Ok(NodeCollection(node_sessions))
97
-
98
-
99
- @dataclasses.dataclass(slots=True, repr=False)
100
- class NodeSession:
101
- node_type: type[Node] | None
102
- value: typing.Any
103
- subnodes: dict[str, typing.Self]
104
- generator: typing.AsyncGenerator[typing.Any, typing.Any | None] | None = None
105
-
106
- def __repr__(self) -> str:
107
- return f"<{self.__class__.__name__}: {self.value!r}" + (" (ACTIVE)>" if self.generator else ">")
108
-
109
- async def close(
110
- self,
111
- with_value: typing.Any | None = None,
112
- scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
113
- ) -> None:
114
- if self.node_type and getattr(self.node_type, "scope", None) not in scopes:
115
- return
116
-
117
- for subnode in self.subnodes.values():
118
- await subnode.close(scopes=scopes)
119
-
120
- if self.generator is None:
121
- return
122
- try:
123
- logger.debug("Closing session for node {!r}...", self.node_type)
124
- await self.generator.asend(with_value)
125
- except StopAsyncIteration:
126
- self.generator = None
127
-
128
-
129
- class NodeCollection:
130
- __slots__ = ("sessions", "_values")
131
-
132
- def __init__(self, sessions: dict[str, NodeSession]) -> None:
133
- self.sessions = sessions
134
- self._values: dict[str, typing.Any] = {}
135
-
136
- def __repr__(self) -> str:
137
- return "<{}: sessions={!r}>".format(self.__class__.__name__, self.sessions)
138
-
139
- @property
140
- def values(self) -> dict[str, typing.Any]:
141
- if self._values.keys() == self.sessions.keys():
142
- return self._values
143
-
144
- for name, session in self.sessions.items():
145
- if name not in self._values:
146
- self._values[name] = session.value
147
-
148
- return self._values
149
-
150
- async def close_all(
151
- self,
152
- with_value: typing.Any | None = None,
153
- scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
154
- ) -> None:
155
- for session in self.sessions.values():
156
- await session.close(with_value, scopes=scopes)
157
-
158
-
159
- @dataclasses.dataclass(slots=True, repr=False)
160
- class Composition:
161
- func: typing.Callable[..., typing.Any]
162
- is_blocking: bool
163
- nodes: dict[str, type[Node]] = dataclasses.field(init=False)
164
-
165
- def __post_init__(self) -> None:
166
- self.nodes = get_nodes(self.func)
167
-
168
- def __repr__(self) -> str:
169
- return "<{}: for function={!r} with nodes={!r}>".format(
170
- ("blocking " if self.is_blocking else "") + self.__class__.__name__,
171
- self.func.__qualname__,
172
- self.nodes,
173
- )
174
-
175
- async def compose_nodes(
176
- self,
177
- update: UpdateCute,
178
- context: Context,
179
- ) -> NodeCollection | None:
180
- match await compose_nodes(
181
- nodes=self.nodes,
182
- ctx=context,
183
- data={Update: update, API: update.api},
184
- ):
185
- case Ok(col):
186
- return col
187
- case Error(err):
188
- logger.debug(f"Composition failed with error: {err!r}")
189
- return None
190
-
191
- async def __call__(self, node_cls: type[Node], **kwargs: typing.Any) -> typing.Any:
192
- result = self.func(node_cls, **magic_bundle(self.func, kwargs, start_idx=0, bundle_ctx=False))
193
- if inspect.isawaitable(result):
194
- result = await result
195
- return result
196
-
197
-
198
- __all__ = ("Composition", "NodeCollection", "NodeSession", "compose_node", "compose_nodes")
1
+ import dataclasses
2
+ import inspect
3
+ import typing
4
+
5
+ from fntypes.error import UnwrapError
6
+ from fntypes.result import Error, Ok, Result
7
+
8
+ from telegrinder.api.api import API
9
+ from telegrinder.bot.cute_types.update import Update, UpdateCute
10
+ from telegrinder.bot.dispatch.context import Context
11
+ from telegrinder.modules import logger
12
+ from telegrinder.node.base import (
13
+ ComposeError,
14
+ Name,
15
+ Node,
16
+ NodeScope,
17
+ get_node_calc_lst,
18
+ get_nodes,
19
+ )
20
+ from telegrinder.tools.magic import magic_bundle
21
+
22
+ CONTEXT_STORE_NODES_KEY = "_node_ctx"
23
+ GLOBAL_VALUE_KEY = "_value"
24
+
25
+
26
+ async def compose_node(
27
+ _node: type[Node],
28
+ linked: dict[type, typing.Any],
29
+ ) -> "NodeSession":
30
+ node = _node.as_node()
31
+ kwargs = magic_bundle(node.compose, linked, typebundle=True)
32
+
33
+ if node.is_generator():
34
+ generator = typing.cast(typing.AsyncGenerator[typing.Any, None], node.compose(**kwargs))
35
+ value = await generator.asend(None)
36
+ else:
37
+ generator = None
38
+ value = typing.cast(typing.Awaitable[typing.Any] | typing.Any, node.compose(**kwargs))
39
+ if inspect.isawaitable(value):
40
+ value = await value
41
+
42
+ return NodeSession(_node, value, {}, generator)
43
+
44
+
45
+ async def compose_nodes(
46
+ nodes: dict[str, type[Node]],
47
+ ctx: Context,
48
+ data: dict[type[typing.Any], typing.Any] | None = None,
49
+ ) -> Result["NodeCollection", ComposeError]:
50
+ logger.debug("Composing nodes: {!r}...", nodes)
51
+
52
+ local_nodes: dict[type[Node], NodeSession]
53
+ data = {Context: ctx} | (data or {})
54
+ parent_nodes: dict[type[Node], NodeSession] = {}
55
+ event_nodes: dict[type[Node], NodeSession] = ctx.get_or_set(CONTEXT_STORE_NODES_KEY, {})
56
+ # TODO: optimize flattened list calculation via caching key = tuple of node types
57
+ calculation_nodes: dict[tuple[str, type[Node]], tuple[type[Node], ...]] = {
58
+ (node_name, node_t): tuple(get_node_calc_lst(node_t)) for node_name, node_t in nodes.items()
59
+ }
60
+
61
+ for (parent_node_name, parent_node_t), linked_nodes in calculation_nodes.items():
62
+ local_nodes = {}
63
+ subnodes = {}
64
+ data[Name] = parent_node_name
65
+
66
+ for node_t in linked_nodes:
67
+ scope = getattr(node_t, "scope", None)
68
+
69
+ if scope is NodeScope.PER_EVENT and node_t in event_nodes:
70
+ local_nodes[node_t] = event_nodes[node_t]
71
+ continue
72
+ elif scope is NodeScope.GLOBAL and hasattr(node_t, GLOBAL_VALUE_KEY):
73
+ local_nodes[node_t] = getattr(node_t, GLOBAL_VALUE_KEY)
74
+ continue
75
+
76
+ subnodes |= {
77
+ k: session.value for k, session in (local_nodes | event_nodes).items() if k not in subnodes
78
+ }
79
+
80
+ try:
81
+ local_nodes[node_t] = await compose_node(node_t, subnodes | data)
82
+ except (ComposeError, UnwrapError) as exc:
83
+ for t, local_node in local_nodes.items():
84
+ if t.scope is NodeScope.PER_CALL:
85
+ await local_node.close()
86
+ return Error(ComposeError(f"Cannot compose {node_t}. Error: {exc}"))
87
+
88
+ if scope is NodeScope.PER_EVENT:
89
+ event_nodes[node_t] = local_nodes[node_t]
90
+ elif scope is NodeScope.GLOBAL:
91
+ setattr(node_t, GLOBAL_VALUE_KEY, local_nodes[node_t])
92
+
93
+ parent_nodes[parent_node_t] = local_nodes[parent_node_t]
94
+
95
+ node_sessions = {k: parent_nodes[t] for k, t in nodes.items()}
96
+ return Ok(NodeCollection(node_sessions))
97
+
98
+
99
+ @dataclasses.dataclass(slots=True, repr=False)
100
+ class NodeSession:
101
+ node_type: type[Node] | None
102
+ value: typing.Any
103
+ subnodes: dict[str, typing.Self]
104
+ generator: typing.AsyncGenerator[typing.Any, typing.Any | None] | None = None
105
+
106
+ def __repr__(self) -> str:
107
+ return f"<{self.__class__.__name__}: {self.value!r}" + (" (ACTIVE)>" if self.generator else ">")
108
+
109
+ async def close(
110
+ self,
111
+ with_value: typing.Any | None = None,
112
+ scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
113
+ ) -> None:
114
+ if self.node_type and getattr(self.node_type, "scope", None) not in scopes:
115
+ return
116
+
117
+ for subnode in self.subnodes.values():
118
+ await subnode.close(scopes=scopes)
119
+
120
+ if self.generator is None:
121
+ return
122
+ try:
123
+ logger.debug("Closing session for node {!r}...", self.node_type)
124
+ await self.generator.asend(with_value)
125
+ except StopAsyncIteration:
126
+ self.generator = None
127
+
128
+
129
+ class NodeCollection:
130
+ __slots__ = ("sessions", "_values")
131
+
132
+ def __init__(self, sessions: dict[str, NodeSession]) -> None:
133
+ self.sessions = sessions
134
+ self._values: dict[str, typing.Any] = {}
135
+
136
+ def __repr__(self) -> str:
137
+ return "<{}: sessions={!r}>".format(self.__class__.__name__, self.sessions)
138
+
139
+ @property
140
+ def values(self) -> dict[str, typing.Any]:
141
+ if self._values.keys() == self.sessions.keys():
142
+ return self._values
143
+
144
+ for name, session in self.sessions.items():
145
+ if name not in self._values:
146
+ self._values[name] = session.value
147
+
148
+ return self._values
149
+
150
+ async def close_all(
151
+ self,
152
+ with_value: typing.Any | None = None,
153
+ scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
154
+ ) -> None:
155
+ for session in self.sessions.values():
156
+ await session.close(with_value, scopes=scopes)
157
+
158
+
159
+ @dataclasses.dataclass(slots=True, repr=False)
160
+ class Composition:
161
+ func: typing.Callable[..., typing.Any]
162
+ is_blocking: bool
163
+ nodes: dict[str, type[Node]] = dataclasses.field(init=False)
164
+
165
+ def __post_init__(self) -> None:
166
+ self.nodes = get_nodes(self.func)
167
+
168
+ def __repr__(self) -> str:
169
+ return "<{}: for function={!r} with nodes={!r}>".format(
170
+ ("blocking " if self.is_blocking else "") + self.__class__.__name__,
171
+ self.func.__qualname__,
172
+ self.nodes,
173
+ )
174
+
175
+ async def compose_nodes(
176
+ self,
177
+ update: UpdateCute,
178
+ context: Context,
179
+ ) -> NodeCollection | None:
180
+ match await compose_nodes(
181
+ nodes=self.nodes,
182
+ ctx=context,
183
+ data={Update: update, API: update.api},
184
+ ):
185
+ case Ok(col):
186
+ return col
187
+ case Error(err):
188
+ logger.debug(f"Composition failed with error: {err!r}")
189
+ return None
190
+
191
+ async def __call__(self, node_cls: type[Node], **kwargs: typing.Any) -> typing.Any:
192
+ result = self.func(node_cls, **magic_bundle(self.func, kwargs, start_idx=0, bundle_ctx=False))
193
+ if inspect.isawaitable(result):
194
+ result = await result
195
+ return result
196
+
197
+
198
+ __all__ = ("Composition", "NodeCollection", "NodeSession", "compose_node", "compose_nodes")
@@ -1,27 +1,27 @@
1
- import typing
2
-
3
- from telegrinder.node.base import Node
4
-
5
-
6
- class ContainerNode(Node):
7
- linked_nodes: typing.ClassVar[list[type[Node]]]
8
-
9
- @classmethod
10
- def compose(cls, **kw) -> tuple[Node, ...]:
11
- return tuple(t[1] for t in sorted(kw.items(), key=lambda t: t[0]))
12
-
13
- @classmethod
14
- def get_subnodes(cls) -> dict[str, type[Node]]:
15
- subnodes = getattr(cls, "subnodes", None)
16
- if subnodes is None:
17
- subnodes_dct = {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
18
- setattr(cls, "subnodes", subnodes_dct)
19
- return subnodes_dct
20
- return subnodes
21
-
22
- @classmethod
23
- def link_nodes(cls, linked_nodes: list[type[Node]]) -> type["ContainerNode"]:
24
- return type("_ContainerNode", (cls,), {"linked_nodes": linked_nodes})
25
-
26
-
27
- __all__ = ("ContainerNode",)
1
+ import typing
2
+
3
+ from telegrinder.node.base import Node
4
+
5
+
6
+ class ContainerNode(Node):
7
+ linked_nodes: typing.ClassVar[list[type[Node]]]
8
+
9
+ @classmethod
10
+ def compose(cls, **kw) -> tuple[Node, ...]:
11
+ return tuple(t[1] for t in sorted(kw.items(), key=lambda t: t[0]))
12
+
13
+ @classmethod
14
+ def get_subnodes(cls) -> dict[str, type[Node]]:
15
+ subnodes = getattr(cls, "subnodes", None)
16
+ if subnodes is None:
17
+ subnodes_dct = {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
18
+ setattr(cls, "subnodes", subnodes_dct)
19
+ return subnodes_dct
20
+ return subnodes
21
+
22
+ @classmethod
23
+ def link_nodes(cls, linked_nodes: list[type[Node]]) -> type["ContainerNode"]:
24
+ return type("_ContainerNode", (cls,), {"linked_nodes": linked_nodes})
25
+
26
+
27
+ __all__ = ("ContainerNode",)
telegrinder/node/event.py CHANGED
@@ -1,65 +1,65 @@
1
- import dataclasses
2
- import typing
3
-
4
- import msgspec
5
-
6
- from telegrinder.api.api import API
7
- from telegrinder.bot.cute_types import BaseCute
8
- from telegrinder.bot.dispatch.context import Context
9
- from telegrinder.msgspec_utils import DataclassInstance, decoder
10
- from telegrinder.node.base import ComposeError, FactoryNode
11
- from telegrinder.node.update import UpdateNode
12
-
13
- if typing.TYPE_CHECKING:
14
- Dataclass = typing.TypeVar("Dataclass", bound="DataclassType")
15
-
16
- DataclassType: typing.TypeAlias = DataclassInstance | msgspec.Struct | dict[str, typing.Any]
17
-
18
- EVENT_NODE_KEY = "_event_node"
19
-
20
-
21
- class _EventNode(FactoryNode):
22
- dataclass: type["DataclassType"]
23
-
24
- def __class_getitem__(cls, dataclass: type["DataclassType"], /) -> typing.Self:
25
- return cls(dataclass=dataclass)
26
-
27
- @classmethod
28
- def compose(cls, raw_update: UpdateNode, ctx: Context, api: API) -> "DataclassType":
29
- dataclass_type = typing.get_origin(cls.dataclass) or cls.dataclass
30
-
31
- try:
32
- if issubclass(dataclass_type, BaseCute):
33
- if isinstance(raw_update.incoming_update, dataclass_type):
34
- dataclass = raw_update.incoming_update
35
- else:
36
- dataclass = dataclass_type.from_update(raw_update.incoming_update, bound_api=api)
37
-
38
- elif issubclass(dataclass_type, msgspec.Struct | dict) or dataclasses.is_dataclass(
39
- dataclass_type
40
- ):
41
- dataclass = decoder.convert(
42
- raw_update.incoming_update.to_full_dict(),
43
- type=cls.dataclass,
44
- str_keys=True,
45
- )
46
-
47
- else:
48
- dataclass = cls.dataclass(**raw_update.incoming_update.to_dict())
49
-
50
- ctx[EVENT_NODE_KEY] = cls
51
- return dataclass
52
- except Exception as exc:
53
- raise ComposeError(f"Cannot parse update into {cls.dataclass.__name__!r}, error: {str(exc)!r}")
54
-
55
-
56
- if typing.TYPE_CHECKING:
57
- EventNode: typing.TypeAlias = typing.Annotated["Dataclass", ...]
58
-
59
- else:
60
-
61
- class EventNode(_EventNode):
62
- pass
63
-
64
-
65
- __all__ = ("EventNode",)
1
+ import dataclasses
2
+ import typing
3
+
4
+ import msgspec
5
+
6
+ from telegrinder.api.api import API
7
+ from telegrinder.bot.cute_types import BaseCute
8
+ from telegrinder.bot.dispatch.context import Context
9
+ from telegrinder.msgspec_utils import DataclassInstance, decoder
10
+ from telegrinder.node.base import ComposeError, FactoryNode
11
+ from telegrinder.node.update import UpdateNode
12
+
13
+ if typing.TYPE_CHECKING:
14
+ Dataclass = typing.TypeVar("Dataclass", bound="DataclassType")
15
+
16
+ DataclassType: typing.TypeAlias = DataclassInstance | msgspec.Struct | dict[str, typing.Any]
17
+
18
+ EVENT_NODE_KEY = "_event_node"
19
+
20
+
21
+ class _EventNode(FactoryNode):
22
+ dataclass: type["DataclassType"]
23
+
24
+ def __class_getitem__(cls, dataclass: type["DataclassType"], /) -> typing.Self:
25
+ return cls(dataclass=dataclass)
26
+
27
+ @classmethod
28
+ def compose(cls, raw_update: UpdateNode, ctx: Context, api: API) -> "DataclassType":
29
+ dataclass_type = typing.get_origin(cls.dataclass) or cls.dataclass
30
+
31
+ try:
32
+ if issubclass(dataclass_type, BaseCute):
33
+ if isinstance(raw_update.incoming_update, dataclass_type):
34
+ dataclass = raw_update.incoming_update
35
+ else:
36
+ dataclass = dataclass_type.from_update(raw_update.incoming_update, bound_api=api)
37
+
38
+ elif issubclass(dataclass_type, msgspec.Struct | dict) or dataclasses.is_dataclass(
39
+ dataclass_type
40
+ ):
41
+ dataclass = decoder.convert(
42
+ raw_update.incoming_update.to_full_dict(),
43
+ type=cls.dataclass,
44
+ str_keys=True,
45
+ )
46
+
47
+ else:
48
+ dataclass = cls.dataclass(**raw_update.incoming_update.to_dict())
49
+
50
+ ctx[EVENT_NODE_KEY] = cls
51
+ return dataclass
52
+ except Exception as exc:
53
+ raise ComposeError(f"Cannot parse update into {cls.dataclass.__name__!r}, error: {str(exc)!r}")
54
+
55
+
56
+ if typing.TYPE_CHECKING:
57
+ EventNode: typing.TypeAlias = typing.Annotated["Dataclass", ...]
58
+
59
+ else:
60
+
61
+ class EventNode(_EventNode):
62
+ pass
63
+
64
+
65
+ __all__ = ("EventNode",)