telegrinder 0.3.4__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of telegrinder might be problematic. Click here for more details.

Files changed (192) hide show
  1. telegrinder/__init__.py +148 -149
  2. telegrinder/api/__init__.py +9 -8
  3. telegrinder/api/api.py +101 -93
  4. telegrinder/api/error.py +20 -16
  5. telegrinder/api/response.py +20 -20
  6. telegrinder/api/token.py +36 -36
  7. telegrinder/bot/__init__.py +72 -66
  8. telegrinder/bot/bot.py +83 -76
  9. telegrinder/bot/cute_types/__init__.py +19 -17
  10. telegrinder/bot/cute_types/base.py +184 -258
  11. telegrinder/bot/cute_types/callback_query.py +400 -385
  12. telegrinder/bot/cute_types/chat_join_request.py +62 -61
  13. telegrinder/bot/cute_types/chat_member_updated.py +157 -160
  14. telegrinder/bot/cute_types/inline_query.py +44 -43
  15. telegrinder/bot/cute_types/message.py +2590 -2637
  16. telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
  17. telegrinder/bot/cute_types/update.py +112 -104
  18. telegrinder/bot/cute_types/utils.py +62 -95
  19. telegrinder/bot/dispatch/__init__.py +59 -55
  20. telegrinder/bot/dispatch/abc.py +76 -77
  21. telegrinder/bot/dispatch/context.py +96 -98
  22. telegrinder/bot/dispatch/dispatch.py +254 -202
  23. telegrinder/bot/dispatch/handler/__init__.py +13 -13
  24. telegrinder/bot/dispatch/handler/abc.py +23 -24
  25. telegrinder/bot/dispatch/handler/audio_reply.py +44 -44
  26. telegrinder/bot/dispatch/handler/base.py +57 -57
  27. telegrinder/bot/dispatch/handler/document_reply.py +44 -44
  28. telegrinder/bot/dispatch/handler/func.py +129 -135
  29. telegrinder/bot/dispatch/handler/media_group_reply.py +44 -43
  30. telegrinder/bot/dispatch/handler/message_reply.py +36 -36
  31. telegrinder/bot/dispatch/handler/photo_reply.py +44 -44
  32. telegrinder/bot/dispatch/handler/sticker_reply.py +37 -37
  33. telegrinder/bot/dispatch/handler/video_reply.py +44 -44
  34. telegrinder/bot/dispatch/middleware/__init__.py +3 -3
  35. telegrinder/bot/dispatch/middleware/abc.py +97 -22
  36. telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
  37. telegrinder/bot/dispatch/process.py +151 -157
  38. telegrinder/bot/dispatch/return_manager/__init__.py +15 -13
  39. telegrinder/bot/dispatch/return_manager/abc.py +104 -108
  40. telegrinder/bot/dispatch/return_manager/callback_query.py +20 -20
  41. telegrinder/bot/dispatch/return_manager/inline_query.py +15 -15
  42. telegrinder/bot/dispatch/return_manager/message.py +36 -36
  43. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +20 -0
  44. telegrinder/bot/dispatch/view/__init__.py +15 -13
  45. telegrinder/bot/dispatch/view/abc.py +45 -41
  46. telegrinder/bot/dispatch/view/base.py +231 -200
  47. telegrinder/bot/dispatch/view/box.py +140 -129
  48. telegrinder/bot/dispatch/view/callback_query.py +16 -17
  49. telegrinder/bot/dispatch/view/chat_join_request.py +11 -16
  50. telegrinder/bot/dispatch/view/chat_member.py +37 -39
  51. telegrinder/bot/dispatch/view/inline_query.py +16 -17
  52. telegrinder/bot/dispatch/view/message.py +43 -44
  53. telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
  54. telegrinder/bot/dispatch/view/raw.py +116 -114
  55. telegrinder/bot/dispatch/waiter_machine/__init__.py +17 -17
  56. telegrinder/bot/dispatch/waiter_machine/actions.py +14 -13
  57. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +8 -8
  58. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +55 -55
  59. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +59 -57
  60. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +51 -51
  61. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +20 -19
  62. telegrinder/bot/dispatch/waiter_machine/machine.py +251 -172
  63. telegrinder/bot/dispatch/waiter_machine/middleware.py +94 -89
  64. telegrinder/bot/dispatch/waiter_machine/short_state.py +57 -68
  65. telegrinder/bot/polling/__init__.py +4 -4
  66. telegrinder/bot/polling/abc.py +25 -25
  67. telegrinder/bot/polling/polling.py +139 -131
  68. telegrinder/bot/rules/__init__.py +85 -62
  69. telegrinder/bot/rules/abc.py +213 -206
  70. telegrinder/bot/rules/callback_data.py +122 -163
  71. telegrinder/bot/rules/chat_join.py +45 -43
  72. telegrinder/bot/rules/command.py +126 -126
  73. telegrinder/bot/rules/enum_text.py +33 -36
  74. telegrinder/bot/rules/func.py +28 -26
  75. telegrinder/bot/rules/fuzzy.py +24 -24
  76. telegrinder/bot/rules/id.py +24 -0
  77. telegrinder/bot/rules/inline.py +58 -56
  78. telegrinder/bot/rules/integer.py +21 -20
  79. telegrinder/bot/rules/is_from.py +127 -127
  80. telegrinder/bot/rules/logic.py +18 -0
  81. telegrinder/bot/rules/markup.py +42 -43
  82. telegrinder/bot/rules/mention.py +14 -14
  83. telegrinder/bot/rules/message.py +15 -17
  84. telegrinder/bot/rules/message_entities.py +33 -35
  85. telegrinder/bot/rules/node.py +33 -27
  86. telegrinder/bot/rules/payload.py +81 -0
  87. telegrinder/bot/rules/payment_invoice.py +29 -0
  88. telegrinder/bot/rules/regex.py +36 -37
  89. telegrinder/bot/rules/rule_enum.py +72 -72
  90. telegrinder/bot/rules/start.py +42 -42
  91. telegrinder/bot/rules/state.py +35 -37
  92. telegrinder/bot/rules/text.py +38 -33
  93. telegrinder/bot/rules/update.py +15 -15
  94. telegrinder/bot/scenario/__init__.py +5 -5
  95. telegrinder/bot/scenario/abc.py +17 -19
  96. telegrinder/bot/scenario/checkbox.py +174 -176
  97. telegrinder/bot/scenario/choice.py +48 -51
  98. telegrinder/client/__init__.py +12 -4
  99. telegrinder/client/abc.py +100 -75
  100. telegrinder/client/aiohttp.py +134 -130
  101. telegrinder/client/form_data.py +31 -0
  102. telegrinder/client/sonic.py +212 -0
  103. telegrinder/model.py +208 -315
  104. telegrinder/modules.py +239 -237
  105. telegrinder/msgspec_json.py +14 -14
  106. telegrinder/msgspec_utils.py +478 -410
  107. telegrinder/node/__init__.py +86 -25
  108. telegrinder/node/attachment.py +163 -87
  109. telegrinder/node/base.py +288 -160
  110. telegrinder/node/callback_query.py +54 -53
  111. telegrinder/node/command.py +34 -33
  112. telegrinder/node/composer.py +163 -198
  113. telegrinder/node/container.py +33 -27
  114. telegrinder/node/either.py +82 -0
  115. telegrinder/node/event.py +54 -65
  116. telegrinder/node/file.py +51 -0
  117. telegrinder/node/me.py +15 -16
  118. telegrinder/node/payload.py +78 -0
  119. telegrinder/node/polymorphic.py +67 -48
  120. telegrinder/node/rule.py +72 -76
  121. telegrinder/node/scope.py +36 -38
  122. telegrinder/node/source.py +87 -71
  123. telegrinder/node/text.py +53 -41
  124. telegrinder/node/tools/__init__.py +3 -3
  125. telegrinder/node/tools/generator.py +36 -40
  126. telegrinder/py.typed +0 -0
  127. telegrinder/rules.py +1 -62
  128. telegrinder/tools/__init__.py +152 -93
  129. telegrinder/tools/adapter/__init__.py +19 -0
  130. telegrinder/tools/adapter/abc.py +49 -0
  131. telegrinder/tools/adapter/dataclass.py +56 -0
  132. telegrinder/{bot/rules → tools}/adapter/errors.py +5 -5
  133. telegrinder/{bot/rules → tools}/adapter/event.py +63 -65
  134. telegrinder/{bot/rules → tools}/adapter/node.py +46 -48
  135. telegrinder/{bot/rules → tools}/adapter/raw_event.py +27 -27
  136. telegrinder/{bot/rules → tools}/adapter/raw_update.py +30 -30
  137. telegrinder/tools/buttons.py +106 -80
  138. telegrinder/tools/callback_data_serilization/__init__.py +5 -0
  139. telegrinder/tools/callback_data_serilization/abc.py +51 -0
  140. telegrinder/tools/callback_data_serilization/json_ser.py +60 -0
  141. telegrinder/tools/callback_data_serilization/msgpack_ser.py +172 -0
  142. telegrinder/tools/error_handler/__init__.py +7 -7
  143. telegrinder/tools/error_handler/abc.py +30 -33
  144. telegrinder/tools/error_handler/error.py +9 -9
  145. telegrinder/tools/error_handler/error_handler.py +179 -193
  146. telegrinder/tools/formatting/__init__.py +83 -63
  147. telegrinder/tools/formatting/deep_links.py +541 -0
  148. telegrinder/tools/formatting/{html.py → html_formatter.py} +266 -294
  149. telegrinder/tools/formatting/spec_html_formats.py +71 -117
  150. telegrinder/tools/functional.py +8 -12
  151. telegrinder/tools/global_context/__init__.py +7 -7
  152. telegrinder/tools/global_context/abc.py +63 -63
  153. telegrinder/tools/global_context/global_context.py +387 -412
  154. telegrinder/tools/global_context/telegrinder_ctx.py +27 -27
  155. telegrinder/tools/i18n/__init__.py +7 -7
  156. telegrinder/tools/i18n/abc.py +30 -30
  157. telegrinder/tools/i18n/middleware/__init__.py +3 -3
  158. telegrinder/tools/i18n/middleware/abc.py +22 -25
  159. telegrinder/tools/i18n/simple.py +43 -43
  160. telegrinder/tools/input_file_directory.py +30 -0
  161. telegrinder/tools/keyboard.py +128 -128
  162. telegrinder/tools/lifespan.py +105 -0
  163. telegrinder/tools/limited_dict.py +32 -37
  164. telegrinder/tools/loop_wrapper/__init__.py +4 -4
  165. telegrinder/tools/loop_wrapper/abc.py +20 -15
  166. telegrinder/tools/loop_wrapper/loop_wrapper.py +169 -224
  167. telegrinder/tools/magic.py +307 -157
  168. telegrinder/tools/parse_mode.py +6 -6
  169. telegrinder/tools/state_storage/__init__.py +4 -4
  170. telegrinder/tools/state_storage/abc.py +31 -35
  171. telegrinder/tools/state_storage/memory.py +25 -25
  172. telegrinder/tools/strings.py +13 -0
  173. telegrinder/types/__init__.py +268 -260
  174. telegrinder/types/enums.py +711 -701
  175. telegrinder/types/input_file.py +51 -0
  176. telegrinder/types/methods.py +5055 -4633
  177. telegrinder/types/objects.py +7058 -6950
  178. telegrinder/verification_utils.py +30 -32
  179. {telegrinder-0.3.4.dist-info → telegrinder-0.4.0.dist-info}/LICENSE +22 -22
  180. telegrinder-0.4.0.dist-info/METADATA +144 -0
  181. telegrinder-0.4.0.dist-info/RECORD +182 -0
  182. {telegrinder-0.3.4.dist-info → telegrinder-0.4.0.dist-info}/WHEEL +1 -1
  183. telegrinder/bot/rules/adapter/__init__.py +0 -17
  184. telegrinder/bot/rules/adapter/abc.py +0 -31
  185. telegrinder/node/message.py +0 -14
  186. telegrinder/node/update.py +0 -15
  187. telegrinder/tools/formatting/links.py +0 -38
  188. telegrinder/tools/kb_set/__init__.py +0 -4
  189. telegrinder/tools/kb_set/base.py +0 -15
  190. telegrinder/tools/kb_set/yaml.py +0 -63
  191. telegrinder-0.3.4.dist-info/METADATA +0 -110
  192. telegrinder-0.3.4.dist-info/RECORD +0 -165
