telegrinder 0.3.4.post1__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 (169) hide show
  1. telegrinder/__init__.py +30 -31
  2. telegrinder/api/__init__.py +2 -1
  3. telegrinder/api/api.py +28 -20
  4. telegrinder/api/error.py +8 -4
  5. telegrinder/api/response.py +2 -2
  6. telegrinder/api/token.py +2 -2
  7. telegrinder/bot/__init__.py +6 -0
  8. telegrinder/bot/bot.py +38 -31
  9. telegrinder/bot/cute_types/__init__.py +2 -0
  10. telegrinder/bot/cute_types/base.py +54 -128
  11. telegrinder/bot/cute_types/callback_query.py +76 -61
  12. telegrinder/bot/cute_types/chat_join_request.py +4 -3
  13. telegrinder/bot/cute_types/chat_member_updated.py +28 -31
  14. telegrinder/bot/cute_types/inline_query.py +5 -4
  15. telegrinder/bot/cute_types/message.py +555 -602
  16. telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
  17. telegrinder/bot/cute_types/update.py +20 -12
  18. telegrinder/bot/cute_types/utils.py +3 -36
  19. telegrinder/bot/dispatch/__init__.py +4 -0
  20. telegrinder/bot/dispatch/abc.py +8 -9
  21. telegrinder/bot/dispatch/context.py +5 -7
  22. telegrinder/bot/dispatch/dispatch.py +85 -33
  23. telegrinder/bot/dispatch/handler/abc.py +5 -6
  24. telegrinder/bot/dispatch/handler/audio_reply.py +2 -2
  25. telegrinder/bot/dispatch/handler/base.py +3 -3
  26. telegrinder/bot/dispatch/handler/document_reply.py +2 -2
  27. telegrinder/bot/dispatch/handler/func.py +36 -42
  28. telegrinder/bot/dispatch/handler/media_group_reply.py +5 -4
  29. telegrinder/bot/dispatch/handler/message_reply.py +2 -2
  30. telegrinder/bot/dispatch/handler/photo_reply.py +2 -2
  31. telegrinder/bot/dispatch/handler/sticker_reply.py +2 -2
  32. telegrinder/bot/dispatch/handler/video_reply.py +2 -2
  33. telegrinder/bot/dispatch/middleware/abc.py +83 -8
  34. telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
  35. telegrinder/bot/dispatch/process.py +44 -50
  36. telegrinder/bot/dispatch/return_manager/__init__.py +2 -0
  37. telegrinder/bot/dispatch/return_manager/abc.py +6 -10
  38. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +20 -0
  39. telegrinder/bot/dispatch/view/__init__.py +2 -0
  40. telegrinder/bot/dispatch/view/abc.py +10 -6
  41. telegrinder/bot/dispatch/view/base.py +81 -50
  42. telegrinder/bot/dispatch/view/box.py +20 -9
  43. telegrinder/bot/dispatch/view/callback_query.py +3 -4
  44. telegrinder/bot/dispatch/view/chat_join_request.py +2 -7
  45. telegrinder/bot/dispatch/view/chat_member.py +3 -5
  46. telegrinder/bot/dispatch/view/inline_query.py +3 -4
  47. telegrinder/bot/dispatch/view/message.py +3 -4
  48. telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
  49. telegrinder/bot/dispatch/view/raw.py +42 -40
  50. telegrinder/bot/dispatch/waiter_machine/actions.py +5 -4
  51. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +0 -0
  52. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +0 -0
  53. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +9 -7
  54. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
  55. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +3 -2
  56. telegrinder/bot/dispatch/waiter_machine/machine.py +113 -34
  57. telegrinder/bot/dispatch/waiter_machine/middleware.py +15 -10
  58. telegrinder/bot/dispatch/waiter_machine/short_state.py +7 -18
  59. telegrinder/bot/polling/polling.py +62 -54
  60. telegrinder/bot/rules/__init__.py +24 -1
  61. telegrinder/bot/rules/abc.py +17 -10
  62. telegrinder/bot/rules/callback_data.py +20 -61
  63. telegrinder/bot/rules/chat_join.py +6 -4
  64. telegrinder/bot/rules/command.py +4 -4
  65. telegrinder/bot/rules/enum_text.py +1 -4
  66. telegrinder/bot/rules/func.py +5 -3
  67. telegrinder/bot/rules/fuzzy.py +1 -1
  68. telegrinder/bot/rules/id.py +24 -0
  69. telegrinder/bot/rules/inline.py +6 -4
  70. telegrinder/bot/rules/integer.py +2 -1
  71. telegrinder/bot/rules/logic.py +18 -0
  72. telegrinder/bot/rules/markup.py +5 -6
  73. telegrinder/bot/rules/message.py +2 -4
  74. telegrinder/bot/rules/message_entities.py +1 -3
  75. telegrinder/bot/rules/node.py +15 -9
  76. telegrinder/bot/rules/payload.py +81 -0
  77. telegrinder/bot/rules/payment_invoice.py +29 -0
  78. telegrinder/bot/rules/regex.py +5 -6
  79. telegrinder/bot/rules/state.py +1 -3
  80. telegrinder/bot/rules/text.py +10 -5
  81. telegrinder/bot/rules/update.py +0 -0
  82. telegrinder/bot/scenario/abc.py +2 -4
  83. telegrinder/bot/scenario/checkbox.py +12 -14
  84. telegrinder/bot/scenario/choice.py +6 -9
  85. telegrinder/client/__init__.py +9 -1
  86. telegrinder/client/abc.py +35 -10
  87. telegrinder/client/aiohttp.py +28 -24
  88. telegrinder/client/form_data.py +31 -0
  89. telegrinder/client/sonic.py +212 -0
  90. telegrinder/model.py +38 -145
  91. telegrinder/modules.py +3 -1
  92. telegrinder/msgspec_utils.py +136 -68
  93. telegrinder/node/__init__.py +74 -13
  94. telegrinder/node/attachment.py +92 -16
  95. telegrinder/node/base.py +196 -68
  96. telegrinder/node/callback_query.py +17 -16
  97. telegrinder/node/command.py +3 -2
  98. telegrinder/node/composer.py +40 -75
  99. telegrinder/node/container.py +13 -7
  100. telegrinder/node/either.py +82 -0
  101. telegrinder/node/event.py +20 -31
  102. telegrinder/node/file.py +51 -0
  103. telegrinder/node/me.py +4 -5
  104. telegrinder/node/payload.py +78 -0
  105. telegrinder/node/polymorphic.py +27 -8
  106. telegrinder/node/rule.py +2 -6
  107. telegrinder/node/scope.py +4 -6
  108. telegrinder/node/source.py +37 -21
  109. telegrinder/node/text.py +20 -8
  110. telegrinder/node/tools/generator.py +7 -11
  111. telegrinder/py.typed +0 -0
  112. telegrinder/rules.py +0 -61
  113. telegrinder/tools/__init__.py +97 -38
  114. telegrinder/tools/adapter/__init__.py +19 -0
  115. telegrinder/tools/adapter/abc.py +49 -0
  116. telegrinder/tools/adapter/dataclass.py +56 -0
  117. telegrinder/{bot/rules → tools}/adapter/event.py +8 -10
  118. telegrinder/{bot/rules → tools}/adapter/node.py +8 -10
  119. telegrinder/{bot/rules → tools}/adapter/raw_event.py +2 -2
  120. telegrinder/{bot/rules → tools}/adapter/raw_update.py +2 -2
  121. telegrinder/tools/buttons.py +52 -26
  122. telegrinder/tools/callback_data_serilization/__init__.py +5 -0
  123. telegrinder/tools/callback_data_serilization/abc.py +51 -0
  124. telegrinder/tools/callback_data_serilization/json_ser.py +60 -0
  125. telegrinder/tools/callback_data_serilization/msgpack_ser.py +172 -0
  126. telegrinder/tools/error_handler/abc.py +4 -7
  127. telegrinder/tools/error_handler/error.py +0 -0
  128. telegrinder/tools/error_handler/error_handler.py +34 -48
  129. telegrinder/tools/formatting/__init__.py +57 -37
  130. telegrinder/tools/formatting/deep_links.py +541 -0
  131. telegrinder/tools/formatting/{html.py → html_formatter.py} +51 -79
  132. telegrinder/tools/formatting/spec_html_formats.py +14 -60
  133. telegrinder/tools/functional.py +1 -5
  134. telegrinder/tools/global_context/global_context.py +26 -51
  135. telegrinder/tools/global_context/telegrinder_ctx.py +3 -3
  136. telegrinder/tools/i18n/abc.py +0 -0
  137. telegrinder/tools/i18n/middleware/abc.py +3 -6
  138. telegrinder/tools/input_file_directory.py +30 -0
  139. telegrinder/tools/keyboard.py +9 -9
  140. telegrinder/tools/lifespan.py +105 -0
  141. telegrinder/tools/limited_dict.py +5 -10
  142. telegrinder/tools/loop_wrapper/abc.py +7 -2
  143. telegrinder/tools/loop_wrapper/loop_wrapper.py +40 -95
  144. telegrinder/tools/magic.py +184 -34
  145. telegrinder/tools/state_storage/__init__.py +0 -0
  146. telegrinder/tools/state_storage/abc.py +5 -9
  147. telegrinder/tools/state_storage/memory.py +1 -1
  148. telegrinder/tools/strings.py +13 -0
  149. telegrinder/types/__init__.py +8 -0
  150. telegrinder/types/enums.py +31 -21
  151. telegrinder/types/input_file.py +51 -0
  152. telegrinder/types/methods.py +531 -109
  153. telegrinder/types/objects.py +934 -826
  154. telegrinder/verification_utils.py +0 -2
  155. {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/LICENSE +2 -2
  156. telegrinder-0.4.0.dist-info/METADATA +144 -0
  157. telegrinder-0.4.0.dist-info/RECORD +182 -0
  158. {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/WHEEL +1 -1
  159. telegrinder/bot/rules/adapter/__init__.py +0 -17
  160. telegrinder/bot/rules/adapter/abc.py +0 -31
  161. telegrinder/node/message.py +0 -14
  162. telegrinder/node/update.py +0 -15
  163. telegrinder/tools/formatting/links.py +0 -38
  164. telegrinder/tools/kb_set/__init__.py +0 -4
  165. telegrinder/tools/kb_set/base.py +0 -15
  166. telegrinder/tools/kb_set/yaml.py +0 -63
  167. telegrinder-0.3.4.post1.dist-info/METADATA +0 -110
  168. telegrinder-0.3.4.post1.dist-info/RECORD +0 -165
  169. /telegrinder/{bot/rules → tools}/adapter/errors.py +0 -0
@@ -5,100 +5,104 @@ import typing
5
5
  from fntypes.error import UnwrapError
6
6
  from fntypes.result import Error, Ok, Result
7
7
 
8
- from telegrinder.api.api import API
9
- from telegrinder.bot.cute_types.update import Update, UpdateCute
10
8
  from telegrinder.bot.dispatch.context import Context
11
9
  from telegrinder.modules import logger
12
10
  from telegrinder.node.base import (
13
11
  ComposeError,
12
+ IsNode,
14
13
  Name,
15
- Node,
14
+ NodeImpersonation,
16
15
  NodeScope,
17
- get_node_calc_lst,
18
- get_nodes,
16
+ NodeType,
17
+ unwrap_node,
19
18
  )
20
- from telegrinder.tools.magic import magic_bundle
19
+ from telegrinder.tools.magic import join_dicts, magic_bundle
20
+
21
+ type AsyncGenerator = typing.AsyncGenerator[typing.Any, None]
21
22
 
22
23
  CONTEXT_STORE_NODES_KEY = "_node_ctx"
23
24
  GLOBAL_VALUE_KEY = "_value"
24
25
 
25
26
 
27
+ def get_scope(node: type[NodeType], /) -> NodeScope | None:
28
+ return getattr(node, "scope", None)
29
+
30
+
26
31
  async def compose_node(
27
- _node: type[Node],
28
- linked: dict[type, typing.Any],
32
+ node: type[NodeType],
33
+ linked: dict[type[typing.Any], typing.Any],
34
+ data: dict[type[typing.Any], typing.Any] | None = None,
29
35
  ) -> "NodeSession":
30
- node = _node.as_node()
31
- kwargs = magic_bundle(node.compose, linked, typebundle=True)
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))
32
42
 
