telegrinder 0.4.2__py3-none-any.whl → 0.5.1__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 (233) hide show
  1. telegrinder/__init__.py +37 -55
  2. telegrinder/__meta__.py +1 -0
  3. telegrinder/api/__init__.py +6 -4
  4. telegrinder/api/api.py +100 -26
  5. telegrinder/api/error.py +42 -8
  6. telegrinder/api/response.py +4 -1
  7. telegrinder/api/token.py +2 -2
  8. telegrinder/bot/__init__.py +9 -25
  9. telegrinder/bot/bot.py +31 -25
  10. telegrinder/bot/cute_types/__init__.py +0 -0
  11. telegrinder/bot/cute_types/base.py +103 -61
  12. telegrinder/bot/cute_types/callback_query.py +447 -400
  13. telegrinder/bot/cute_types/chat_join_request.py +59 -62
  14. telegrinder/bot/cute_types/chat_member_updated.py +154 -157
  15. telegrinder/bot/cute_types/inline_query.py +41 -44
  16. telegrinder/bot/cute_types/message.py +98 -67
  17. telegrinder/bot/cute_types/pre_checkout_query.py +38 -42
  18. telegrinder/bot/cute_types/update.py +1 -8
  19. telegrinder/bot/cute_types/utils.py +1 -1
  20. telegrinder/bot/dispatch/__init__.py +10 -15
  21. telegrinder/bot/dispatch/abc.py +12 -11
  22. telegrinder/bot/dispatch/action.py +104 -0
  23. telegrinder/bot/dispatch/context.py +32 -26
  24. telegrinder/bot/dispatch/dispatch.py +61 -134
  25. telegrinder/bot/dispatch/handler/__init__.py +2 -0
  26. telegrinder/bot/dispatch/handler/abc.py +10 -8
  27. telegrinder/bot/dispatch/handler/audio_reply.py +2 -3
  28. telegrinder/bot/dispatch/handler/base.py +10 -33
  29. telegrinder/bot/dispatch/handler/document_reply.py +2 -3
  30. telegrinder/bot/dispatch/handler/func.py +55 -87
  31. telegrinder/bot/dispatch/handler/media_group_reply.py +2 -3
  32. telegrinder/bot/dispatch/handler/message_reply.py +2 -3
  33. telegrinder/bot/dispatch/handler/photo_reply.py +2 -3
  34. telegrinder/bot/dispatch/handler/sticker_reply.py +2 -3
  35. telegrinder/bot/dispatch/handler/video_reply.py +2 -3
  36. telegrinder/bot/dispatch/middleware/__init__.py +0 -0
  37. telegrinder/bot/dispatch/middleware/abc.py +79 -55
  38. telegrinder/bot/dispatch/middleware/global_middleware.py +18 -33
  39. telegrinder/bot/dispatch/process.py +84 -105
  40. telegrinder/bot/dispatch/return_manager/__init__.py +0 -0
  41. telegrinder/bot/dispatch/return_manager/abc.py +102 -65
  42. telegrinder/bot/dispatch/return_manager/callback_query.py +4 -5
  43. telegrinder/bot/dispatch/return_manager/inline_query.py +3 -4
  44. telegrinder/bot/dispatch/return_manager/message.py +8 -10
  45. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +4 -5
  46. telegrinder/bot/dispatch/view/__init__.py +4 -4
  47. telegrinder/bot/dispatch/view/abc.py +6 -16
  48. telegrinder/bot/dispatch/view/base.py +54 -178
  49. telegrinder/bot/dispatch/view/box.py +19 -18
  50. telegrinder/bot/dispatch/view/callback_query.py +4 -8
  51. telegrinder/bot/dispatch/view/chat_join_request.py +5 -6
  52. telegrinder/bot/dispatch/view/chat_member.py +5 -25
  53. telegrinder/bot/dispatch/view/error.py +9 -0
  54. telegrinder/bot/dispatch/view/inline_query.py +4 -8
  55. telegrinder/bot/dispatch/view/message.py +5 -25
  56. telegrinder/bot/dispatch/view/pre_checkout_query.py +4 -8
  57. telegrinder/bot/dispatch/view/raw.py +3 -109
  58. telegrinder/bot/dispatch/waiter_machine/__init__.py +2 -5
  59. telegrinder/bot/dispatch/waiter_machine/actions.py +6 -4
  60. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +1 -3
  61. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +1 -1
  62. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +11 -7
  63. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
  64. telegrinder/bot/dispatch/waiter_machine/machine.py +43 -60
  65. telegrinder/bot/dispatch/waiter_machine/middleware.py +19 -23
  66. telegrinder/bot/dispatch/waiter_machine/short_state.py +6 -5
  67. telegrinder/bot/polling/__init__.py +0 -0
  68. telegrinder/bot/polling/abc.py +0 -0
  69. telegrinder/bot/polling/polling.py +209 -88
  70. telegrinder/bot/rules/__init__.py +3 -16
  71. telegrinder/bot/rules/abc.py +42 -122
  72. telegrinder/bot/rules/callback_data.py +29 -49
  73. telegrinder/bot/rules/chat_join.py +5 -23
  74. telegrinder/bot/rules/command.py +8 -4
  75. telegrinder/bot/rules/enum_text.py +3 -4
  76. telegrinder/bot/rules/func.py +7 -14
  77. telegrinder/bot/rules/fuzzy.py +3 -4
  78. telegrinder/bot/rules/inline.py +8 -20
  79. telegrinder/bot/rules/integer.py +2 -3
  80. telegrinder/bot/rules/is_from.py +12 -11
  81. telegrinder/bot/rules/logic.py +11 -5
  82. telegrinder/bot/rules/markup.py +22 -14
  83. telegrinder/bot/rules/mention.py +8 -7
  84. telegrinder/bot/rules/message_entities.py +8 -4
  85. telegrinder/bot/rules/node.py +23 -12
  86. telegrinder/bot/rules/payload.py +5 -4
  87. telegrinder/bot/rules/payment_invoice.py +6 -21
  88. telegrinder/bot/rules/regex.py +2 -4
  89. telegrinder/bot/rules/rule_enum.py +8 -7
  90. telegrinder/bot/rules/start.py +5 -6
  91. telegrinder/bot/rules/state.py +1 -1
  92. telegrinder/bot/rules/text.py +4 -15
  93. telegrinder/bot/rules/update.py +3 -4
  94. telegrinder/bot/scenario/__init__.py +0 -0
  95. telegrinder/bot/scenario/abc.py +6 -5
  96. telegrinder/bot/scenario/checkbox.py +1 -1
  97. telegrinder/bot/scenario/choice.py +30 -39
  98. telegrinder/client/__init__.py +3 -5
  99. telegrinder/client/abc.py +11 -6
  100. telegrinder/client/aiohttp.py +141 -27
  101. telegrinder/client/form_data.py +1 -1
  102. telegrinder/model.py +61 -89
  103. telegrinder/modules.py +325 -102
  104. telegrinder/msgspec_utils/__init__.py +40 -0
  105. telegrinder/msgspec_utils/abc.py +18 -0
  106. telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
  107. telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
  108. telegrinder/msgspec_utils/custom_types/enum_meta.py +43 -0
  109. telegrinder/msgspec_utils/custom_types/literal.py +25 -0
  110. telegrinder/msgspec_utils/custom_types/option.py +17 -0
  111. telegrinder/msgspec_utils/decoder.py +389 -0
  112. telegrinder/msgspec_utils/encoder.py +206 -0
  113. telegrinder/{msgspec_json.py → msgspec_utils/json.py} +6 -5
  114. telegrinder/msgspec_utils/tools.py +75 -0
  115. telegrinder/node/__init__.py +24 -7
  116. telegrinder/node/attachment.py +1 -0
  117. telegrinder/node/base.py +154 -72
  118. telegrinder/node/callback_query.py +5 -5
  119. telegrinder/node/collection.py +39 -0
  120. telegrinder/node/command.py +1 -2
  121. telegrinder/node/composer.py +121 -72
  122. telegrinder/node/container.py +11 -8
  123. telegrinder/node/context.py +48 -0
  124. telegrinder/node/either.py +27 -40
  125. telegrinder/node/error.py +41 -0
  126. telegrinder/node/event.py +37 -11
  127. telegrinder/node/exceptions.py +7 -0
  128. telegrinder/node/file.py +0 -0
  129. telegrinder/node/i18n.py +108 -0
  130. telegrinder/node/me.py +3 -2
  131. telegrinder/node/payload.py +1 -1
  132. telegrinder/node/polymorphic.py +63 -28
  133. telegrinder/node/reply_message.py +12 -0
  134. telegrinder/node/rule.py +6 -13
  135. telegrinder/node/scope.py +14 -5
  136. telegrinder/node/session.py +53 -0
  137. telegrinder/node/source.py +41 -9
  138. telegrinder/node/text.py +1 -2
  139. telegrinder/node/tools/__init__.py +0 -0
  140. telegrinder/node/tools/generator.py +3 -5
  141. telegrinder/node/utility.py +16 -0
  142. telegrinder/py.typed +0 -0
  143. telegrinder/rules.py +0 -0
  144. telegrinder/tools/__init__.py +48 -88
  145. telegrinder/tools/aio.py +103 -0
  146. telegrinder/tools/callback_data_serialization/__init__.py +5 -0
  147. telegrinder/tools/{callback_data_serilization → callback_data_serialization}/abc.py +0 -0
  148. telegrinder/tools/{callback_data_serilization → callback_data_serialization}/json_ser.py +2 -3
  149. telegrinder/tools/{callback_data_serilization → callback_data_serialization}/msgpack_ser.py +45 -27
  150. telegrinder/tools/final.py +21 -0
  151. telegrinder/tools/formatting/__init__.py +2 -18
  152. telegrinder/tools/formatting/deep_links/__init__.py +39 -0
  153. telegrinder/tools/formatting/{deep_links.py → deep_links/links.py} +12 -85
  154. telegrinder/tools/formatting/deep_links/parsing.py +90 -0
  155. telegrinder/tools/formatting/deep_links/validators.py +8 -0
  156. telegrinder/tools/formatting/html_formatter.py +18 -45
  157. telegrinder/tools/fullname.py +83 -0
  158. telegrinder/tools/global_context/__init__.py +4 -3
  159. telegrinder/tools/global_context/abc.py +17 -14
  160. telegrinder/tools/global_context/builtin_context.py +39 -0
  161. telegrinder/tools/global_context/global_context.py +138 -39
  162. telegrinder/tools/input_file_directory.py +0 -0
  163. telegrinder/tools/keyboard/__init__.py +39 -0
  164. telegrinder/tools/keyboard/abc.py +159 -0
  165. telegrinder/tools/keyboard/base.py +77 -0
  166. telegrinder/tools/keyboard/buttons/__init__.py +14 -0
  167. telegrinder/tools/keyboard/buttons/base.py +18 -0
  168. telegrinder/tools/{buttons.py → keyboard/buttons/buttons.py} +71 -23
  169. telegrinder/tools/keyboard/buttons/static_buttons.py +56 -0
  170. telegrinder/tools/keyboard/buttons/tools.py +18 -0
  171. telegrinder/tools/keyboard/data.py +20 -0
  172. telegrinder/tools/keyboard/keyboard.py +131 -0
  173. telegrinder/tools/keyboard/static_keyboard.py +83 -0
  174. telegrinder/tools/lifespan.py +87 -51
  175. telegrinder/tools/limited_dict.py +4 -1
  176. telegrinder/tools/loop_wrapper.py +332 -0
  177. telegrinder/tools/magic/__init__.py +32 -0
  178. telegrinder/tools/magic/annotations.py +165 -0
  179. telegrinder/tools/magic/dictionary.py +20 -0
  180. telegrinder/tools/magic/function.py +246 -0
  181. telegrinder/tools/magic/shortcut.py +111 -0
  182. telegrinder/tools/parse_mode.py +9 -3
  183. telegrinder/tools/singleton/__init__.py +4 -0
  184. telegrinder/tools/singleton/abc.py +14 -0
  185. telegrinder/tools/singleton/singleton.py +18 -0
  186. telegrinder/tools/state_storage/__init__.py +0 -0
  187. telegrinder/tools/state_storage/abc.py +6 -1
  188. telegrinder/tools/state_storage/memory.py +1 -1
  189. telegrinder/tools/strings.py +0 -0
  190. telegrinder/types/__init__.py +307 -268
  191. telegrinder/types/enums.py +68 -37
  192. telegrinder/types/input_file.py +3 -3
  193. telegrinder/types/methods.py +5699 -5055
  194. telegrinder/types/methods_utils.py +62 -0
  195. telegrinder/types/objects.py +1782 -994
  196. telegrinder/verification_utils.py +3 -1
  197. telegrinder-0.5.1.dist-info/METADATA +162 -0
  198. telegrinder-0.5.1.dist-info/RECORD +200 -0
  199. {telegrinder-0.4.2.dist-info → telegrinder-0.5.1.dist-info}/licenses/LICENSE +2 -2
  200. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +0 -20
  201. telegrinder/bot/rules/id.py +0 -24
  202. telegrinder/bot/rules/message.py +0 -15
  203. telegrinder/client/sonic.py +0 -212
  204. telegrinder/msgspec_utils.py +0 -478
  205. telegrinder/tools/adapter/__init__.py +0 -19
  206. telegrinder/tools/adapter/abc.py +0 -49
  207. telegrinder/tools/adapter/dataclass.py +0 -56
  208. telegrinder/tools/adapter/errors.py +0 -5
  209. telegrinder/tools/adapter/event.py +0 -61
  210. telegrinder/tools/adapter/node.py +0 -46
  211. telegrinder/tools/adapter/raw_event.py +0 -27
  212. telegrinder/tools/adapter/raw_update.py +0 -30
  213. telegrinder/tools/callback_data_serilization/__init__.py +0 -5
  214. telegrinder/tools/error_handler/__init__.py +0 -10
  215. telegrinder/tools/error_handler/abc.py +0 -30
  216. telegrinder/tools/error_handler/error.py +0 -9
  217. telegrinder/tools/error_handler/error_handler.py +0 -179
  218. telegrinder/tools/formatting/spec_html_formats.py +0 -75
  219. telegrinder/tools/functional.py +0 -8
  220. telegrinder/tools/global_context/telegrinder_ctx.py +0 -27
  221. telegrinder/tools/i18n/__init__.py +0 -12
  222. telegrinder/tools/i18n/abc.py +0 -32
  223. telegrinder/tools/i18n/middleware/__init__.py +0 -3
  224. telegrinder/tools/i18n/middleware/abc.py +0 -22
  225. telegrinder/tools/i18n/simple.py +0 -43
  226. telegrinder/tools/keyboard.py +0 -132
  227. telegrinder/tools/loop_wrapper/__init__.py +0 -4
  228. telegrinder/tools/loop_wrapper/abc.py +0 -20
  229. telegrinder/tools/loop_wrapper/loop_wrapper.py +0 -169
  230. telegrinder/tools/magic.py +0 -344
  231. telegrinder-0.4.2.dist-info/METADATA +0 -151
  232. telegrinder-0.4.2.dist-info/RECORD +0 -182
  233. {telegrinder-0.4.2.dist-info → telegrinder-0.5.1.dist-info}/WHEEL +0 -0
