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,8 +1,5 @@
1
- import typing
2
-
3
1
  from fntypes.co import Nothing, Some
4
2
 
5
- from telegrinder.api.api import API
6
3
  from telegrinder.bot.cute_types.base import BaseCute
7
4
  from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
8
5
  from telegrinder.bot.cute_types.chat_join_request import ChatJoinRequestCute
@@ -14,12 +11,8 @@ from telegrinder.model import UNSET, From, field
14
11
  from telegrinder.msgspec_utils import Option
15
12
  from telegrinder.types.objects import *
16
13
 
17
- EventModel = typing.TypeVar("EventModel", bound=Model)
18
-
19
14
 
20
15
  class UpdateCute(BaseCute[Update], Update, kw_only=True):
21
- api: API
22
-
23
16
  message: Option[MessageCute] = field(
24
17
  default=UNSET,
25
18
  converter=From[MessageCute | None],
@@ -103,7 +96,7 @@ class UpdateCute(BaseCute[Update], Update, kw_only=True):
103
96
  """Optional. New incoming pre-checkout query. Contains full information
104
97
  about checkout."""
105
98
 
106
- def get_event(self, event_model: type[EventModel]) -> Option[EventModel]:
99
+ def get_event[T: Model](self, event_model: type[T], /) -> Option[T]:
107
100
  if isinstance(self.incoming_update, event_model):
108
101
  return Some(self.incoming_update)
109
102
  return Nothing()
@@ -1,6 +1,6 @@
1
1
  import typing
2
2
 
3
- from telegrinder.model import get_params
3
+ from telegrinder.types.methods_utils import get_params
4
4
  from telegrinder.types.objects import (
5
5
  InputFile,
6
6
  InputMediaAnimation,
@@ -1,4 +1,5 @@
1
1
  from telegrinder.bot.dispatch.abc import ABCDispatch
2
+ from telegrinder.bot.dispatch.action import action
2
3
  from telegrinder.bot.dispatch.context import Context
3
4
  from telegrinder.bot.dispatch.dispatch import Dispatch, TelegrinderContext
4
5
  from telegrinder.bot.dispatch.handler import (
@@ -25,13 +26,12 @@ from telegrinder.bot.dispatch.return_manager import (
25
26
  register_manager,
26
27
  )
27
28
  from telegrinder.bot.dispatch.view import (
28
- ABCStateView,
29
29
  ABCView,
30
- BaseStateView,
31
30
  BaseView,
32
31
  CallbackQueryView,
33
32
  ChatJoinRequestView,
34
33
  ChatMemberView,
34
+ ErrorView,
35
35
  InlineQueryView,
36
36
  MessageView,
37
37
  PreCheckoutQueryView,
@@ -47,25 +47,24 @@ from telegrinder.bot.dispatch.waiter_machine import (
47
47
  MESSAGE_IN_CHAT,
48
48
  Hasher,
49
49
  ShortState,
50
- StateViewHasher,
51
50
  WaiterMachine,
52
- clear_wm_storage_worker,
53
51
  )
54
52
 
55
53
  __all__ = (
54
+ "CALLBACK_QUERY_FOR_MESSAGE",
55
+ "CALLBACK_QUERY_FROM_CHAT",
56
+ "CALLBACK_QUERY_IN_CHAT_FOR_MESSAGE",
57
+ "MESSAGE_FROM_USER",
58
+ "MESSAGE_FROM_USER_IN_CHAT",
59
+ "MESSAGE_IN_CHAT",
56
60
  "ABCDispatch",
57
61
  "ABCHandler",
58
62
  "ABCMiddleware",
59
63
  "ABCReturnManager",
60
- "ABCStateView",
61
64
  "ABCView",
62
65
  "AudioReplyHandler",
63
66
  "BaseReturnManager",
64
- "BaseStateView",
65
67
  "BaseView",
66
- "CALLBACK_QUERY_FOR_MESSAGE",
67
- "CALLBACK_QUERY_FROM_CHAT",
68
- "CALLBACK_QUERY_IN_CHAT_FOR_MESSAGE",
69
68
  "CallbackQueryReturnManager",
70
69
  "CallbackQueryView",
71
70
  "ChatJoinRequestView",
@@ -73,13 +72,11 @@ __all__ = (
73
72
  "Context",
74
73
  "Dispatch",
75
74
  "DocumentReplyHandler",
75
+ "ErrorView",
76
76
  "FuncHandler",
77
77
  "Hasher",
78
78
  "InlineQueryReturnManager",
79
79
  "InlineQueryView",
80
- "MESSAGE_FROM_USER",
81
- "MESSAGE_FROM_USER_IN_CHAT",
82
- "MESSAGE_IN_CHAT",
83
80
  "Manager",
84
81
  "MediaGroupReplyHandler",
85
82
  "MessageReplyHandler",
@@ -90,15 +87,13 @@ __all__ = (
90
87
  "PreCheckoutQueryView",
91
88
  "RawEventView",
92
89
  "ShortState",
93
- "StateViewHasher",
94
90
  "StickerReplyHandler",
95
91
  "TelegrinderContext",
96
92
  "VideoReplyHandler",
97
93
  "ViewBox",
98
94
  "WaiterMachine",
95
+ "action",
99
96
  "check_rule",
100
- "clear_wm_storage_worker",
101
- "clear_wm_storage_worker",
102
97
  "process_inner",
103
98
  "register_manager",
104
99
  )
@@ -10,9 +10,6 @@ from fntypes.option import Option
10
10
  from telegrinder.api.api import API
11
11
  from telegrinder.types.objects import Update
12
12
 
13
- if typing.TYPE_CHECKING:
14
- from telegrinder.bot.dispatch.view.abc import ABCView
15
-
16
13
 
17
14
  class PathExistsError(BaseException):
18
15
  pass
@@ -20,7 +17,7 @@ class PathExistsError(BaseException):
20
17
 
21
18
  class ABCDispatch(ABC):
22
19
  @abstractmethod
23
- async def feed(self, event: Update, api: API[typing.Any]) -> bool:
20
+ async def feed(self, event: Update, api: API) -> bool:
24
21
  pass
25
22
 
26
23
  @abstractmethod
@@ -31,26 +28,30 @@ class ABCDispatch(ABC):
31
28
  def get_view[T](self, of_type: type[T]) -> Option[T]:
32
29
  pass
33
30
 
34
- @abstractmethod
35
- def get_views(self) -> dict[str, "ABCView"]:
36
- pass
37
-
38
31
  def load_many(self, *externals: typing.Self) -> None:
39
32
  for external in externals:
40
33
  self.load(external)
41
34
 
42
- def load_from_dir(self, directory: str | pathlib.Path) -> bool:
35
+ def load_from_dir(
36
+ self,
37
+ directory: str | pathlib.Path,
38
+ *,
39
+ recursive: bool = False,
40
+ ) -> bool:
43
41
  """Loads dispatchers from a directory containing Python modules where global variables
44
42
  are declared with instances of dispatch.
45
43
  Returns True if dispatchers were found, otherwise False.
46
44
  """
45
+
47
46
  directory = pathlib.Path(directory)
48
47
 
49
48
  if not directory.exists():
50
49
  raise PathExistsError(f"Path {str(directory)!r} does not exists.")
51
50
 
52
51
  dps: list[typing.Self] = []
53
- for root, _, files in os.walk(directory):
52
+ files_iter = os.walk(directory) if recursive else [(directory, [], os.listdir(directory))]
53
+
54
+ for root, _, files in files_iter:
54
55
  for f in files:
55
56
  if f.endswith(".py") and f != "__init__.py":
56
57
  module_path = os.path.join(root, f)
@@ -66,7 +67,7 @@ class ABCDispatch(ABC):
66
67
  spec.loader.exec_module(module)
67
68
 
68
69
  for obj in module.__dict__.values():
69
- if isinstance(obj, self.__class__):
70
+ if isinstance(obj, type(self)):
70
71
  dps.append(obj)
71
72
 
72
73
  self.load_many(*dps)
@@ -0,0 +1,104 @@
1
+ import inspect
2
+ import typing
3
+
4
+ from fntypes.co import Error, Ok, Result, unwrapping
5
+
6
+ if typing.TYPE_CHECKING:
7
+ from telegrinder.bot.rules.abc import ABCRule
8
+
9
+ from telegrinder.api.api import API
10
+ from telegrinder.bot.dispatch.context import Context
11
+ from telegrinder.bot.dispatch.handler.func import FuncHandler
12
+ from telegrinder.bot.dispatch.process import check_rule
13
+ from telegrinder.modules import logger
14
+ from telegrinder.node.base import get_nodes
15
+ from telegrinder.node.composer import compose_nodes
16
+ from telegrinder.tools.aio import maybe_awaitable, next_generator, stop_generator
17
+ from telegrinder.tools.magic.function import bundle
18
+ from telegrinder.types.objects import Update
19
+
20
+ type When = ABCRule
21
+ type Handler = typing.Callable[..., typing.Any]
22
+ type ActionFunction = typing.Callable[..., ActionFunctionResult]
23
+ type ActionFunctionResult = typing.Union[
24
+ typing.AsyncGenerator[typing.Any, typing.Any],
25
+ typing.Awaitable[typing.Any],
26
+ typing.Generator[typing.Any, typing.Any, typing.Any],
27
+ typing.Any,
28
+ ]
29
+
30
+
31
+ @unwrapping
32
+ async def run_action_function[T: Handler](
33
+ func_handler: FuncHandler[T],
34
+ function: ActionFunction,
35
+ api: API,
36
+ update: Update,
37
+ context: Context,
38
+ ) -> Result[typing.Any, str]:
39
+ data = {API: api, Update: update}
40
+ node_col = (
41
+ (
42
+ await compose_nodes(
43
+ nodes=get_nodes(function),
44
+ ctx=context,
45
+ data=data,
46
+ )
47
+ )
48
+ .map_err(lambda error: error.message)
49
+ .unwrap()
50
+ )
51
+
52
+ temp_ctx = context.copy()
53
+ bundle_function = bundle(function, {Context: temp_ctx, **data}, start_idx=0, typebundle=True)
54
+ bundle_function &= bundle(
55
+ function,
56
+ context | ({} if node_col is None else node_col.values),
57
+ start_idx=0,
58
+ )
59
+ result = bundle_function()
60
+
61
+ try:
62
+ if inspect.isasyncgen(result) or inspect.isgenerator(result):
63
+ value = await next_generator(result)
64
+ handler_result = await func_handler.run(api, update, context)
65
+ await stop_generator(result, value)
66
+ return handler_result
67
+
68
+ await maybe_awaitable(result)
69
+ return await func_handler.run(api, update, context)
70
+ finally:
71
+ context |= temp_ctx
72
+ await node_col.close_all()
73
+
74
+
75
+ def action[T: Handler](
76
+ function: ActionFunction,
77
+ *,
78
+ when: When | None = None,
79
+ ) -> typing.Callable[[T], T]:
80
+ def decorator(handler: T, /) -> T:
81
+ func_handler = FuncHandler(function=handler)
82
+
83
+ async def action_wrapper(api: API, update: Update, context: Context) -> typing.Any:
84
+ if when and not await check_rule(api, when, update, context):
85
+ logger.debug("When action rule `{!r}` failed.", when)
86
+ result = await func_handler.run(api, update, context)
87
+ else:
88
+ result = await run_action_function(func_handler, function, api, update, context)
89
+
90
+ match result:
91
+ case Ok(value):
92
+ return value
93
+ case Error(error):
94
+ logger.debug(error)
95
+ return None
96
+
97
+ action_wrapper.__name__ = f"<action for {handler.__name__}>"
98
+ action_wrapper.__qualname__ = f"<action for {handler.__qualname__}>"
99
+ return action_wrapper # type: ignore
100
+
101
+ return decorator
102
+
103
+
104
+ __all__ = ("action",)
@@ -4,45 +4,30 @@ import enum
4
4
  import typing
5
5
  from reprlib import recursive_repr
6
6
 
7
- from telegrinder.types.objects import Update
7
+ from fntypes import Some
8
+ from fntypes.option import Nothing, Option
8
9
 
9
10
  if typing.TYPE_CHECKING:
10
- from telegrinder.node.composer import NodeCollection
11
+ from telegrinder.api.api import API
12
+ from telegrinder.bot.cute_types.update import UpdateCute
13
+ from telegrinder.types.objects import Update
11
14
 
12
15
  type Key = str | enum.Enum
13
16
  type AnyValue = typing.Any
14
17
 
15
18
 
16
19
  class Context(dict[str, AnyValue]):
17
- """Context class like dict & dotdict.
20
+ """Low level per event context storage."""
18
21
 
19
- For example:
20
- ```python
21
- class MyRule(ABCRule[T]):
22
- async def check(self, event: T, ctx: Context) -> bool:
23
- ctx.me = (await event.ctx_api.get_me()).unwrap()
24
- ctx["items"] = [1, 2, 3]
25
- return True
26
- ```
27
- """
28
-
29
- raw_update: Update
30
- node_col: NodeCollection | None = None
22
+ update_cute: Option[UpdateCute] = Nothing()
23
+ exception_update: Option[BaseException] = Nothing()
31
24
 
32
25
  def __init__(self, **kwargs: AnyValue) -> None:
33
- cls_vars = vars(self.__class__)
34
- defaults = {}
35
-
36
- for k in self.__class__.__annotations__:
37
- if k in cls_vars:
38
- defaults[k] = cls_vars[k]
39
- delattr(self.__class__, k)
40
-
41
- dict.__init__(self, **defaults | kwargs)
26
+ dict.__init__(self, **kwargs)
42
27
 
43
28
  @recursive_repr()
44
29
  def __repr__(self) -> str:
45
- return "{}({})".format(self.__class__.__name__, ", ".join(f"{k}={v!r}" for k, v in self.items()))
30
+ return "{}({})".format(type(self).__name__, ", ".join(f"{k}={v!r}" for k, v in self.items()))
46
31
 
47
32
  def __setitem__(self, __key: Key, __value: AnyValue) -> None:
48
33
  dict.__setitem__(self, self.key_to_str(__key), __value)
@@ -56,12 +41,30 @@ class Context(dict[str, AnyValue]):
56
41
  def __setattr__(self, __name: str, __value: AnyValue) -> None:
57
42
  self.__setitem__(__name, __value)
58
43
 
59
- def __getattr__(self, __name: str) -> AnyValue:
44
+ def __getattribute__(self, __name: str) -> AnyValue:
45
+ cls = type(self)
46
+
47
+ if __name in cls.__annotations__:
48
+ return self[__name] if __name in self else super().__getattribute__(__name)
49
+
50
+ if __name in _CONTEXT_CLASS_ATTRS:
51
+ return super().__getattribute__(__name)
52
+
60
53
  return self.__getitem__(__name)
61
54
 
62
55
  def __delattr__(self, __name: str) -> None:
63
56
  self.__delitem__(__name)
64
57
 
58
+ def add_update_cute(self, update: Update, bound_api: API, /) -> typing.Self:
59
+ from telegrinder.bot.cute_types.update import UpdateCute
60
+
61
+ self.update_cute = Some(UpdateCute.from_update(update, bound_api))
62
+ return self
63
+
64
+ def add_exception_update(self, exception_update: BaseException, /) -> typing.Self:
65
+ self.exception_update = Some(exception_update)
66
+ return self
67
+
65
68
  @staticmethod
66
69
  def key_to_str(key: Key) -> str:
67
70
  return key if isinstance(key, str) else str(key.value)
@@ -93,4 +96,7 @@ class Context(dict[str, AnyValue]):
93
96
  del self[key]
94
97
 
95
98
 
99
+ _CONTEXT_CLASS_ATTRS = frozenset(Context.__dict__ | dict.__dict__ | object.__dict__)
100
+
101
+
96
102
  __all__ = ("Context",)
@@ -3,13 +3,13 @@ from __future__ import annotations
3
3
  import dataclasses
4
4
 
5
5
  import typing_extensions as typing
6
- from fntypes import Nothing, Option, Some
7
- from vbml.patcher import Patcher
6
+ from fntypes.option import Nothing, Option, Some
7
+ from vbml.patcher.abc import ABCPatcher
8
8
 
9
- from telegrinder.api.api import API, HTTPClient
9
+ from telegrinder.api.api import API
10
10
  from telegrinder.bot.dispatch.abc import ABCDispatch
11
11
  from telegrinder.bot.dispatch.context import Context
12
- from telegrinder.bot.dispatch.handler.func import ErrorHandlerT, Func, FuncHandler
12
+ from telegrinder.bot.dispatch.handler.func import FuncHandler, Function
13
13
  from telegrinder.bot.dispatch.middleware.abc import run_middleware
14
14
  from telegrinder.bot.dispatch.middleware.global_middleware import GlobalMiddleware
15
15
  from telegrinder.bot.dispatch.view.abc import ABCView
@@ -17,6 +17,7 @@ from telegrinder.bot.dispatch.view.box import (
17
17
  CallbackQueryView,
18
18
  ChatJoinRequestView,
19
19
  ChatMemberView,
20
+ ErrorView,
20
21
  InlineQueryView,
21
22
  MessageView,
22
23
  PreCheckoutQueryView,
@@ -24,23 +25,20 @@ from telegrinder.bot.dispatch.view.box import (
24
25
  ViewBox,
25
26
  )
26
27
  from telegrinder.modules import logger
27
- from telegrinder.tools.error_handler.error_handler import ErrorHandler
28
+ from telegrinder.node.composer import CONTEXT_STORE_NODES_KEY, NodeScope
28
29
  from telegrinder.tools.global_context import TelegrinderContext
29
- from telegrinder.types.enums import UpdateType
30
30
  from telegrinder.types.objects import Update
31
31
 
32
32
  if typing.TYPE_CHECKING:
33
33
  from telegrinder.bot.cute_types.base import BaseCute
34
- from telegrinder.bot.cute_types.update import UpdateCute
35
34
  from telegrinder.bot.rules.abc import ABCRule
35
+ from telegrinder.node.composer import Composer
36
36
 
37
37
  T = typing.TypeVar("T", default=typing.Any)
38
38
  R = typing.TypeVar("R", covariant=True, default=typing.Any)
39
39
  Event = typing.TypeVar("Event", bound="BaseCute")
40
40
  P = typing.ParamSpec("P", default=...)
41
41
 
42
- DEFAULT_DATACLASS: typing.Final[type[Update]] = Update
43
-
44
42
 
45
43
  @dataclasses.dataclass(repr=False, kw_only=True)
46
44
  class Dispatch(
@@ -53,9 +51,9 @@ class Dispatch(
53
51
  MessageView,
54
52
  PreCheckoutQueryView,
55
53
  RawEventView,
54
+ ErrorView,
56
55
  ],
57
56
  typing.Generic[
58
- HTTPClient,
59
57
  CallbackQueryView,
60
58
  ChatJoinRequestView,
61
59
  ChatMemberView,
@@ -63,9 +61,10 @@ class Dispatch(
63
61
  MessageView,
64
62
  PreCheckoutQueryView,
65
63
  RawEventView,
64
+ ErrorView,
66
65
  ],
67
66
  ):
68
- _global_context: TelegrinderContext = dataclasses.field(
67
+ global_context: TelegrinderContext = dataclasses.field(
69
68
  init=False,
70
69
  default_factory=TelegrinderContext,
71
70
  )
@@ -77,156 +76,82 @@ class Dispatch(
77
76
  return "Dispatch(%s)" % ", ".join(f"{k}={v!r}" for k, v in self.get_views().items())
78
77
 
79
78
  @property
80
- def global_context(self) -> TelegrinderContext:
81
- return self._global_context
82
-
83
- @property
84
- def patcher(self) -> Patcher:
79
+ def patcher(self) -> ABCPatcher:
85
80
  """Alias `patcher` to get `vbml.Patcher` from the global context."""
86
81
  return self.global_context.vbml_patcher
87
82
 
88
- @typing.overload
89
- def handle(
90
- self,
91
- *rules: "ABCRule",
92
- final: bool = True,
93
- ) -> typing.Callable[[Func[P, R]], FuncHandler["UpdateCute", Func[P, R], ErrorHandler[UpdateCute]]]: ...
94
-
95
- @typing.overload
96
- def handle(
97
- self,
98
- *rules: "ABCRule",
99
- dataclass: type[T],
100
- final: bool = True,
101
- ) -> typing.Callable[[Func[P, R]], FuncHandler["UpdateCute", Func[P, R], ErrorHandler[T]]]: ...
102
-
103
- @typing.overload
104
- def handle(
105
- self,
106
- *rules: "ABCRule",
107
- update_type: UpdateType,
108
- final: bool = True,
109
- ) -> typing.Callable[[Func[P, R]], FuncHandler["UpdateCute", Func[P, R], ErrorHandler[UpdateCute]]]: ...
110
-
111
- @typing.overload
112
- def handle(
113
- self,
114
- *rules: "ABCRule",
115
- dataclass: type[T],
116
- update_type: UpdateType,
117
- final: bool = True,
118
- ) -> typing.Callable[[Func[P, R]], FuncHandler["UpdateCute", Func[P, R], ErrorHandler[T]]]: ...
119
-
120
- @typing.overload
121
- def handle(
122
- self,
123
- *rules: "ABCRule",
124
- error_handler: ErrorHandlerT,
125
- final: bool = True,
126
- ) -> typing.Callable[[Func[P, R]], FuncHandler["UpdateCute", Func[P, R], ErrorHandlerT]]: ...
127
-
128
- @typing.overload
129
- def handle(
130
- self,
131
- *rules: "ABCRule",
132
- update_type: UpdateType,
133
- error_handler: ErrorHandlerT,
134
- final: bool = True,
135
- ) -> typing.Callable[[Func[P, R]], FuncHandler["UpdateCute", Func[P, R], ErrorHandlerT]]: ...
136
-
137
- @typing.overload
138
- def handle(
139
- self,
140
- *rules: "ABCRule",
141
- dataclass: type[T],
142
- error_handler: ErrorHandlerT,
143
- final: bool = True,
144
- ) -> typing.Callable[[Func[P, R]], FuncHandler[T, Func[P, R], ErrorHandlerT]]: ...
145
-
146
- @typing.overload
147
- def handle(
148
- self,
149
- *rules: "ABCRule",
150
- dataclass: type[T],
151
- update_type: UpdateType,
152
- error_handler: ErrorHandlerT,
153
- final: bool = True,
154
- ) -> typing.Callable[[Func[P, R]], FuncHandler[T, Func[P, R], ErrorHandlerT]]: ...
155
-
156
- @typing.overload
157
- def handle(
158
- self,
159
- *rules: "ABCRule",
160
- update_type: UpdateType | None = None,
161
- dataclass: type[T] = DEFAULT_DATACLASS,
162
- error_handler: typing.Literal[None] = None,
163
- final: bool = True,
164
- ) -> typing.Callable[[Func[P, R]], FuncHandler[T, Func[P, R], ErrorHandler[T]]]: ...
165
-
166
- def handle(
167
- self,
168
- *rules: "ABCRule",
169
- update_type: UpdateType | None = None,
170
- dataclass: type[typing.Any] = DEFAULT_DATACLASS,
171
- error_handler: ErrorHandlerT | None = None,
172
- final: bool = True,
173
- ) -> typing.Callable[..., typing.Any]:
174
- def wrapper(func):
175
- handler = FuncHandler(
176
- func,
177
- list(rules),
178
- final=final,
179
- dataclass=dataclass,
180
- error_handler=error_handler or ErrorHandler(),
181
- update_type=update_type,
83
+ @property
84
+ def composer(self) -> Composer:
85
+ """Alias `composer` to get `telegrinder.node.composer.Composer` from the global context."""
86
+ return self.global_context.composer.unwrap()
87
+
88
+ def handle[T: Function](self, *rules: ABCRule, final: bool = True) -> typing.Callable[[T], T]:
89
+ def wrapper(func: T, /) -> T:
90
+ self.raw_event.handlers.append(
91
+ FuncHandler(
92
+ function=func,
93
+ rules=list(rules),
94
+ final=final,
95
+ ),
182
96
  )
183
- self.raw_event.handlers.append(handler)
184
- return handler
97
+ return func
185
98
 
186
99
  return wrapper
187
100
 
188
- async def feed(self, event: Update, api: API[HTTPClient]) -> bool:
189
- logger.debug(
190
- "Processing update (update_id={}, update_type={!r})",
191
- event.update_id,
192
- event.update_type.name,
193
- )
194
- context = Context(raw_update=event)
101
+ async def feed(self, event: Update, api: API) -> bool:
102
+ logger.info("New Update(id={}, type={!r})", event.update_id, event.update_type)
103
+ processed = False
104
+ context = Context().add_update_cute(event, api)
105
+ start_time = self.global_context.loop_wrapper.loop.time()
195
106
 
196
107
  if (
197
108
  await run_middleware(
198
109
  self.global_middleware.pre,
199
110
  api,
200
- event, # type: ignore
201
- raw_event=event,
202
- ctx=context,
203
- adapter=self.global_middleware.adapter,
111
+ event,
112
+ context,
113
+ required_nodes=self.global_middleware.pre_required_nodes,
204
114
  )
205
115
  is False
206
116
  ):
207
- return False
117
+ return processed
208
118
 
209
119
  for view in self.get_views().values():
210
120
  if await view.check(event):
211
121
  logger.debug(
212
- "Update (update_id={}, update_type={!r}) matched view {!r}.",
122
+ "Processing update (id={}, type={!r}) with view {!r} by bot (id={})",
213
123
  event.update_id,
214
- event.update_type.name,
124
+ event.update_type,
215
125
  view,
126
+ api.id,
216
127
  )
217
- if await view.process(event, api, context):
218
- return True
128
+
129
+ try:
130
+ if await view.process(event, api, context):
131
+ processed = True
132
+ break
133
+ except BaseException as exception:
134
+ if not await self.error.process(event, api, context.add_exception_update(exception)):
135
+ raise exception
136
+ finally:
137
+ for session in context.get(CONTEXT_STORE_NODES_KEY, {}).values():
138
+ await session.close(scopes=(NodeScope.PER_EVENT,))
219
139
 
220
140
  await run_middleware(
221
141
  self.global_middleware.post,
222
142
  api,
223
143
  event,
224
- raw_event=event,
225
- ctx=context,
226
- adapter=self.global_middleware.adapter,
144
+ context,
145
+ required_nodes=self.global_middleware.post_required_nodes,
227
146
  )
228
-
229
- return False
147
+ logger.debug(
148
+ "Update (id={}, type={!r}) processed in {} ms by bot (id={})",
149
+ event.update_id,
150
+ event.update_type,
151
+ int((self.global_context.loop_wrapper.loop.time() - start_time) * 1000),
152
+ api.id,
153
+ )
154
+ return processed
230
155
 
231
156
  def load(self, external: typing.Self) -> None:
232
157
  views_external = external.get_views()
@@ -246,7 +171,9 @@ class Dispatch(
246
171
 
247
172
  def get_views(self) -> dict[str, ABCView]:
248
173
  """Get all views."""
249
- return {name: view for name, view in self.__dict__.items() if isinstance(view, ABCView)}
174
+ return {
175
+ name: view for name, view in self.__dict__.items() if isinstance(view, ABCView) and name != "error"
176
+ }
250
177
 
251
178
  __call__ = handle
252
179
 
@@ -1,5 +1,6 @@
1
1
  from telegrinder.bot.dispatch.handler.abc import ABCHandler
2
2
  from telegrinder.bot.dispatch.handler.audio_reply import AudioReplyHandler
3
+ from telegrinder.bot.dispatch.handler.base import BaseReplyHandler
3
4
  from telegrinder.bot.dispatch.handler.document_reply import DocumentReplyHandler
4
5
  from telegrinder.bot.dispatch.handler.func import FuncHandler
5
6
  from telegrinder.bot.dispatch.handler.media_group_reply import MediaGroupReplyHandler
@@ -11,6 +12,7 @@ from telegrinder.bot.dispatch.handler.video_reply import VideoReplyHandler
11
12
  __all__ = (
12
13
  "ABCHandler",
13
14
  "AudioReplyHandler",
15
+ "BaseReplyHandler",
14
16
  "DocumentReplyHandler",
15
17
  "FuncHandler",
16
18
  "MediaGroupReplyHandler",