33
43
  if node.is_generator():
34
- generator = typing.cast(typing.AsyncGenerator[typing.Any, None], node.compose(**kwargs))
44
+ generator = typing.cast(AsyncGenerator, node.compose(**kwargs))
35
45
  value = await generator.asend(None)
36
46
  else:
37
47
  generator = None
38
- value = typing.cast(typing.Awaitable[typing.Any] | typing.Any, node.compose(**kwargs))
48
+ value = node.compose(**kwargs)
39
49
  if inspect.isawaitable(value):
40
50
  value = await value
41
51
 
42
- return NodeSession(_node, value, {}, generator)
52
+ return NodeSession(node, value, subnodes={}, generator=generator)
43
53
 
44
54
 
45
55
  async def compose_nodes(
46
- nodes: dict[str, type[Node]],
56
+ nodes: typing.Mapping[str, IsNode | NodeImpersonation],
47
57
  ctx: Context,
48
58
  data: dict[type[typing.Any], typing.Any] | None = None,
49
59
  ) -> Result["NodeCollection", ComposeError]:
50
- logger.debug("Composing nodes: {!r}...", nodes)
60
+ logger.debug("Composing nodes: ({})...", " ".join(f"{k}={v!r}" for k, v in nodes.items()))
51
61
 
52
- local_nodes: dict[type[Node], NodeSession]
53
62
  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