@@ -1,133 +1,144 @@
1
- import dataclasses
2
- import inspect
3
1
  import typing
2
+ from collections import defaultdict
3
+ from typing import Any
4
4
 
5
- from fntypes.error import UnwrapError
5
+ from fntypes.option import Some
6
6
  from fntypes.result import Error, Ok, Result
7
7
 
8
8
  from telegrinder.bot.dispatch.context import Context
9
- from telegrinder.modules import logger
10
9
  from telegrinder.node.base import (
10
+ AnyNode,
11
11
  ComposeError,
12
12
  IsNode,
13
13
  Name,
14
- NodeImpersonation,
14
+ NodeClass,
15
15
  NodeScope,
16
- NodeType,
16
+ as_node,
17
17
  unwrap_node,
18
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)
19
+ from telegrinder.node.context import get_global_session, set_global_session
20
+ from telegrinder.node.scope import get_scope
21
+ from telegrinder.node.session import NodeSession
22
+ from telegrinder.tools.aio import Generator, maybe_awaitable, next_generator
23
+ from telegrinder.tools.fullname import fullname
24
+ from telegrinder.tools.global_context.builtin_context import TelegrinderContext
25
+ from telegrinder.tools.magic import bundle, get_func_annotations, join_dicts
26
+
27
+ type ComposeGenerator = Generator[typing.Any, typing.Any, typing.Any]
28
+ type Impls = dict[type[typing.Any], type[typing.Any]]
29
+
30
+ CONTEXT_STORE_NODES_KEY: typing.Final[str] = "_node_ctx"
31
+ GLOBAL_VALUE_KEY: typing.Final[str] = "_value"
32
+ TELEGRINDER_CONTEXT: typing.Final[TelegrinderContext] = TelegrinderContext()
33
+
34
+
35
+ def get_impls(
36
+ compose_function: typing.Callable[..., typing.Any],
37
+ impls: Impls,
38
+ /,
39
+ ) -> dict[str, typing.Any]:
40
+ return {
41
+ key: impls[tp]
42
+ for key, annotation in get_func_annotations(compose_function).items()
43
+ if typing.get_origin(annotation) is type
44
+ and (typing.get_origin(tp := typing.get_args(annotation)[0]) or tp) in impls
45
+ }
29
46
 
