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