+ 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]()
63
69
  subnodes = {}
64
70
  data[Name] = parent_node_name
65
71
 
66
72
  for node_t in linked_nodes:
67
- scope = getattr(node_t, "scope", None)
73
+ scope = get_scope(node_t)
68
74
 
69
75
  if scope is NodeScope.PER_EVENT and node_t in event_nodes:
70
76
  local_nodes[node_t] = event_nodes[node_t]
71
77
  continue
72
78
  elif scope is NodeScope.GLOBAL and hasattr(node_t, GLOBAL_VALUE_KEY):
73
- local_nodes[node_t] = getattr(node_t, GLOBAL_VALUE_KEY)
79
+ local_nodes[node_t] = NodeSession(node_t, getattr(node_t, GLOBAL_VALUE_KEY), {})
74
80
  continue
75
81
 
76
82
  subnodes |= {
77
83
  k: session.value for k, session in (local_nodes | event_nodes).items() if k not in subnodes
78
84
  }
79
-
80
85
  try:
81
- local_nodes[node_t] = await compose_node(node_t, subnodes | data)
86
+ local_nodes[node_t] = await compose_node(node_t, linked=subnodes, data=data)
82
87
  except (ComposeError, UnwrapError) as exc:
83
88
  for t, local_node in local_nodes.items():