30
47
 
31
48
  async def compose_node(
32
- node: type[NodeType],
49
+ node: IsNode,
33
50
  linked: dict[type[typing.Any], typing.Any],
34
51
  data: dict[type[typing.Any], typing.Any] | None = None,
52
+ impls: Impls | None = None,
35
53
  ) -> "NodeSession":
36
54
  subnodes = node.get_subnodes()
37
- kwargs = magic_bundle(node.compose, join_dicts(subnodes, linked))
55
+ compose = bundle(node.compose, join_dicts(subnodes, linked), bundle_kwargs=True)
38
56
 
39
- # Linking data via typebundle
40
57
  if data:
41
- kwargs.update(magic_bundle(node.compose, data, typebundle=True))
58
+ compose &= bundle(node.compose, data, typebundle=True)
42
59
 
60
+ if impls:
61
+ compose &= bundle(node.compose, get_impls(node.compose, impls))
62
+
63
+ result = compose()
43
64
  if node.is_generator():
44
- generator = typing.cast(AsyncGenerator, node.compose(**kwargs))
45
- value = await generator.asend(None)
65
+ generator = typing.cast("ComposeGenerator", result)
66
+ value = await next_generator(generator)
46
67
  else:
47
68
  generator = None
48
- value = node.compose(**kwargs)
49
- if inspect.isawaitable(value):
50
- value = await value
69
+ value = await maybe_awaitable(result)
51
70
 