@@ -1,198 +1,163 @@
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.bot.dispatch.context import Context
9
+ from telegrinder.modules import logger
10
+ from telegrinder.node.base import (
11
+ ComposeError,
12
+ IsNode,
13
+ Name,
14
+ NodeImpersonation,
15
+ NodeScope,
16
+ NodeType,
17
+ unwrap_node,
18
+ )
19
+ from telegrinder.tools.magic import join_dicts, magic_bundle
20
+
21
+ type AsyncGenerator = typing.AsyncGenerator[typing.Any, None]
22
+
23
+ CONTEXT_STORE_NODES_KEY = "_node_ctx"
24
+ GLOBAL_VALUE_KEY = "_value"
25
+
26
+
27
+ def get_scope(node: type[NodeType], /) -> NodeScope | None:
28
+ return getattr(node, "scope", None)
29
+
30
+
31
+ async def compose_node(
32
+ node: type[NodeType],
33
+ linked: dict[type[typing.Any], typing.Any],
34
+ data: dict[type[typing.Any], typing.Any] | None = None,
35
+ ) -> "NodeSession":
36
+ subnodes = node.get_subnodes()
37
+ kwargs = magic_bundle(node.compose, join_dicts(subnodes, linked))
38
+
39
+ # Linking data via typebundle
40
+ if data:
41
+ kwargs.update(magic_bundle(node.compose, data, typebundle=True))
42
+
43
+ if node.is_generator():
44
+ generator = typing.cast(AsyncGenerator, node.compose(**kwargs))
45
+ value = await generator.asend(None)
46
+ else:
47
+ generator = None
48
+ value = node.compose(**kwargs)
49
+ if inspect.isawaitable(value):
50
+ value = await value
51
+
52
+ return NodeSession(node, value, subnodes={}, generator=generator)
53
+
54
+
55
+ async def compose_nodes(
56
+ nodes: typing.Mapping[str, IsNode | NodeImpersonation],
57
+ ctx: Context,
58
+ data: dict[type[typing.Any], typing.Any] | None = None,
59
+ ) -> Result["NodeCollection", ComposeError]:
60
+ logger.debug("Composing nodes: ({})...", " ".join(f"{k}={v!r}" for k, v in nodes.items()))
61
+
62
+ data = {Context: ctx} | (data or {})
63
+ parent_nodes = dict[IsNode, NodeSession]()
64
+ event_nodes: dict[IsNode, NodeSession] = ctx.get_or_set(CONTEXT_STORE_NODES_KEY, {})
65
+ unwrapped_nodes = {(key, n := node.as_node()): unwrap_node(n) for key, node in nodes.items()}
66
+
67
+ for (parent_node_name, parent_node_t), linked_nodes in unwrapped_nodes.items():
68
+ local_nodes = dict[type[NodeType], NodeSession]()
69
+ subnodes = {}
70
+ data[Name] = parent_node_name
71
+
72
+ for node_t in linked_nodes:
73
+ scope = get_scope(node_t)
74
+
75
+ if scope is NodeScope.PER_EVENT and node_t in event_nodes:
76
+ local_nodes[node_t] = event_nodes[node_t]
77
+ continue
78
+ elif scope is NodeScope.GLOBAL and hasattr(node_t, GLOBAL_VALUE_KEY):
79
+ local_nodes[node_t] = NodeSession(node_t, getattr(node_t, GLOBAL_VALUE_KEY), {})
80
+ continue
81
+
82
+ subnodes |= {
83
+ k: session.value for k, session in (local_nodes | event_nodes).items() if k not in subnodes
84
+ }
85
+ try:
86
+ local_nodes[node_t] = await compose_node(node_t, linked=subnodes, data=data)
87
+ except (ComposeError, UnwrapError) as exc:
88
+ for t, local_node in local_nodes.items():
89
+ if get_scope(t) is NodeScope.PER_CALL:
90
+ await local_node.close()
91
+ return Error(ComposeError(f"Cannot compose {node_t!r}, error: {str(exc)}"))
92
+
93
+ if scope is NodeScope.PER_EVENT:
94
+ event_nodes[node_t] = local_nodes[node_t]
95
+ elif scope is NodeScope.GLOBAL:
96
+ setattr(node_t, GLOBAL_VALUE_KEY, local_nodes[node_t].value)
97
+
98
+ parent_nodes[parent_node_t] = local_nodes[parent_node_t]
99
+
100
+ return Ok(NodeCollection({k: parent_nodes[t] for k, t in unwrapped_nodes}))
101
+
102
+
103
+ @dataclasses.dataclass(slots=True, repr=False)
104
+ class NodeSession:
105
+ node_type: type[NodeType] | None
106
+ value: typing.Any
107
+ subnodes: dict[str, typing.Self]
108
+ generator: typing.AsyncGenerator[typing.Any, typing.Any | None] | None = None
109
+
110
+ def __repr__(self) -> str:
111
+ return f"<{self.__class__.__name__}: {self.value!r}" + (" (ACTIVE)>" if self.generator else ">")
112
+
113
+ async def close(
114
+ self,
115
+ with_value: typing.Any | None = None,
116
+ scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
117
+ ) -> None:
118
+ if self.node_type and getattr(self.node_type, "scope", None) not in scopes:
119
+ return
120
+
121
+ for subnode in self.subnodes.values():
122
+ await subnode.close(scopes=scopes)
123
+
124
+ if self.generator is None:
125
+ return
126
+ try:
127
+ logger.debug("Closing session for node {!r}...", self.node_type)
128
+ await self.generator.asend(with_value)
129
+ except StopAsyncIteration:
130
+ self.generator = None
131
+
132
+
133
+ class NodeCollection:
134
+ __slots__ = ("sessions", "_values")
135
+
136
+ def __init__(self, sessions: dict[str, NodeSession]) -> None:
137
+ self.sessions = sessions
138
+ self._values: dict[str, typing.Any] = {}
139
+
140
+ def __repr__(self) -> str:
141
+ return "<{}: sessions={!r}>".format(self.__class__.__name__, self.sessions)
142
+
143
+ @property
144
+ def values(self) -> dict[str, typing.Any]:
145
+ if self._values.keys() == self.sessions.keys():
146
+ return self._values
147
+
148
+ for name, session in self.sessions.items():
149
+ if name not in self._values:
150
+ self._values[name] = session.value
151
+
152
+ return self._values
153
+
154
+ async def close_all(
155
+ self,
156
+ with_value: typing.Any | None = None,
157
+ scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
158
+ ) -> None:
159
+ for session in self.sessions.values():
160
+ await session.close(with_value, scopes=scopes)
161
+
162
+
163
+ __all__ = ("NodeCollection", "NodeSession", "compose_node", "compose_nodes")
@@ -1,27 +1,33 @@
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 IsNode, Node
4
+
5
+
6
+ class ContainerNode(Node):
7
+ linked_nodes: typing.ClassVar[list[IsNode]]
8
+ composer: typing.Callable[..., typing.Awaitable[typing.Any]]
9
+
10
+ @classmethod
11
+ async def compose(cls, **kw: typing.Any) -> typing.Any:
12
+ subnodes = cls.get_subnodes().keys()
13
+ return await cls.composer(*tuple(t[1] for t in sorted(kw.items(), key=lambda t: t[0]) if t[0] in subnodes))
14
+
15
+ @classmethod
16
+ def get_subnodes(cls) -> dict[str, IsNode]:
17
+ subnodes = getattr(cls, "subnodes", None)
18
+ if subnodes is None:
19
+ subnodes_dct = {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
20
+ setattr(cls, "subnodes", subnodes_dct)
21
+ return subnodes_dct
22
+ return subnodes
23
+
24
+ @classmethod
25
+ def link_nodes(
26
+ cls,
27
+ linked_nodes: list[IsNode],
28
+ composer: typing.Callable[..., typing.Awaitable[typing.Any]],
29
+ ) -> type["ContainerNode"]:
30
+ return type(cls.__name__, (cls,), {"linked_nodes": linked_nodes, "composer": classmethod(composer)})
31
+
32
+
33
+ __all__ = ("ContainerNode",)
@@ -0,0 +1,82 @@
1
+ import typing
2
+
3
+ from fntypes.result import Ok
4
+
5
+ from telegrinder.api.api import API
6
+ from telegrinder.bot.dispatch.context import Context
7
+ from telegrinder.node.base import ComposeError, FactoryNode, Node
8
+ from telegrinder.node.composer import CONTEXT_STORE_NODES_KEY, GLOBAL_VALUE_KEY, compose_node, compose_nodes
9
+ from telegrinder.node.scope import NodeScope, per_call
10
+ from telegrinder.types.objects import Update
11
+
12
+
13
+ @per_call
14
+ class _Either(FactoryNode):
15
+ """Represents a node that either to compose `left` or `right` nodes.
16
+
17
+ For example:
18
+ ```python
19
+ # ScalarNode `Integer` -> int
20
+ # ScalarNode `Float` -> float
21
+
22
+ Number = Either[Integer, Float] # using a type alias just as an example
23
+
24
+ def number_to_int(number: Number) -> int:
25
+ return int(number)
26
+ ```
27
+ """
28
+
29
+ nodes: tuple[type[Node], type[Node] | None]
30
+
31
+ def __class_getitem__(cls, node: type[Node] | tuple[type[Node], type[Node]], /):
32
+ nodes = (node, None) if not isinstance(node, tuple) else node
33
+ assert len(nodes) == 2, "Node `Either` must have at least two nodes."
34
+ return cls(nodes=nodes)
35
+
36
+ @classmethod
37
+ async def compose(cls, api: API, update: Update, context: Context) -> typing.Any | None:
38
+ data = {API: api, Update: update, Context: context}
39
+ node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
40
+
41
+ for node in cls.nodes:
42
+ if node is None:
43
+ return None
44
+
45
+ if node.scope is NodeScope.PER_EVENT and node in node_ctx:
46
+ return node_ctx[node].value
47
+ elif node.scope is NodeScope.GLOBAL and hasattr(node, GLOBAL_VALUE_KEY):
48
+ return getattr(node, GLOBAL_VALUE_KEY)
49
+
50
+ subnodes = node.as_node().get_subnodes()
51
+ match await compose_nodes(subnodes, context, data):
52
+ case Ok(col):
53
+ try:
54
+ session = await compose_node(
55
+ node=node,
56
+ linked={
57
+ typing.cast(type, n): col.sessions[name].value for name, n in subnodes.items()
58
+ },
59
+ data=data,
60
+ )
61
+ except ComposeError:
62
+ continue
63
+
64
+ if node.scope is NodeScope.PER_EVENT:
65
+ node_ctx[node] = session
66
+ elif node.scope is NodeScope.GLOBAL:
67
+ setattr(node, GLOBAL_VALUE_KEY, session.value)
68
+
69
+ return session.value
70
+
71
+ raise ComposeError("Cannot compose either nodes: {}.".format(", ".join(repr(n) for n in cls.nodes)))
72
+
73
+
74
+ if typing.TYPE_CHECKING:
75
+ type Either[Left, Right: typing.Any | None] = Left | Right
76
+ type Optional[Left] = Either[Left, None]
77
+ else:
78
+ Either = _Either
79
+ Optional = type("Optional", (Either,), {})
80
+
81
+
82
+ __all__ = ("Either", "Optional")
telegrinder/node/event.py CHANGED
@@ -1,65 +1,54 @@
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.msgspec_utils import decoder
9
+ from telegrinder.node.base import ComposeError, FactoryNode
10
+ from telegrinder.types.objects import Update
11
+
12
+ if typing.TYPE_CHECKING:
13
+ from _typeshed import DataclassInstance
14
+
15
+ type DataclassType = DataclassInstance | msgspec.Struct | dict[str, typing.Any]
16
+
17
+
18
+ class _EventNode(FactoryNode):
19
+ dataclass: type[DataclassType]
20
+ orig_dataclass: type[DataclassType]
21
+
22
+ def __class_getitem__(cls, dataclass: type[DataclassType], /) -> typing.Self:
23
+ return cls(dataclass=dataclass, orig_dataclass=typing.get_origin(dataclass) or dataclass)
24
+
25
+ @classmethod
26
+ def compose(cls, raw_update: Update, api: API) -> DataclassType:
27
+ try:
28
+ if issubclass(cls.orig_dataclass, BaseCute):
29
+ update = raw_update if issubclass(cls.orig_dataclass, Update) else raw_update.incoming_update
30
+ dataclass = cls.orig_dataclass.from_update(update=update, bound_api=api)
31
+
32
+ elif issubclass(cls.orig_dataclass, msgspec.Struct) or dataclasses.is_dataclass(
33
+ cls.orig_dataclass,
34
+ ):
35
+ dataclass = decoder.convert(
36
+ obj=raw_update.incoming_update,
37
+ type=cls.dataclass,
38
+ from_attributes=True,
39
+ )
40
+ else:
41
+ dataclass = cls.dataclass(**raw_update.incoming_update.to_full_dict())
42
+
43
+ return dataclass
44
+ except Exception as exc:
45
+ raise ComposeError(f"Cannot parse an update object into {cls.dataclass!r}, error: {str(exc)}")
46
+
47
+
48
+ if typing.TYPE_CHECKING:
49
+ type EventNode[Dataclass: DataclassType] = Dataclass
50
+ else:
51
+ EventNode = _EventNode
52
+
53
+
54
+ __all__ = ("EventNode",)