84
- if t.scope is NodeScope.PER_CALL:
89
+ if get_scope(t) is NodeScope.PER_CALL:
85
90
  await local_node.close()
86
- return Error(ComposeError(f"Cannot compose {node_t}. Error: {exc}"))
91
+ return Error(ComposeError(f"Cannot compose {node_t!r}, error: {str(exc)}"))
87
92
 
88
93
  if scope is NodeScope.PER_EVENT:
89
94
  event_nodes[node_t] = local_nodes[node_t]
90
95
  elif scope is NodeScope.GLOBAL:
91
- setattr(node_t, GLOBAL_VALUE_KEY, local_nodes[node_t])
96
+ setattr(node_t, GLOBAL_VALUE_KEY, local_nodes[node_t].value)
92
97
 
93
98
  parent_nodes[parent_node_t] = local_nodes[parent_node_t]
94
99
 
95
- node_sessions = {k: parent_nodes[t] for k, t in nodes.items()}
96
- return Ok(NodeCollection(node_sessions))
100
+ return Ok(NodeCollection({k: parent_nodes[t] for k, t in unwrapped_nodes}))
97
101
 
98
102
 
99
103
  @dataclasses.dataclass(slots=True, repr=False)
100
104
  class NodeSession:
101
- node_type: type[Node] | None
105
+ node_type: type[NodeType] | None
102
106
  value: typing.Any
103
107
  subnodes: dict[str, typing.Self]
104
108
  generator: typing.AsyncGenerator[typing.Any, typing.Any | None] | None = None
@@ -156,43 +160,4 @@ class NodeCollection:
156
160
  await session.close(with_value, scopes=scopes)