52
- return NodeSession(node, value, subnodes={}, generator=generator)
71
+ return NodeSession(node, value, generator)
53
72
 
54
73
 
55
74
  async def compose_nodes(
56
- nodes: typing.Mapping[str, IsNode | NodeImpersonation],
75
+ nodes: typing.Mapping[str, AnyNode],
57
76
  ctx: Context,
58
77
  data: dict[type[typing.Any], typing.Any] | None = None,
78
+ impls: Impls | None = None,
59
79
  ) -> Result["NodeCollection", ComposeError]:
60
- logger.debug("Composing nodes: ({})...", " ".join(f"{k}={v!r}" for k, v in nodes.items()))
61
-
80
+ impls = impls or TELEGRINDER_CONTEXT.composer.unwrap().selected_impls
62
81
  data = {Context: ctx} | (data or {})
63
82
  parent_nodes = dict[IsNode, NodeSession]()
64
83
  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()}
84
+ unwrapped_nodes = {(key, n := (as_node(node)), node): unwrap_node(n) for key, node in nodes.items()}
85
+
86
+ for (parent_node_name, parent_node_t, parent_original_type), linked_nodes in unwrapped_nodes.items():
87
+ data.update({Name: parent_node_name, NodeClass: parent_original_type})
66
88
 
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
89
+ local_nodes = LocalNodeCollection()
90
+ subnodes = dict[Any, Any]()
71
91
 
