telegrinder 0.4.2__py3-none-any.whl → 0.5.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 (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 +2621 -2590
  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 +64 -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 +7846 -7058
  196. telegrinder/verification_utils.py +3 -1
  197. telegrinder-0.5.0.dist-info/METADATA +162 -0
  198. telegrinder-0.5.0.dist-info/RECORD +200 -0
  199. {telegrinder-0.4.2.dist-info → telegrinder-0.5.0.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.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,108 @@
1
+ import abc
2
+ import dataclasses
3
+ import gettext
4
+ import os
5
+ import pathlib
6
+ import typing
7
+ from functools import cached_property
8
+
9
+ from telegrinder.node.base import DataNode, GlobalNode
10
+ from telegrinder.node.source import Locale
11
+
12
+ type Separator = KeySeparator
13
+
14
+ DEFAULT_SEPARATOR: typing.Final[str] = "-"
15
+
16
+
17
+ @dataclasses.dataclass(frozen=True)
18
+ class KeySeparator(GlobalNode[Separator], DataNode):
19
+ value: str
20
+
21
+ @classmethod
22
+ def set(cls, value: str, /) -> None:
23
+ super().set(cls(value))
24
+
25
+ @classmethod
26
+ def compose(cls) -> Separator:
27
+ return cls.get(default=cls(DEFAULT_SEPARATOR))
28
+
29
+
30
+ @dataclasses.dataclass(kw_only=True)
31
+ class ABCTranslator(DataNode, abc.ABC):
32
+ locale: str
33
+ separator: str
34
+ _keys: list[str] = dataclasses.field(default_factory=list[str], init=False)
35
+
36
+ @typing.overload
37
+ def __call__(self, **context: typing.Any) -> str: ...
38
+
39
+ @typing.overload
40
+ def __call__(self, message_id: str, /, **context: typing.Any) -> str: ...
41
+
42
+ def __call__(self, message_id: str | None = None, **context: typing.Any) -> str:
43
+ result = self.translate(message_id or self.message_id, **context)
44
+ if not message_id:
45
+ self._keys.clear()
46
+ return result
47
+
48
+ def __getattr__(self, __key: str) -> typing.Self:
49
+ self._keys.append(__key)
50
+ return self
51
+
52
+ @property
53
+ def message_id(self) -> str:
54
+ return self.separator.join(self._keys)
55
+
56
+ @abc.abstractmethod
57
+ def translate(self, message_id: str, **context: typing.Any) -> str:
58
+ pass
59
+
60
+ @classmethod
61
+ def compose(cls, locale: Locale, separator: KeySeparator) -> typing.Self:
62
+ return cls(locale=locale, separator=separator.value)
63
+
64
+
65
+ @dataclasses.dataclass
66
+ class I18NConfig:
67
+ domain: str
68
+ folder: str | pathlib.Path
69
+ default_locale: str = dataclasses.field(default="en")
70
+
71
+ @cached_property
72
+ def translators(self) -> dict[str, gettext.GNUTranslations]:
73
+ result = {}
74
+
75
+ for name in os.listdir(self.folder):
76
+ if not os.path.isdir(os.path.join(self.folder, name)):
77
+ continue
78
+
79
+ mo_path = os.path.join(self.folder, name, "LC_MESSAGES", f"{self.domain}.mo")
80
+
81
+ if os.path.exists(mo_path):
82
+ with open(mo_path, "rb") as f:
83
+ result[name] = gettext.GNUTranslations(f)
84
+ elif os.path.exists(mo_path[:-2] + "po"):
85
+ raise FileNotFoundError(".po files should be compiled first")
86
+
87
+ return result
88
+
89
+ def get_translator(self, locale: str, /) -> gettext.GNUTranslations:
90
+ locale = locale if locale in self.translators else self.default_locale
91
+ return self.translators[locale]
92
+
93
+
94
+ class BaseTranslator(ABCTranslator):
95
+ config: typing.ClassVar[I18NConfig]
96
+
97
+ def __class_getitem__(cls, config: I18NConfig, /) -> typing.Any:
98
+ return type(cls.__name__, (cls,), dict(config=config))
99
+
100
+ @classmethod
101
+ def configure(cls, config: I18NConfig, /) -> None:
102
+ cls.config = config
103
+
104
+ def translate(self, message_id: str, **context: typing.Any) -> str:
105
+ return self.config.get_translator(self.locale).gettext(message_id).format(**context)
106
+
107
+
108
+ __all__ = ("ABCTranslator", "BaseTranslator", "I18NConfig", "KeySeparator")
telegrinder/node/me.py CHANGED
@@ -1,10 +1,11 @@
1
1
  from telegrinder.api.api import API
2
2
  from telegrinder.node.base import ComposeError, scalar_node
3
- from telegrinder.node.scope import GLOBAL
3
+ from telegrinder.node.scope import global_node
4
4
  from telegrinder.types.objects import User
5
5
 
6
6
 
7
- @scalar_node(scope=GLOBAL)
7
+ @scalar_node
8
+ @global_node
8
9
  class Me:
9
10
  @classmethod
10
11
  async def compose(cls, api: API) -> User:
@@ -8,7 +8,7 @@ from telegrinder.bot.cute_types.message import MessageCute
8
8
  from telegrinder.bot.cute_types.pre_checkout_query import PreCheckoutQueryCute
9
9
  from telegrinder.node.base import ComposeError, DataNode, FactoryNode, GlobalNode, scalar_node
10
10
  from telegrinder.node.polymorphic import Polymorphic, impl
11
- from telegrinder.tools.callback_data_serilization import ABCDataSerializer, JSONSerializer
11
+ from telegrinder.tools.callback_data_serialization import ABCDataSerializer, JSONSerializer
12
12
 
13
13
 
14
14
  @scalar_node[str]
@@ -1,4 +1,3 @@
1
- import inspect
2
1
  import typing
3
2
 
4
3
  from fntypes.result import Error, Ok
@@ -9,16 +8,48 @@ from telegrinder.bot.dispatch.context import Context
9
8
  from telegrinder.modules import logger
10
9
  from telegrinder.node.base import ComposeError, Node, get_nodes
11
10
  from telegrinder.node.composer import CONTEXT_STORE_NODES_KEY, NodeSession, compose_nodes
12
- from telegrinder.node.scope import NodeScope
13
- from telegrinder.tools.magic import get_polymorphic_implementations, impl, magic_bundle
11
+ from telegrinder.node.scope import NodeScope, get_scope
12
+ from telegrinder.tools.aio import maybe_awaitable
13
+ from telegrinder.tools.fullname import fullname
14
+ from telegrinder.tools.magic.function import bundle
14
15
  from telegrinder.types.objects import Update
15
16
 
17
+ type Impl = type[classmethod]
18
+
19
+ MORPH_IMPLEMENTATIONS_KEY: typing.Final[str] = "__morph_implementations__"
20
+ IMPL_MARK_KEY: typing.Final[str] = "_is_impl"
21
+
22
+
23
+ @typing.cast("typing.Callable[..., Impl]", lambda f: f)
24
+ def impl(method: typing.Callable[..., typing.Any]):
25
+ setattr(method, IMPL_MARK_KEY, True)
26
+ return classmethod(method)
27
+
28
+
29
+ def get_polymorphic_implementations(
30
+ cls: type["Polymorphic"],
31
+ /,
32
+ ) -> list[typing.Callable[typing.Concatenate[type["Polymorphic"], ...], typing.Any]]:
33
+ moprh_impls = getattr(cls, MORPH_IMPLEMENTATIONS_KEY, None)
34
+ if moprh_impls is not None:
35
+ return moprh_impls
36
+
37
+ impls = []
38
+ for cls_ in cls.mro():
39
+ impls += [
40
+ obj.__func__
41
+ for obj in vars(cls_).values()
42
+ if isinstance(obj, classmethod) and getattr(obj.__func__, IMPL_MARK_KEY, False)
43
+ ]
44
+
45
+ setattr(cls, MORPH_IMPLEMENTATIONS_KEY, impls)
46
+ return impls
47
+
16
48
 
17
49
  class Polymorphic(Node):
18
50
  @classmethod
19
51
  async def compose(cls, raw_update: Update, update: UpdateCute, context: Context) -> typing.Any:
20
- logger.debug(f"Composing polymorphic node {cls.__name__!r}...")
21
- scope = getattr(cls, "scope", None)
52
+ scope = get_scope(cls)
22
53
  node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
23
54
  data = {
24
55
  API: update.ctx_api,
@@ -27,39 +58,43 @@ class Polymorphic(Node):
27
58
  }
28
59
 
29
60
  for i, impl_ in enumerate(get_polymorphic_implementations(cls)):
30
- logger.debug("Checking impl {!r}...", impl_.__name__)
31
- node_collection = None
61
+ # To determine whether this is a right morph, all subnodes must be resolved
62
+ if scope is NodeScope.PER_EVENT and (cls, i) in node_ctx:
63
+ return node_ctx[(cls, i)].value
32
64
 
33
65
  match await compose_nodes(get_nodes(impl_), context, data=data):
34
66
  case Ok(col):
35
67
  node_collection = col
36
68
  case Error(err):
37
- logger.debug(f"Composition failed with error: {err!r}")
69
+ logger.debug(
70
+ "Impl `{}` composition failed with error: {!r}",
71
+ fullname(impl_),
72
+ err,
73
+ )
74
+ continue
38
75
 
39
- if node_collection is None:
40
- logger.debug("Impl {!r} composition failed!", impl_.__name__)
41
- continue
76
+ result = None
42
77
 
43
- # To determine whether this is a right morph, all subnodes must be resolved
44
- if scope is NodeScope.PER_EVENT and (cls, i) in node_ctx:
45
- logger.debug(
46
- "Morph is already cached as per_event node, using its value. Impl {!r} succeeded!",
47
- impl_.__name__,
78
+ try:
79
+ result = await maybe_awaitable(
80
+ bundle(impl_, data, typebundle=True)(
81
+ cls,
82
+ **node_collection.values,
83
+ ),
48
84
  )
49
- res: NodeSession = node_ctx[(cls, i)]
50
- await node_collection.close_all()
51
- return res.value
52
-
53
- result = impl_(cls, **node_collection.values | magic_bundle(impl_, data, typebundle=True))
54
- if inspect.isawaitable(result):
55
- result = await result
56
85
 
57
- if scope is NodeScope.PER_EVENT:
58
- node_ctx[(cls, i)] = NodeSession(cls, result, {})
86
+ if scope is NodeScope.PER_EVENT:
87
+ node_ctx[(cls, i)] = NodeSession(cls, result)
59
88
 
60
- await node_collection.close_all(with_value=result)
61
- logger.debug("Impl {!r} succeeded with value: {!r}", impl_.__name__, result)
62
- return result
89
+ return result
90
+ except ComposeError as compose_error:
91
+ logger.debug(
92
+ "Failed to compose morph impl `{}`, error: {!r}",
93
+ fullname(impl_),
94
+ compose_error.message,
95
+ )
96
+ finally:
97
+ await node_collection.close_all(with_value=result)
63
98
 
64
99
  raise ComposeError("No implementation found.")
65
100
 
@@ -0,0 +1,12 @@
1
+ from telegrinder.bot.cute_types.message import MessageCute
2
+ from telegrinder.node.base import ComposeError, scalar_node
3
+
4
+
5
+ @scalar_node
6
+ class ReplyMessage:
7
+ @classmethod
8
+ def compose(cls, message: MessageCute) -> MessageCute:
9
+ return message.reply_to_message.expect(ComposeError("Message doesn't have reply"))
10
+
11
+
12
+ __all__ = ("ReplyMessage",)
telegrinder/node/rule.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import dataclasses
2
- import importlib
3
2
  import typing
4
3
 
5
4
  from telegrinder.bot.cute_types.update import UpdateCute
@@ -9,6 +8,12 @@ from telegrinder.node.base import ComposeError, Node
9
8
  if typing.TYPE_CHECKING:
10
9
  from telegrinder.bot.dispatch.process import check_rule
11
10
  from telegrinder.bot.rules.abc import ABCRule
11
+ else:
12
+
13
+ def check_rule(*args, **kwargs):
14
+ from telegrinder.bot.dispatch.process import check_rule
15
+
16
+ return check_rule(*args, **kwargs)
12
17
 
13
18
 
14
19
  class RuleChain(dict[str, typing.Any], Node):
@@ -36,18 +41,6 @@ class RuleChain(dict[str, typing.Any], Node):
36
41
 
37
42
  @classmethod
38
43
  async def compose(cls, update: UpdateCute) -> typing.Any:
39
- # Hack to avoid circular import
40
- globalns = globals()
41
- if "check_rule" not in globalns:
42
- globalns.update(
43
- {
44
- "check_rule": getattr(
45
- importlib.import_module("telegrinder.bot.dispatch.process"),
46
- "check_rule",
47
- ),
48
- },
49
- )
50
-
51
44
  ctx = Context()
52
45
  for rule in cls.rules:
53
46
  if not await check_rule(update.api, rule, update, ctx):
telegrinder/node/scope.py CHANGED
@@ -1,8 +1,12 @@
1
+ from __future__ import annotations
2
+
1
3
  import enum
2
4
  import typing
3
5
 
4
6
  if typing.TYPE_CHECKING:
5
- from .base import IsNode
7
+ from telegrinder.node.base import Composable
8
+
9
+ NODE_SCOPE_KEY: typing.Final[str] = "scope"
6
10
 
7
11
 
8
12
  class NodeScope(enum.Enum):
@@ -16,26 +20,31 @@ PER_CALL = NodeScope.PER_CALL
16
20
  GLOBAL = NodeScope.GLOBAL
17
21
 
18
22
 
19
- def per_call[T: IsNode](node: type[T]) -> type[T]:
23
+ def per_call[T: Composable[typing.Any]](node: type[T]) -> type[T]:
20
24
  setattr(node, "scope", PER_CALL)
21
25
  return node
22
26
 
23
27
 
24
- def per_event[T: IsNode](node: type[T]) -> type[T]:
28
+ def per_event[T: Composable[typing.Any]](node: type[T]) -> type[T]:
25
29
  setattr(node, "scope", PER_EVENT)
26
30
  return node
27
31
 
28
32
 
29
- def global_node[T: IsNode](node: type[T]) -> type[T]:
33
+ def global_node[T: Composable[typing.Any]](node: type[T]) -> type[T]:
30
34
  setattr(node, "scope", GLOBAL)
31
35
  return node
32
36
 
33
37
 
38
+ def get_scope(node: Composable[typing.Any], /) -> NodeScope:
39
+ return getattr(node, NODE_SCOPE_KEY, PER_EVENT)
40
+
41
+
34
42
  __all__ = (
35
43
  "GLOBAL",
36
- "NodeScope",
37
44
  "PER_CALL",
38
45
  "PER_EVENT",
46
+ "NodeScope",
47
+ "get_scope",
39
48
  "global_node",
40
49
  "per_call",
41
50
  "per_event",
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import typing
5
+
6
+ from telegrinder.modules import logger
7
+ from telegrinder.node.exceptions import ComposeError
8
+ from telegrinder.node.scope import NodeScope, get_scope
9
+ from telegrinder.tools.aio import Generator, stop_generator
10
+ from telegrinder.tools.fullname import fullname
11
+
12
+ if typing.TYPE_CHECKING:
13
+ from telegrinder.node.base import IsNode
14
+
15
+
16
+ @dataclasses.dataclass(slots=True, repr=False)
17
+ class NodeSession:
18
+ node: IsNode
19
+ value: typing.Any
20
+ generator: Generator[typing.Any, typing.Any, typing.Any] | None = None
21
+
22
+ def __repr__(self) -> str:
23
+ return f"<{type(self).__name__}: {self.value!r}" + (" (ACTIVE)>" if self.generator else ">")
24
+
25
+ async def close(
26
+ self,
27
+ with_value: typing.Any | None = None,
28
+ scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
29
+ ) -> typing.Any | None:
30
+ result = None
31
+
32
+ if self.generator is not None and get_scope(self.node) in scopes:
33
+ try:
34
+ await stop_generator(self.generator, with_value)
35
+ except ComposeError as compose_error:
36
+ logger.debug(
37
+ "Caught compose error when closing session for node `{}`: {}",
38
+ fullname(self.node),
39
+ compose_error.message,
40
+ )
41
+ except BaseException as exception:
42
+ logger.debug(
43
+ "Uncaught {!r} was occurred when closing session for node `{}`",
44
+ exception,
45
+ fullname(self.node),
46
+ )
47
+ finally:
48
+ self.generator = None
49
+
50
+ return result
51
+
52
+
53
+ __all__ = ("NodeSession",)
@@ -4,7 +4,13 @@ import typing
4
4
  from fntypes.option import Nothing, Option, Some
5
5
 
6
6
  from telegrinder.api.api import API
7
- from telegrinder.bot.cute_types import CallbackQueryCute, ChatJoinRequestCute, MessageCute, PreCheckoutQueryCute
7
+ from telegrinder.bot.cute_types import (
8
+ CallbackQueryCute,
9
+ ChatJoinRequestCute,
10
+ ChatMemberUpdatedCute,
11
+ MessageCute,
12
+ PreCheckoutQueryCute,
13
+ )
8
14
  from telegrinder.node.base import ComposeError, DataNode, scalar_node
9
15
  from telegrinder.node.polymorphic import Polymorphic, impl
10
16
  from telegrinder.types.objects import Chat, Message, User
@@ -20,7 +26,7 @@ class Source(Polymorphic, DataNode):
20
26
  @impl
21
27
  def compose_message(cls, message: MessageCute) -> typing.Self:
22
28
  return cls(
23
- api=message.ctx_api,
29
+ api=message.api,
24
30
  from_user=message.from_user,
25
31
  chat=Some(message.chat),
26
32
  thread_id=message.message_thread_id,
@@ -29,28 +35,33 @@ class Source(Polymorphic, DataNode):
29
35
  @impl
30
36
  def compose_callback_query(cls, callback_query: CallbackQueryCute) -> typing.Self:
31
37
  return cls(
32
- api=callback_query.ctx_api,
38
+ api=callback_query.api,
33
39
  from_user=callback_query.from_user,
34
40
  chat=callback_query.chat,
35
41
  thread_id=callback_query.message_thread_id,
36
42
  )
37
43
 
44
+ @impl
45
+ def compose_chat_member_updated(cls, chat_member_updated: ChatMemberUpdatedCute) -> typing.Self:
46
+ return cls(
47
+ api=chat_member_updated.api,
48
+ from_user=chat_member_updated.from_user,
49
+ chat=Some(chat_member_updated.chat),
50
+ )
51
+
38
52
  @impl
39
53
  def compose_chat_join_request(cls, chat_join_request: ChatJoinRequestCute) -> typing.Self:
40
54
  return cls(
41
- api=chat_join_request.ctx_api,
55
+ api=chat_join_request.api,
42
56
  from_user=chat_join_request.from_user,
43
57
  chat=Some(chat_join_request.chat),
44
- thread_id=Nothing(),
45
58
  )
46
59
 
47
60
  @impl
48
61
  def compose_pre_checkout_query(cls, pre_checkout_query: PreCheckoutQueryCute) -> typing.Self:
49
62
  return cls(
50
- api=pre_checkout_query.ctx_api,
63
+ api=pre_checkout_query.api,
51
64
  from_user=pre_checkout_query.from_user,
52
- chat=Nothing(),
53
- thread_id=Nothing(),
54
65
  )
55
66
 
56
67
  async def send(self, text: str, **kwargs: typing.Any) -> Message:
@@ -77,6 +88,13 @@ class UserSource:
77
88
  return source.from_user
78
89
 
79
90
 
91
+ @scalar_node
92
+ class ChatId:
93
+ @classmethod
94
+ def compose(cls, chat: ChatSource) -> int:
95
+ return chat.id
96
+
97
+
80
98
  @scalar_node
81
99
  class UserId:
82
100
  @classmethod
@@ -84,4 +102,18 @@ class UserId:
84
102
  return user.id
85
103
 
86
104
 
87
- __all__ = ("ChatSource", "Source", "UserId", "UserSource")
105
+ @scalar_node
106
+ class Locale:
107
+ @classmethod
108
+ def compose(cls, user: UserSource) -> str:
109
+ return user.language_code.expect(ComposeError("User has no language code."))
110
+
111
+
112
+ __all__ = (
113
+ "ChatId",
114
+ "ChatSource",
115
+ "Locale",
116
+ "Source",
117
+ "UserId",
118
+ "UserSource",
119
+ )
telegrinder/node/text.py CHANGED
@@ -2,7 +2,6 @@ import typing
2
2
 
3
3
  from telegrinder.bot.cute_types.message import MessageCute
4
4
  from telegrinder.node.base import ComposeError, FactoryNode, scalar_node
5
- from telegrinder.node.either import Either
6
5
 
7
6
 
8
7
  @scalar_node
@@ -26,7 +25,7 @@ class Text:
26
25
  @scalar_node
27
26
  class TextInteger:
28
27
  @classmethod
29
- def compose(cls, text: Either[Text, Caption]) -> int:
28
+ def compose(cls, text: Text | Caption) -> int:
30
29
  if not text.isdigit():
31
30
  raise ComposeError("Text is not digit.")
32
31
  return int(text)
File without changes
@@ -1,8 +1,8 @@
1
- import inspect
2
1
  import typing
3
2
 
4
3
  from telegrinder.node.base import ComposeError, IsNode, Node
5
4
  from telegrinder.node.container import ContainerNode
5
+ from telegrinder.tools.aio import maybe_awaitable
6
6
 
7
7
 
8
8
  def cast_false_to_none[Value](value: Value) -> Value | None:
@@ -22,10 +22,8 @@ def generate_node(
22
22
  func: typing.Callable[..., typing.Any],
23
23
  casts: tuple[typing.Callable[[typing.Any], typing.Any], ...] = (cast_false_to_none, error_on_none),
24
24
  ) -> type[Node]:
25
- async def compose(cls, *args: typing.Any) -> typing.Any:
26
- result = func(*args)
27
- if inspect.isawaitable(result):
28
- result = await result
25
+ async def compose(_, *args: typing.Any) -> typing.Any:
26
+ result = await maybe_awaitable(func(*args))
29
27
  for cast in casts:
30
28
  result = cast(result)
31
29
  return result
@@ -0,0 +1,16 @@
1
+ import typing
2
+
3
+ from telegrinder.node.base import ComposeError, NodeClass, scalar_node
4
+ from telegrinder.node.scope import per_call
5
+ from telegrinder.tools.magic.annotations import TypeParameter, get_generic_parameters
6
+
7
+
8
+ @scalar_node
9
+ @per_call
10
+ class TypeArgs:
11
+ @classmethod
12
+ def compose(cls, node_cls: NodeClass) -> dict[TypeParameter, typing.Any]:
13
+ return get_generic_parameters(node_cls).expect(ComposeError("No generic alias."))
14
+
15
+
16
+ __all__ = ("TypeArgs",)
telegrinder/py.typed CHANGED
File without changes
telegrinder/rules.py CHANGED
File without changes