157
161
 
158
162
 
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")
163
+ __all__ = ("NodeCollection", "NodeSession", "compose_node", "compose_nodes")
@@ -1,17 +1,19 @@
1
1
  import typing
2
2
 
3
- from telegrinder.node.base import Node
3
+ from telegrinder.node.base import IsNode, Node
4
4
 
5
5
 
6
6
  class ContainerNode(Node):
7
- linked_nodes: typing.ClassVar[list[type[Node]]]
7
+ linked_nodes: typing.ClassVar[list[IsNode]]
8
+ composer: typing.Callable[..., typing.Awaitable[typing.Any]]
8
9
 
9
10
  @classmethod
10
- def compose(cls, **kw) -> tuple[Node, ...]:
11
- return tuple(t[1] for t in sorted(kw.items(), key=lambda t: t[0]))
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))
12
14
 
13
15
  @classmethod
14
- def get_subnodes(cls) -> dict[str, type[Node]]:
16
+ def get_subnodes(cls) -> dict[str, IsNode]:
15
17
  subnodes = getattr(cls, "subnodes", None)
16
18
  if subnodes is None:
17
19
  subnodes_dct = {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
@@ -20,8 +22,12 @@ class ContainerNode(Node):
20
22
  return subnodes
21
23
 
22
24
  @classmethod
23
- def link_nodes(cls, linked_nodes: list[type[Node]]) -> type["ContainerNode"]:
24
- return type("_ContainerNode", (cls,), {"linked_nodes": linked_nodes})
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)})
25
31
 
26
32
 
27
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
@@ -5,61 +5,50 @@ import msgspec
5
5
 
6
6
  from telegrinder.api.api import API
7
7
  from telegrinder.bot.cute_types import BaseCute
8
- from telegrinder.bot.dispatch.context import Context
9
- from telegrinder.msgspec_utils import DataclassInstance, decoder
8
+ from telegrinder.msgspec_utils import decoder
10
9
  from telegrinder.node.base import ComposeError, FactoryNode
11
- from telegrinder.node.update import UpdateNode
10
+ from telegrinder.types.objects import Update
12
11
 
13
12
  if typing.TYPE_CHECKING:
14
- Dataclass = typing.TypeVar("Dataclass", bound="DataclassType")
13
+ from _typeshed import DataclassInstance
15
14
 
16
- DataclassType: typing.TypeAlias = DataclassInstance | msgspec.Struct | dict[str, typing.Any]
17
-
18
- EVENT_NODE_KEY = "_event_node"
15
+ type DataclassType = DataclassInstance | msgspec.Struct | dict[str, typing.Any]
19
16
 
20
17
 
21
18
  class _EventNode(FactoryNode):
22
- dataclass: type["DataclassType"]
19
+ dataclass: type[DataclassType]
20
+ orig_dataclass: type[DataclassType]
23
21
 
24
- def __class_getitem__(cls, dataclass: type["DataclassType"], /) -> typing.Self:
25
- return cls(dataclass=dataclass)
22
+ def __class_getitem__(cls, dataclass: type[DataclassType], /) -> typing.Self:
23
+ return cls(dataclass=dataclass, orig_dataclass=typing.get_origin(dataclass) or dataclass)
26
24
 
27
25
  @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
-
26
+ def compose(cls, raw_update: Update, api: API) -> DataclassType:
31
27
  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)
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)
37
31
 