72
92
  for node_t in linked_nodes:
93
+ node_t = as_node(node_t)
73
94
  scope = get_scope(node_t)
74
95
 
75
96
  if scope is NodeScope.PER_EVENT and node_t in event_nodes:
76
97
  local_nodes[node_t] = event_nodes[node_t]
77
98
  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), {})
99
+
100
+ if scope is NodeScope.GLOBAL and (global_session := get_global_session(node_t)) is not None:
101
+ local_nodes[node_t] = global_session
80
102
  continue
81
103
 
82
104
  subnodes |= {
83
105
  k: session.value for k, session in (local_nodes | event_nodes).items() if k not in subnodes
84
106
  }
85
107
  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)}"))
108
+ session = await compose_node(node_t, linked=subnodes, data=data, impls=impls)
109
+ except ComposeError as error:
110
+ await local_nodes.close_local_sessions()
111
+ return Error(ComposeError(f"Cannot compose node `{fullname(node_t)}`, error: {error.message!r}"))
112
+
113
+ if scope is NodeScope.PER_CALL:
114
+ await local_nodes.set_local_session(node_t, session)
115
+ else:
116
+ local_nodes[node_t] = session
92
117
 
93
118
  if scope is NodeScope.PER_EVENT:
94
- event_nodes[node_t] = local_nodes[node_t]
119
+ event_nodes[node_t] = session
95
120
  elif scope is NodeScope.GLOBAL:
96
- setattr(node_t, GLOBAL_VALUE_KEY, local_nodes[node_t].value)
121
+ set_global_session(node_t, session)
97
122
 
98
123
  parent_nodes[parent_node_t] = local_nodes[parent_node_t]
99
124
 
100
- return Ok(NodeCollection({k: parent_nodes[t] for k, t in unwrapped_nodes}))
125
+ return Ok(NodeCollection(sessions={k: parent_nodes[t] for k, t, _ in unwrapped_nodes}))
101
126
 
102
127
 
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 ">")
128
+ class LocalNodeCollection(dict[IsNode, NodeSession]):
129
+ def __init__(self) -> None:
130
+ super().__init__()
112
131
 
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
132
+ async def set_local_session(self, node: IsNode, session: NodeSession, /) -> NodeSession:
133
+ if node in self:
134
+ await self[node].close()
120
135
 
121
- for subnode in self.subnodes.values():
122
- await subnode.close(scopes=scopes)
136
+ self[node] = session
137
+ return session
123
138
 
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
139
+ async def close_local_sessions(self) -> None:
140
+ for session in self.values():
141
+ await session.close(scopes=(NodeScope.PER_CALL,))
131
142
 
132
143
 
133
144
  class NodeCollection:
@@ -138,7 +149,7 @@ class NodeCollection:
138
149
  self._values: dict[str, typing.Any] = {}
139
150
 
140
151
  def __repr__(self) -> str:
141
- return "<{}: sessions={!r}>".format(self.__class__.__name__, self.sessions)
152
+ return "<{}: sessions={!r}>".format(type(self).__name__, self.sessions)
142
153
 
143
154
  @property
144
155
  def values(self) -> dict[str, typing.Any]:
@@ -160,4 +171,42 @@ class NodeCollection:
160
171
  await session.close(with_value, scopes=scopes)
161
172
 
162
173
 
163
- __all__ = ("NodeCollection", "NodeSession", "compose_node", "compose_nodes")
174
+ class Composer:
175
+ impls: dict[type[typing.Any], set[typing.Any]]
176
+ selected_impls: Impls
177
+
178
+ def __init__(self) -> None:
179
+ self.impls = defaultdict(set)
180
+ self.selected_impls = dict()
181
+
182
+ def __setitem__(self, for_type: typing.Any, impl: type[typing.Any], /) -> None:
183
+ for_type = typing.get_origin(for_type) or for_type
184
+
185
+ if for_type not in self.impls:
186
+ raise LookupError(f"No impls defined for type of `{fullname(for_type)}`.")
187
+
188
+ if (typing.get_origin(impl) or impl) not in self.impls[for_type]:
189
+ raise LookupError(f"Impl `{fullname(impl)}` is not defined for type of `{fullname(for_type)}`.")
190
+
191
+ self.selected_impls[for_type] = impl
192
+
193
+ def impl[T](self, for_type: typing.Any, /) -> typing.Callable[[type[T]], type[T]]:
194
+ def decorator(impl: type[T], /) -> type[T]:
195
+ self.impls[typing.get_origin(for_type) or for_type].add(impl)
196
+ return impl
197
+
198
+ return decorator
199
+
200
+ async def compose_nodes(
201
+ self,
202
+ nodes: typing.Mapping[str, AnyNode],
203
+ ctx: Context,
204
+ data: dict[type[typing.Any], typing.Any] | None = None,
205
+ ) -> Result[NodeCollection, ComposeError]:
206
+ return await compose_nodes(nodes, ctx, data, self.selected_impls)
207
+
208
+
209
+ TELEGRINDER_CONTEXT.composer = Some(Composer())
210
+
211
+
212
+ __all__ = ("Composer", "NodeCollection", "compose_node", "compose_nodes")
@@ -7,6 +7,8 @@ class ContainerNode(Node):
7
7
  linked_nodes: typing.ClassVar[list[IsNode]]
8
8
  composer: typing.Callable[..., typing.Awaitable[typing.Any]]
9
9
 
10
+ __subnodes__: typing.ClassVar[dict[str, IsNode] | None] = None
11
+
10
12
  @classmethod
11
13
  async def compose(cls, **kw: typing.Any) -> typing.Any:
12
14
  subnodes = cls.get_subnodes().keys()
@@ -14,20 +16,21 @@ class ContainerNode(Node):
14
16
 
15
17
  @classmethod
16
18
  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
19
+ if cls.__subnodes__ is None:
20
+ cls.__subnodes__ = {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes, start=1)}
21
+ return cls.__subnodes__
23
22
 
24
23
  @classmethod
25
24
  def link_nodes(
26
25
  cls,
27
26
  linked_nodes: list[IsNode],
28
27
  composer: typing.Callable[..., typing.Awaitable[typing.Any]],
29
- ) -> type["ContainerNode"]:
30
- return type(cls.__name__, (cls,), {"linked_nodes": linked_nodes, "composer": classmethod(composer)})
28
+ ) -> type[typing.Self]:
29
+ return type(
30
+ cls.__name__,
31
+ (cls,),
32
+ {"linked_nodes": linked_nodes, "composer": classmethod(composer), "__module__": __name__},
33
+ ) # type: ignore
31
34
 
32
35
 
33
36
  __all__ = ("ContainerNode",)
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+
5
+ from telegrinder.node.scope import NodeScope
6
+ from telegrinder.tools.global_context import GlobalContext, ctx_var
7
+ from telegrinder.tools.global_context.builtin_context import TelegrinderContext
8
+
9
+ if typing.TYPE_CHECKING:
10
+ from telegrinder.node.base import IsNode
11
+ from telegrinder.node.composer import NodeSession
12
+
13
+ TELEGRINDER_CONTEXT: typing.Final[TelegrinderContext] = TelegrinderContext()
14
+
15
+
16
+ class NodeGlobalContext(GlobalContext):
17
+ __ctx_name__ = "node_context"
18
+
19
+ global_sessions: dict[IsNode, NodeSession] = ctx_var(
20
+ const=True,
21
+ init=False,
22
+ default_factory=dict,
23
+ )
24
+
25
+ async def close_global_scopes(self) -> None:
26
+ for session in self.global_sessions.values():
27
+ await session.close(scopes=(NodeScope.GLOBAL,))
28
+
29
+ self.global_sessions.clear()
30
+
31
+
32
+ def get_global_session(node: IsNode, /) -> NodeSession | None:
33
+ return NODE_CONTEXT.global_sessions.get(node)
34
+
35
+
36
+ def set_global_session(node: IsNode, session: NodeSession, /) -> None:
37
+ NODE_CONTEXT.global_sessions[node] = session
38
+
39
+
40
+ @TELEGRINDER_CONTEXT.loop_wrapper.lifespan.on_shutdown
41
+ async def close_nodes_global_scopes() -> None:
42
+ await NODE_CONTEXT.close_global_scopes()
43
+
44
+
45
+ NODE_CONTEXT: typing.Final[NodeGlobalContext] = NodeGlobalContext()
46
+
47
+
48
+ __all__ = ("NODE_CONTEXT", "NodeGlobalContext", "get_global_session", "set_global_session")
@@ -4,9 +4,10 @@ from fntypes.result import Ok
4
4
 