38
- elif issubclass(dataclass_type, msgspec.Struct | dict) or dataclasses.is_dataclass(
39
- dataclass_type
32
+ elif issubclass(cls.orig_dataclass, msgspec.Struct) or dataclasses.is_dataclass(
33
+ cls.orig_dataclass,
40
34
  ):
41
35
  dataclass = decoder.convert(
42
- raw_update.incoming_update.to_full_dict(),
36
+ obj=raw_update.incoming_update,
43
37
  type=cls.dataclass,
44
- str_keys=True,
38
+ from_attributes=True,
45
39
  )
46
-
47
40
  else:
48
- dataclass = cls.dataclass(**raw_update.incoming_update.to_dict())
41
+ dataclass = cls.dataclass(**raw_update.incoming_update.to_full_dict())
49
42
 
50
- ctx[EVENT_NODE_KEY] = cls
51
43
  return dataclass
52
44
  except Exception as exc:
53
- raise ComposeError(f"Cannot parse update into {cls.dataclass.__name__!r}, error: {str(exc)!r}")
45
+ raise ComposeError(f"Cannot parse an update object into {cls.dataclass!r}, error: {str(exc)}")
54
46
 
55
47
 
56
48
  if typing.TYPE_CHECKING:
57
- EventNode: typing.TypeAlias = typing.Annotated["Dataclass", ...]
58
-
49
+ type EventNode[Dataclass: DataclassType] = Dataclass
59
50
  else:
60
-
61
- class EventNode(_EventNode):
62
- pass
51
+ EventNode = _EventNode
63
52
 
64
53
 
65
54
  __all__ = ("EventNode",)
@@ -0,0 +1,51 @@
1
+ import typing
2
+
3
+ import telegrinder.types as tg_types
4
+ from telegrinder.api.api import API
5
+ from telegrinder.node.attachment import Animation, Audio, Document, Photo, Video, VideoNote, Voice
6
+ from telegrinder.node.base import FactoryNode
7
+
8
+ type Attachment = Animation | Audio | Document | Photo | Video | VideoNote | Voice
9
+
10
+
11
+ class _FileId(FactoryNode):
12
+ attachment_node: type[Attachment]
13
+
14
+ def __class_getitem__(cls, attachment_node: type[Attachment], /):
15
+ return cls(attachment_node=attachment_node)
16
+
17
+ @classmethod
18
+ def get_subnodes(cls):
19
+ return {"attach": cls.attachment_node}
20
+
21
+ @classmethod
22
+ def compose(cls, attach: Attachment) -> str:
23
+ if isinstance(attach, Photo):
24
+ return attach.sizes[-1].file_id
25
+ return attach.file_id
26
+
27
+
28
+ class _File(FactoryNode):
29
+ attachment_node: type[Attachment]
30
+
31
+ def __class_getitem__(cls, attachment_node: type[Attachment], /):
32
+ return cls(attachment_node=attachment_node)
33
+
34
+ @classmethod
35
+ def get_subnodes(cls):
36
+ return {"file_id": _FileId[cls.attachment_node]}
37
+
38
+ @classmethod
39
+ async def compose(cls, file_id: str, api: API) -> tg_types.File:
40
+ return (await api.get_file(file_id=file_id)).expect("File can't be downloaded.")
41
+
42
+
43
+ if typing.TYPE_CHECKING:
44
+ type FileId[T: Attachment] = str
45
+ type File[T: Attachment] = tg_types.File
46
+ else:
47
+ FileId = _FileId
48
+ File = _File
49
+
50
+
51
+ __all__ = ("File", "FileId")
telegrinder/node/me.py CHANGED
@@ -1,16 +1,15 @@
1
1
  from telegrinder.api.api import API
2
- from telegrinder.node.base import ComposeError, ScalarNode
2
+ from telegrinder.node.base import ComposeError, scalar_node
3
3
  from telegrinder.node.scope import GLOBAL
4
4
  from telegrinder.types.objects import User
5
5
 
6
6
 
7
- class Me(ScalarNode, User):
8
- scope = GLOBAL
9
-
7
+ @scalar_node(scope=GLOBAL)
8
+ class Me:
10
9
  @classmethod
11
10
  async def compose(cls, api: API) -> User:
12
11
  me = await api.get_me()
13
- return me.expect(ComposeError("Can't complete get_me request"))
12
+ return me.expect(ComposeError("Can't complete api.get_me() request."))
14
13
 
15
14
 
16
15
  __all__ = ("Me",)
@@ -0,0 +1,78 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from fntypes.result import Error, Ok
5
+
6
+ from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
7
+ from telegrinder.bot.cute_types.message import MessageCute
8
+ from telegrinder.bot.cute_types.pre_checkout_query import PreCheckoutQueryCute
9
+ from telegrinder.node.base import ComposeError, DataNode, FactoryNode, GlobalNode, scalar_node
10
+ from telegrinder.node.polymorphic import Polymorphic, impl
11
+ from telegrinder.tools.callback_data_serilization import ABCDataSerializer, JSONSerializer
12
+
13
+
14
+ @scalar_node[str]
15
+ class Payload(Polymorphic):
16
+ @impl
17
+ def compose_pre_checkout_query(cls, event: PreCheckoutQueryCute) -> str:
18
+ return event.invoice_payload
19
+
20
+ @impl
21
+ def compose_callback_query(cls, event: CallbackQueryCute) -> str:
22
+ return event.data.expect("CallbackQuery has no data.")
23
+
24
+ @impl
25
+ def compose_message(cls, event: MessageCute) -> str:
26
+ return event.successful_payment.map(
27
+ lambda payment: payment.invoice_payload,
28
+ ).expect("Message has no successful payment.")
29
+
30
+
31
+ @dataclasses.dataclass(frozen=True, slots=True)
32
+ class PayloadSerializer[T: type[ABCDataSerializer[typing.Any]]](DataNode, GlobalNode[T]):
33
+ serializer: type[ABCDataSerializer[typing.Any]]
34
+
35
+ @classmethod
36
+ def compose(cls) -> typing.Self:
37
+ return cls(serializer=cls.get(default=JSONSerializer))
38
+
39
+
40
+ class _PayloadData(FactoryNode):
41
+ data_type: type[typing.Any]
42
+ serializer: type[ABCDataSerializer[typing.Any]] | None = None
43
+
44
+ def __class_getitem__(
45
+ cls,
46
+ data_type: type[typing.Any] | tuple[type[typing.Any], type[ABCDataSerializer[typing.Any]]],
47
+ /,
48
+ ):
49
+ data_type, serializer = (data_type, None) if not isinstance(data_type, tuple) else data_type
50
+ return cls(data_type=data_type, serializer=serializer)
51
+
52
+ @classmethod
53
+ def compose(cls, payload: Payload, payload_serializer: PayloadSerializer) -> typing.Any:
54
+ serializer = cls.serializer or payload_serializer.serializer
55
+ match serializer(cls.data_type).deserialize(payload):
56
+ case Ok(value):
57
+ return value
58
+ case Error(err):
59
+ raise ComposeError(err)
60
+
61
+
62
+ if typing.TYPE_CHECKING:
63
+ import typing_extensions
64
+
65
+ DataType = typing.TypeVar("DataType")
66
+ Serializer = typing_extensions.TypeVar(
67
+ "Serializer",
68
+ bound=ABCDataSerializer,
69
+ default=JSONSerializer[typing.Any],
70
+ )
71
+
72
+ type PayloadDataType[DataType, Serializer] = typing.Annotated[DataType, Serializer]
73
+ PayloadData: typing.TypeAlias = PayloadDataType[DataType, Serializer]
74
+ else:
75
+ PayloadData = _PayloadData
76
+
77
+
78
+ __all__ = ("Payload", "PayloadData", "PayloadSerializer")
@@ -1,25 +1,41 @@
1
+ import inspect
1
2
  import typing
2
3
 
4
+ from fntypes.result import Error, Ok
5
+
6
+ from telegrinder.api.api import API
7
+ from telegrinder.bot.cute_types.update import UpdateCute
3
8
  from telegrinder.bot.dispatch.context import Context
4
9
  from telegrinder.modules import logger
5
- from telegrinder.node.base import ComposeError, Node
6
- from telegrinder.node.composer import CONTEXT_STORE_NODES_KEY, Composition, NodeSession
10
+ from telegrinder.node.base import ComposeError, Node, get_nodes
11
+ from telegrinder.node.composer import CONTEXT_STORE_NODES_KEY, NodeSession, compose_nodes
7
12
  from telegrinder.node.scope import NodeScope
8
- from telegrinder.node.update import UpdateNode
9
- from telegrinder.tools.magic import get_impls, impl
13
+ from telegrinder.tools.magic import get_impls, impl, magic_bundle
14
+ from telegrinder.types.objects import Update
10
15
 
11
16
 
12
17
  class Polymorphic(Node):
13
18
  @classmethod
14
- async def compose(cls, update: UpdateNode, context: Context) -> typing.Any:
19
+ async def compose(cls, raw_update: Update, update: UpdateCute, context: Context) -> typing.Any:
15
20
  logger.debug(f"Composing polymorphic node {cls.__name__!r}...")
16
21
  scope = getattr(cls, "scope", None)
17
22
  node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
23
+ data = {
24
+ API: update.ctx_api,
25
+ Context: context,
26
+ Update: raw_update,
27
+ }
18
28
 
19
29
  for i, impl_ in enumerate(get_impls(cls)):
20
30
  logger.debug("Checking impl {!r}...", impl_.__name__)
21
- composition = Composition(impl_, True)
22
- node_collection = await composition.compose_nodes(update, context)
31
+ node_collection = None
32
+
33
+ match await compose_nodes(get_nodes(impl_), context, data=data):
34
+ case Ok(col):
35
+ node_collection = col
36
+ case Error(err):
37
+ logger.debug(f"Composition failed with error: {err!r}")
38
+
23
39
  if node_collection is None:
24
40
  logger.debug("Impl {!r} composition failed!", impl_.__name__)
25
41
  continue
@@ -34,7 +50,10 @@ class Polymorphic(Node):
34
50
  await node_collection.close_all()
35
51
  return res.value
36
52
 
37
- result = await composition(cls, **node_collection.values)
53
+ result = impl_(cls, **node_collection.values | magic_bundle(impl_, data, typebundle=True))
54
+ if inspect.isawaitable(result):
55
+ result = await result
56
+
38
57
  if scope is NodeScope.PER_EVENT:
39
58
  node_ctx[(cls, i)] = NodeSession(cls, result, {})
40
59
 
telegrinder/node/rule.py CHANGED
@@ -2,9 +2,9 @@ import dataclasses
2
2
  import importlib
3
3
  import typing
4
4
 
5
+ from telegrinder.bot.cute_types.update import UpdateCute
5
6
  from telegrinder.bot.dispatch.context import Context
6
7
  from telegrinder.node.base import ComposeError, Node
7
- from telegrinder.node.update import UpdateNode
8
8
 
9
9
  if typing.TYPE_CHECKING:
10
10
  from telegrinder.bot.dispatch.process import check_rule
@@ -35,7 +35,7 @@ class RuleChain(dict[str, typing.Any], Node):
35
35
  return dataclasses.dataclass(type(cls_.__name__, (object,), dict(cls_.__dict__)))
36
36
 
37
37
  @classmethod
38
- async def compose(cls, update: UpdateNode) -> typing.Any:
38
+ async def compose(cls, update: UpdateCute) -> typing.Any:
39
39
  # Hack to avoid circular import
40
40
  globalns = globals()
41
41
  if "check_rule" not in globalns:
@@ -64,10 +64,6 @@ class RuleChain(dict[str, typing.Any], Node):
64
64
  def as_node(cls) -> type[typing.Self]:
65
65
  return cls
66
66
 
67
- @classmethod
68
- def get_subnodes(cls) -> dict[typing.Literal["update"], type[UpdateNode]]:
69
- return {"update": UpdateNode}
70
-
71
67
  @classmethod
72
68
  def is_generator(cls) -> typing.Literal[False]:
73
69
  return False
telegrinder/node/scope.py CHANGED
@@ -2,9 +2,7 @@ import enum
2
2
  import typing
3
3
 
4
4
  if typing.TYPE_CHECKING:
5
- from .base import Node
6
-
7
- T = typing.TypeVar("T", bound=type["Node"])
5
+ from .base import IsNode
8
6
 
9
7
 
10
8
  class NodeScope(enum.Enum):
@@ -18,17 +16,17 @@ PER_CALL = NodeScope.PER_CALL
18
16
  GLOBAL = NodeScope.GLOBAL
19
17
 
20
18
 
21
- def per_call(node: T) -> T:
19
+ def per_call[T: IsNode](node: type[T]) -> type[T]:
22
20
  setattr(node, "scope", PER_CALL)
23
21
  return node
24
22
 
25
23
 
26
- def per_event(node: T) -> T:
24
+ def per_event[T: IsNode](node: type[T]) -> type[T]:
27
25
  setattr(node, "scope", PER_EVENT)
28
26
  return node
29
27
 
30
28
 
31
- def global_node(node: T) -> T:
29
+ def global_node[T: IsNode](node: type[T]) -> type[T]:
32
30
  setattr(node, "scope", GLOBAL)
33
31
  return node
34
32