5
5
  from telegrinder.api.api import API
6
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
7
+ from telegrinder.node.base import ComposeError, FactoryNode, IsNode
8
+ from telegrinder.node.composer import compose_nodes
9
+ from telegrinder.node.scope import per_call
10
+ from telegrinder.tools.fullname import fullname
10
11
  from telegrinder.types.objects import Update
11
12
 
12
13
 
@@ -26,57 +27,43 @@ class _Either(FactoryNode):
26
27
  ```
27
28
  """
28
29
 
29
- nodes: tuple[type[Node], type[Node] | None]
30
+ nodes: tuple[IsNode, IsNode | None]
30
31
 
31
- def __class_getitem__(cls, node: type[Node] | tuple[type[Node], type[Node]], /):
32
+ def __class_getitem__(cls, node: IsNode | tuple[IsNode, IsNode], /) -> typing.Any:
32
33
  nodes = (node, None) if not isinstance(node, tuple) else node
33
34
  assert len(nodes) == 2, "Node `Either` must have at least two nodes."
34
35
  return cls(nodes=nodes)
35
36
 
36
37
  @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, {})
38
+ async def compose(cls, api: API, update: Update, context: Context) -> typing.Any:
39
+ composed = True
40
40
 
41
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)))
42
+ if node is not None:
43
+ match await compose_nodes(dict(node_result=node), context, {API: api, Update: update}):
44
+ case Ok(col):
45
+ value = col.values["node_result"]
46
+ yield value
47
+ await col.close_all(with_value=value)
48
+ break
49
+ case _:
50
+ composed = False
51
+ else:
52
+ yield None
53
+ break
54
+
55
+ if not composed:
56
+ raise ComposeError(
57
+ "Cannot compose either nodes: {}.".format(", ".join(fullname(n) for n in cls.nodes))
58
+ )
72
59
 
73
60
 
74
61
  if typing.TYPE_CHECKING:
75
62
  type Either[Left, Right: typing.Any | None] = Left | Right
76
63
  type Optional[Left] = Either[Left, None]
77
64
  else:
78
- Either = _Either
79
- Optional = type("Optional", (Either,), {})
65
+ Either = type("Either", (_Either,), {"__module__": __name__})
66
+ Optional = type("Optional", (_Either,), {"__module__": __name__})
80
67
 
81
68
 
82
69
  __all__ = ("Either", "Optional")
@@ -0,0 +1,41 @@
1
+ import dataclasses
2
+
3
+ import typing_extensions as typing
4
+
5
+ from telegrinder.bot.dispatch.context import Context
6
+ from telegrinder.node.base import ComposeError, DataNode
7
+ from telegrinder.node.utility import TypeArgs
8
+
9
+ type ExceptionType = type[BaseException]
10
+
11
+ ExceptionT = typing.TypeVar("ExceptionT", bound=BaseException, default=BaseException)
12
+ ExceptionTs = typing.TypeVarTuple("ExceptionTs", default=typing.Unpack[tuple[BaseException, ...]])
13
+
14
+
15
+ def can_catch[ExceptionT: BaseException](
16
+ exc: BaseException | ExceptionType,
17
+ exc_types: type[ExceptionT] | tuple[type[ExceptionT], ...],
18
+ ) -> typing.TypeGuard[ExceptionT]:
19
+ return issubclass(exc, exc_types) if isinstance(exc, type) else isinstance(exc, exc_types)
20
+
21
+
22
+ @dataclasses.dataclass(kw_only=True, frozen=True)
23
+ class Error(DataNode, typing.Generic[*ExceptionTs]):
24
+ exception_update: BaseException
25
+
26
+ @property
27
+ def exception(self: "Error[*tuple[ExceptionT, ...]]") -> ExceptionT:
28
+ return self.exception_update # type: ignore
29
+
30
+ @classmethod
31
+ def compose(cls, ctx: Context, type_args: TypeArgs) -> typing.Self:
32
+ exception = ctx.exception_update.expect(ComposeError("No exception."))
33
+ exception_types: tuple[ExceptionType, ...] | None = type_args.get(ExceptionTs)
34
+
35
+ if exception_types is None or can_catch(exception, exception_types):
36
+ return cls(exception_update=exception)
37
+
38
+ raise ComposeError("Foreign exception.")
39
+
40
+
41
+ __all__ = ("Error",)
telegrinder/node/event.py CHANGED
@@ -2,12 +2,16 @@ import dataclasses
2
2
  import typing
3
3
 
4
4
  import msgspec
5
+ from fntypes.option import Some
5
6
 
6
7
  from telegrinder.api.api import API
7
- from telegrinder.bot.cute_types import BaseCute
8
+ from telegrinder.bot.cute_types.base import BaseCute
9
+ from telegrinder.bot.cute_types.update import UpdateCute
10
+ from telegrinder.bot.dispatch.context import Context
8
11
  from telegrinder.msgspec_utils import decoder
9
- from telegrinder.node.base import ComposeError, FactoryNode
10
- from telegrinder.types.objects import Update
12
+ from telegrinder.node.base import ComposeError, FactoryNode, scalar_node
13
+ from telegrinder.tools.fullname import fullname
14
+ from telegrinder.types.objects import Model, Update
11
15
 
12
16
  if typing.TYPE_CHECKING:
13
17
  from _typeshed import DataclassInstance
@@ -15,6 +19,17 @@ if typing.TYPE_CHECKING:
15
19
  type DataclassType = DataclassInstance | msgspec.Struct | dict[str, typing.Any]
16
20
 
17
21
 
22
+ @scalar_node
23
+ class UpdateNode:
24
+ @classmethod
25
+ def compose(cls, update: Update, api: API, context: Context) -> UpdateCute:
26
+ match context.update_cute:
27
+ case Some(update_cute):
28
+ return update_cute
29
+ case _:
30
+ return context.add_update_cute(update, api).update_cute.unwrap()
31
+
32
+
18
33
  class _EventNode(FactoryNode):
19
34
  dataclass: type[DataclassType]
20
35
  orig_dataclass: type[DataclassType]
@@ -23,13 +38,24 @@ class _EventNode(FactoryNode):
23
38
  return cls(dataclass=dataclass, orig_dataclass=typing.get_origin(dataclass) or dataclass)
24
39
 
25
40
  @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)
41
+ def compose(cls, update_cute: UpdateNode, raw_update: Update, api: API) -> DataclassType:
42
+ if cls.orig_dataclass is UpdateCute:
43
+ return update_cute
31
44
 
32
- elif issubclass(cls.orig_dataclass, msgspec.Struct) or dataclasses.is_dataclass(
45
+ if issubclass(cls.orig_dataclass, BaseCute | Model):
46
+ incoming_update = (
47
+ update_cute.incoming_update
48
+ if issubclass(cls.orig_dataclass, BaseCute)
49
+ else raw_update.incoming_update
50
+ )
51
+
52
+ if type(incoming_update) is not cls.orig_dataclass:
53
+ raise ComposeError(f"Incoming update is not `{fullname(cls.orig_dataclass)}`.")
54
+
55
+ return incoming_update
56
+
57
+ try:
58
+ if issubclass(cls.orig_dataclass, msgspec.Struct) or dataclasses.is_dataclass(
33
59
  cls.orig_dataclass,
34
60
  ):
35
61
  dataclass = decoder.convert(
@@ -42,13 +68,13 @@ class _EventNode(FactoryNode):
42
68
 
43
69
  return dataclass
44
70
  except Exception as exc:
45
- raise ComposeError(f"Cannot parse an update object into {cls.dataclass!r}, error: {str(exc)}")
71
+ raise ComposeError(f"Cannot parse an update object into `{fullname(cls.dataclass)}`, error: {exc}")
46
72
 
47
73
 
48
74
  if typing.TYPE_CHECKING:
49
75
  type EventNode[Dataclass: DataclassType] = Dataclass
50
76
  else:
51
- EventNode = _EventNode
77
+ EventNode = type("EventNode", (_EventNode,), {"__module__": __name__})
52
78
 
53
79
 
54
80
  __all__ = ("EventNode",)
@@ -0,0 +1,7 @@
1
+ class ComposeError(BaseException):
2
+ def __init__(self, message: str = "<no error description>", /) -> None:
3
+ self.message = message
4
+ super().__init__(message)
5
+
6
+
7
+ __all__ = ("ComposeError",)
telegrinder/node/file.py CHANGED
File without changes