telegrinder 0.3.4.post1__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of telegrinder might be problematic. Click here for more details.

Files changed (169) hide show
  1. telegrinder/__init__.py +30 -31
  2. telegrinder/api/__init__.py +2 -1
  3. telegrinder/api/api.py +28 -20
  4. telegrinder/api/error.py +8 -4
  5. telegrinder/api/response.py +2 -2
  6. telegrinder/api/token.py +2 -2
  7. telegrinder/bot/__init__.py +6 -0
  8. telegrinder/bot/bot.py +38 -31
  9. telegrinder/bot/cute_types/__init__.py +2 -0
  10. telegrinder/bot/cute_types/base.py +54 -128
  11. telegrinder/bot/cute_types/callback_query.py +76 -61
  12. telegrinder/bot/cute_types/chat_join_request.py +4 -3
  13. telegrinder/bot/cute_types/chat_member_updated.py +28 -31
  14. telegrinder/bot/cute_types/inline_query.py +5 -4
  15. telegrinder/bot/cute_types/message.py +555 -602
  16. telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
  17. telegrinder/bot/cute_types/update.py +20 -12
  18. telegrinder/bot/cute_types/utils.py +3 -36
  19. telegrinder/bot/dispatch/__init__.py +4 -0
  20. telegrinder/bot/dispatch/abc.py +8 -9
  21. telegrinder/bot/dispatch/context.py +5 -7
  22. telegrinder/bot/dispatch/dispatch.py +85 -33
  23. telegrinder/bot/dispatch/handler/abc.py +5 -6
  24. telegrinder/bot/dispatch/handler/audio_reply.py +2 -2
  25. telegrinder/bot/dispatch/handler/base.py +3 -3
  26. telegrinder/bot/dispatch/handler/document_reply.py +2 -2
  27. telegrinder/bot/dispatch/handler/func.py +36 -42
  28. telegrinder/bot/dispatch/handler/media_group_reply.py +5 -4
  29. telegrinder/bot/dispatch/handler/message_reply.py +2 -2
  30. telegrinder/bot/dispatch/handler/photo_reply.py +2 -2
  31. telegrinder/bot/dispatch/handler/sticker_reply.py +2 -2
  32. telegrinder/bot/dispatch/handler/video_reply.py +2 -2
  33. telegrinder/bot/dispatch/middleware/abc.py +83 -8
  34. telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
  35. telegrinder/bot/dispatch/process.py +44 -50
  36. telegrinder/bot/dispatch/return_manager/__init__.py +2 -0
  37. telegrinder/bot/dispatch/return_manager/abc.py +6 -10
  38. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +20 -0
  39. telegrinder/bot/dispatch/view/__init__.py +2 -0
  40. telegrinder/bot/dispatch/view/abc.py +10 -6
  41. telegrinder/bot/dispatch/view/base.py +81 -50
  42. telegrinder/bot/dispatch/view/box.py +20 -9
  43. telegrinder/bot/dispatch/view/callback_query.py +3 -4
  44. telegrinder/bot/dispatch/view/chat_join_request.py +2 -7
  45. telegrinder/bot/dispatch/view/chat_member.py +3 -5
  46. telegrinder/bot/dispatch/view/inline_query.py +3 -4
  47. telegrinder/bot/dispatch/view/message.py +3 -4
  48. telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
  49. telegrinder/bot/dispatch/view/raw.py +42 -40
  50. telegrinder/bot/dispatch/waiter_machine/actions.py +5 -4
  51. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +0 -0
  52. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +0 -0
  53. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +9 -7
  54. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
  55. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +3 -2
  56. telegrinder/bot/dispatch/waiter_machine/machine.py +113 -34
  57. telegrinder/bot/dispatch/waiter_machine/middleware.py +15 -10
  58. telegrinder/bot/dispatch/waiter_machine/short_state.py +7 -18
  59. telegrinder/bot/polling/polling.py +62 -54
  60. telegrinder/bot/rules/__init__.py +24 -1
  61. telegrinder/bot/rules/abc.py +17 -10
  62. telegrinder/bot/rules/callback_data.py +20 -61
  63. telegrinder/bot/rules/chat_join.py +6 -4
  64. telegrinder/bot/rules/command.py +4 -4
  65. telegrinder/bot/rules/enum_text.py +1 -4
  66. telegrinder/bot/rules/func.py +5 -3
  67. telegrinder/bot/rules/fuzzy.py +1 -1
  68. telegrinder/bot/rules/id.py +24 -0
  69. telegrinder/bot/rules/inline.py +6 -4
  70. telegrinder/bot/rules/integer.py +2 -1
  71. telegrinder/bot/rules/logic.py +18 -0
  72. telegrinder/bot/rules/markup.py +5 -6
  73. telegrinder/bot/rules/message.py +2 -4
  74. telegrinder/bot/rules/message_entities.py +1 -3
  75. telegrinder/bot/rules/node.py +15 -9
  76. telegrinder/bot/rules/payload.py +81 -0
  77. telegrinder/bot/rules/payment_invoice.py +29 -0
  78. telegrinder/bot/rules/regex.py +5 -6
  79. telegrinder/bot/rules/state.py +1 -3
  80. telegrinder/bot/rules/text.py +10 -5
  81. telegrinder/bot/rules/update.py +0 -0
  82. telegrinder/bot/scenario/abc.py +2 -4
  83. telegrinder/bot/scenario/checkbox.py +12 -14
  84. telegrinder/bot/scenario/choice.py +6 -9
  85. telegrinder/client/__init__.py +9 -1
  86. telegrinder/client/abc.py +35 -10
  87. telegrinder/client/aiohttp.py +28 -24
  88. telegrinder/client/form_data.py +31 -0
  89. telegrinder/client/sonic.py +212 -0
  90. telegrinder/model.py +38 -145
  91. telegrinder/modules.py +3 -1
  92. telegrinder/msgspec_utils.py +136 -68
  93. telegrinder/node/__init__.py +74 -13
  94. telegrinder/node/attachment.py +92 -16
  95. telegrinder/node/base.py +196 -68
  96. telegrinder/node/callback_query.py +17 -16
  97. telegrinder/node/command.py +3 -2
  98. telegrinder/node/composer.py +40 -75
  99. telegrinder/node/container.py +13 -7
  100. telegrinder/node/either.py +82 -0
  101. telegrinder/node/event.py +20 -31
  102. telegrinder/node/file.py +51 -0
  103. telegrinder/node/me.py +4 -5
  104. telegrinder/node/payload.py +78 -0
  105. telegrinder/node/polymorphic.py +27 -8
  106. telegrinder/node/rule.py +2 -6
  107. telegrinder/node/scope.py +4 -6
  108. telegrinder/node/source.py +37 -21
  109. telegrinder/node/text.py +20 -8
  110. telegrinder/node/tools/generator.py +7 -11
  111. telegrinder/py.typed +0 -0
  112. telegrinder/rules.py +0 -61
  113. telegrinder/tools/__init__.py +97 -38
  114. telegrinder/tools/adapter/__init__.py +19 -0
  115. telegrinder/tools/adapter/abc.py +49 -0
  116. telegrinder/tools/adapter/dataclass.py +56 -0
  117. telegrinder/{bot/rules → tools}/adapter/event.py +8 -10
  118. telegrinder/{bot/rules → tools}/adapter/node.py +8 -10
  119. telegrinder/{bot/rules → tools}/adapter/raw_event.py +2 -2
  120. telegrinder/{bot/rules → tools}/adapter/raw_update.py +2 -2
  121. telegrinder/tools/buttons.py +52 -26
  122. telegrinder/tools/callback_data_serilization/__init__.py +5 -0
  123. telegrinder/tools/callback_data_serilization/abc.py +51 -0
  124. telegrinder/tools/callback_data_serilization/json_ser.py +60 -0
  125. telegrinder/tools/callback_data_serilization/msgpack_ser.py +172 -0
  126. telegrinder/tools/error_handler/abc.py +4 -7
  127. telegrinder/tools/error_handler/error.py +0 -0
  128. telegrinder/tools/error_handler/error_handler.py +34 -48
  129. telegrinder/tools/formatting/__init__.py +57 -37
  130. telegrinder/tools/formatting/deep_links.py +541 -0
  131. telegrinder/tools/formatting/{html.py → html_formatter.py} +51 -79
  132. telegrinder/tools/formatting/spec_html_formats.py +14 -60
  133. telegrinder/tools/functional.py +1 -5
  134. telegrinder/tools/global_context/global_context.py +26 -51
  135. telegrinder/tools/global_context/telegrinder_ctx.py +3 -3
  136. telegrinder/tools/i18n/abc.py +0 -0
  137. telegrinder/tools/i18n/middleware/abc.py +3 -6
  138. telegrinder/tools/input_file_directory.py +30 -0
  139. telegrinder/tools/keyboard.py +9 -9
  140. telegrinder/tools/lifespan.py +105 -0
  141. telegrinder/tools/limited_dict.py +5 -10
  142. telegrinder/tools/loop_wrapper/abc.py +7 -2
  143. telegrinder/tools/loop_wrapper/loop_wrapper.py +40 -95
  144. telegrinder/tools/magic.py +184 -34
  145. telegrinder/tools/state_storage/__init__.py +0 -0
  146. telegrinder/tools/state_storage/abc.py +5 -9
  147. telegrinder/tools/state_storage/memory.py +1 -1
  148. telegrinder/tools/strings.py +13 -0
  149. telegrinder/types/__init__.py +8 -0
  150. telegrinder/types/enums.py +31 -21
  151. telegrinder/types/input_file.py +51 -0
  152. telegrinder/types/methods.py +531 -109
  153. telegrinder/types/objects.py +934 -826
  154. telegrinder/verification_utils.py +0 -2
  155. {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/LICENSE +2 -2
  156. telegrinder-0.4.0.dist-info/METADATA +144 -0
  157. telegrinder-0.4.0.dist-info/RECORD +182 -0
  158. {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/WHEEL +1 -1
  159. telegrinder/bot/rules/adapter/__init__.py +0 -17
  160. telegrinder/bot/rules/adapter/abc.py +0 -31
  161. telegrinder/node/message.py +0 -14
  162. telegrinder/node/update.py +0 -15
  163. telegrinder/tools/formatting/links.py +0 -38
  164. telegrinder/tools/kb_set/__init__.py +0 -4
  165. telegrinder/tools/kb_set/base.py +0 -15
  166. telegrinder/tools/kb_set/yaml.py +0 -63
  167. telegrinder-0.3.4.post1.dist-info/METADATA +0 -110
  168. telegrinder-0.3.4.post1.dist-info/RECORD +0 -165
  169. /telegrinder/{bot/rules → tools}/adapter/errors.py +0 -0
@@ -4,15 +4,15 @@ from functools import cached_property
4
4
  import typing_extensions as typing
5
5
 
6
6
  from telegrinder.api.api import API
7
- from telegrinder.bot.cute_types import BaseCute, UpdateCute
8
7
  from telegrinder.bot.dispatch.context import Context
9
8
  from telegrinder.bot.dispatch.process import check_rule
10
- from telegrinder.model import Model
11
9
  from telegrinder.modules import logger
12
- from telegrinder.node.base import Node, get_nodes
10
+ from telegrinder.node.base import NodeType, get_nodes
13
11
  from telegrinder.node.composer import NodeCollection, compose_nodes
14
- from telegrinder.node.event import EVENT_NODE_KEY
12
+ from telegrinder.tools.adapter.abc import ABCAdapter
13
+ from telegrinder.tools.adapter.dataclass import DataclassAdapter
15
14
  from telegrinder.tools.error_handler import ABCErrorHandler, ErrorHandler
15
+ from telegrinder.tools.magic import get_annotations, magic_bundle
16
16
  from telegrinder.types.enums import UpdateType
17
17
  from telegrinder.types.objects import Update
18
18
 
@@ -22,21 +22,20 @@ if typing.TYPE_CHECKING:
22
22
  from telegrinder.bot.rules.abc import ABCRule
23
23
  from telegrinder.node.composer import NodeCollection
24
24
 
25
- Rest = typing.ParamSpec("Rest")
26
- Result = typing.TypeVar("Result")
27
25
  Function = typing.TypeVar("Function", bound="Func[..., typing.Any]")
28
- Event = typing.TypeVar("Event", bound=Model)
26
+ Event = typing.TypeVar("Event")
29
27
  ErrorHandlerT = typing.TypeVar("ErrorHandlerT", bound=ABCErrorHandler, default=ErrorHandler)
30
28
 
31
- Func: typing.TypeAlias = typing.Callable[Rest, typing.Coroutine[typing.Any, typing.Any, Result]]
29
+ type Func[**Rest, Result] = typing.Callable[Rest, typing.Coroutine[typing.Any, typing.Any, Result]]
32
30
 
33
31
 
34
32
  @dataclasses.dataclass(repr=False, slots=True)
35
33
  class FuncHandler(ABCHandler[Event], typing.Generic[Event, Function, ErrorHandlerT]):
36
34
  function: Function
37
35
  rules: list["ABCRule"]
38
- is_blocking: bool = dataclasses.field(default=True, kw_only=True)
39
- dataclass: type[typing.Any] | None = dataclasses.field(default=dict, kw_only=True)
36
+ adapter: ABCAdapter[Update, Event] | None = dataclasses.field(default=None, kw_only=True)
37
+ final: bool = dataclasses.field(default=True, kw_only=True)
38
+ dataclass: type[typing.Any] | None = dataclasses.field(default=dict[str, typing.Any], kw_only=True)
40
39
  error_handler: ErrorHandlerT = dataclasses.field(
41
40
  default_factory=lambda: typing.cast(ErrorHandlerT, ErrorHandler()),
42
41
  kw_only=True,
@@ -47,6 +46,9 @@ class FuncHandler(ABCHandler[Event], typing.Generic[Event, Function, ErrorHandle
47
46
  def __post_init__(self) -> None:
48
47
  self.dataclass = typing.get_origin(self.dataclass) or self.dataclass
49
48
 
49
+ if self.dataclass is not None and self.adapter is None:
50
+ self.adapter = DataclassAdapter(self.dataclass, self.update_type)
51
+
50
52
  @property
51
53
  def __call__(self) -> Function:
52
54
  return self.function
@@ -54,7 +56,7 @@ class FuncHandler(ABCHandler[Event], typing.Generic[Event, Function, ErrorHandle
54
56
  def __repr__(self) -> str:
55
57
  return "<{}: {}={!r} with rules={!r}, dataclass={!r}, error_handler={!r}>".format(
56
58
  self.__class__.__name__,
57
- "blocking function" if self.is_blocking else "function",
59
+ "final function" if self.final else "function",
58
60
  self.function.__qualname__,
59
61
  self.rules,
60
62
  self.dataclass,
@@ -62,9 +64,17 @@ class FuncHandler(ABCHandler[Event], typing.Generic[Event, Function, ErrorHandle
62
64
  )
63
65
 
64
66
  @cached_property
65
- def required_nodes(self) -> dict[str, type[Node]]:
67
+ def required_nodes(self) -> dict[str, type[NodeType]]:
66
68
  return get_nodes(self.function)
67
69
 
70
+ def get_name_event_param(self, event: Event) -> str | None:
71
+ event_class = self.dataclass or event.__class__
72
+ for k, v in get_annotations(self.function).items():
73
+ if isinstance(v := typing.get_origin(v) or v, type) and v is event_class:
74
+ self.func_event_param = k
75
+ return k
76
+ return None
77
+
68
78
  async def check(self, api: API, event: Update, ctx: Context | None = None) -> bool:
69
79
  if self.update_type is not None and self.update_type != event.update_type:
70
80
  return False
@@ -72,7 +82,7 @@ class FuncHandler(ABCHandler[Event], typing.Generic[Event, Function, ErrorHandle
72
82
  logger.debug("Checking handler {!r}...", self)
73
83
  ctx = Context(raw_update=event) if ctx is None else ctx
74
84
  temp_ctx = ctx.copy()
75
- temp_ctx |= self.preset_context
85
+ temp_ctx |= self.preset_context.copy()
76
86
  update = event
77
87
 
78
88
  for rule in self.rules:
@@ -83,21 +93,15 @@ class FuncHandler(ABCHandler[Event], typing.Generic[Event, Function, ErrorHandle
83
93
  nodes = self.required_nodes
84
94
  node_col = None
85
95
  if nodes:
86
- result = await compose_nodes(nodes, ctx, data={Update: event, API: api})
96
+ result = await compose_nodes(nodes, ctx, data={Update: update, API: api})
87
97
  if not result:
88
- logger.debug(f"Cannot compose nodes for handler. Error: {result.error!r}")
98
+ logger.debug(f"Cannot compose nodes for handler, error: {str(result.error)}")
89
99
  return False
90
100
 
91
101
  node_col = result.value
92
102
  temp_ctx |= node_col.values
93
103
 
94
- if EVENT_NODE_KEY in ctx:
95
- for name, node in nodes.items():
96
- if node is ctx[EVENT_NODE_KEY] and name in temp_ctx:
97
- ctx[name] = temp_ctx.pop(name)
98
-
99
104
  logger.debug("All checks passed for handler.")
100
-
101
105
  temp_ctx["node_col"] = node_col
102
106
  ctx |= temp_ctx
103
107
  return True
@@ -109,27 +113,17 @@ class FuncHandler(ABCHandler[Event], typing.Generic[Event, Function, ErrorHandle
109
113
  ctx: Context,
110
114
  node_col: "NodeCollection | None" = None,
111
115
  ) -> typing.Any:
112
- logger.debug(f"Running func handler {self.function.__qualname__!r}")
113
-
114
- if self.dataclass is not None and EVENT_NODE_KEY not in ctx:
115
- if self.update_type is not None and isinstance(event, Update):
116
- update = getattr(event, event.update_type.value).unwrap()
117
- event = (
118
- self.dataclass.from_update(update, bound_api=api)
119
- if issubclass(self.dataclass, BaseCute)
120
- else self.dataclass(**update.to_dict()) # type: ignore
121
- )
122
-
123
- elif issubclass(self.dataclass, UpdateCute) and isinstance(event, Update):
124
- event = self.dataclass.from_update(event, bound_api=api)
125
-
126
- else:
127
- event = self.dataclass(**event.to_dict()) # type: ignore
128
-
129
- result = (await self.error_handler.run(self.function, event, api, ctx)).unwrap()
130
- if node_col := ctx.node_col:
131
- await node_col.close_all()
132
- return result
116
+ logger.debug(f"Running handler {self!r}...")
117
+
118
+ try:
119
+ if event_param := self.get_name_event_param(event):
120
+ ctx = Context(**{event_param: event, **ctx})
121
+ return await self(**magic_bundle(self.function, ctx, start_idx=0))
122
+ except BaseException as exception:
123
+ return await self.error_handler.run(exception, event, api, ctx)
124
+ finally:
125
+ if node_col := ctx.node_col:
126
+ await node_col.close_all()
133
127
 
134
128
 
135
129
  __all__ = ("FuncHandler",)
@@ -1,20 +1,21 @@
1
1
  import typing
2
2
 
3
3
  from telegrinder.api.api import API
4
- from telegrinder.bot.cute_types.message import InputMediaType, MessageCute
4
+ from telegrinder.bot.cute_types.message import MessageCute
5
5
  from telegrinder.bot.dispatch.context import Context
6
6
  from telegrinder.bot.dispatch.handler.base import BaseReplyHandler
7
7
  from telegrinder.bot.rules.abc import ABCRule
8
+ from telegrinder.types.objects import InputMedia
8
9
 
9
10
 
10
11
  class MediaGroupReplyHandler(BaseReplyHandler):
11
12
  def __init__(
12
13
  self,
13
- media: InputMediaType | list[InputMediaType],
14
+ media: InputMedia | list[InputMedia],
14
15
  *rules: ABCRule,
15
16
  caption: str | list[str] | None = None,
16
17
  parse_mode: str | list[str] | None = None,
17
- is_blocking: bool = True,
18
+ final: bool = True,
18
19
  as_reply: bool = False,
19
20
  preset_context: Context | None = None,
20
21
  **default_params: typing.Any,
@@ -24,7 +25,7 @@ class MediaGroupReplyHandler(BaseReplyHandler):
24
25
  self.caption = caption
25
26
  super().__init__(
26
27
  *rules,
27
- is_blocking=is_blocking,
28
+ final=final,
28
29
  as_reply=as_reply,
29
30
  preset_context=preset_context,
30
31
  **default_params,
@@ -13,7 +13,7 @@ class MessageReplyHandler(BaseReplyHandler):
13
13
  text: str,
14
14
  *rules: ABCRule,
15
15
  parse_mode: str | None = None,
16
- is_blocking: bool = True,
16
+ final: bool = True,
17
17
  as_reply: bool = False,
18
18
  preset_context: Context | None = None,
19
19
  **default_params: typing.Any,
@@ -22,7 +22,7 @@ class MessageReplyHandler(BaseReplyHandler):
22
22
  self.parse_mode = parse_mode
23
23
  super().__init__(
24
24
  *rules,
25
- is_blocking=is_blocking,
25
+ final=final,
26
26
  as_reply=as_reply,
27
27
  preset_context=preset_context,
28
28
  **default_params,
@@ -15,7 +15,7 @@ class PhotoReplyHandler(BaseReplyHandler):
15
15
  *rules: ABCRule,
16
16
  caption: str | None = None,
17
17
  parse_mode: str | None = None,
18
- is_blocking: bool = True,
18
+ final: bool = True,
19
19
  as_reply: bool = False,
20
20
  preset_context: Context | None = None,
21
21
  **default_params: typing.Any,
@@ -25,7 +25,7 @@ class PhotoReplyHandler(BaseReplyHandler):
25
25
  self.caption = caption
26
26
  super().__init__(
27
27
  *rules,
28
- is_blocking=is_blocking,
28
+ final=final,
29
29
  as_reply=as_reply,
30
30
  preset_context=preset_context,
31
31
  **default_params,
@@ -14,7 +14,7 @@ class StickerReplyHandler(BaseReplyHandler):
14
14
  sticker: InputFile | str,
15
15
  *rules: ABCRule,
16
16
  emoji: str | None = None,
17
- is_blocking: bool = True,
17
+ final: bool = True,
18
18
  as_reply: bool = False,
19
19
  preset_context: Context | None = None,
20
20
  **default_params: typing.Any,
@@ -23,7 +23,7 @@ class StickerReplyHandler(BaseReplyHandler):
23
23
  self.emoji = emoji
24
24
  super().__init__(
25
25
  *rules,
26
- is_blocking=is_blocking,
26
+ final=final,
27
27
  as_reply=as_reply,
28
28
  preset_context=preset_context,
29
29
  **default_params,
@@ -15,7 +15,7 @@ class VideoReplyHandler(BaseReplyHandler):
15
15
  *rules: ABCRule,
16
16
  caption: str | None = None,
17
17
  parse_mode: str | None = None,
18
- is_blocking: bool = True,
18
+ final: bool = True,
19
19
  as_reply: bool = False,
20
20
  preset_context: Context | None = None,
21
21
  **default_params: typing.Any,
@@ -25,7 +25,7 @@ class VideoReplyHandler(BaseReplyHandler):
25
25
  self.caption = caption
26
26
  super().__init__(
27
27
  *rules,
28
- is_blocking=is_blocking,
28
+ final=final,
29
29
  as_reply=as_reply,
30
30
  preset_context=preset_context,
31
31
  **default_params,
@@ -1,22 +1,97 @@
1
- import typing
1
+ from __future__ import annotations
2
+
2
3
  from abc import ABC
3
4
 
5
+ import typing_extensions as typing
6
+ from fntypes import Some
7
+
8
+ from telegrinder.api import API
9
+ from telegrinder.bot.cute_types.base import BaseCute
4
10
  from telegrinder.bot.dispatch.context import Context
5
11
  from telegrinder.model import Model
12
+ from telegrinder.modules import logger
13
+ from telegrinder.tools.adapter.abc import ABCAdapter, run_adapter
14
+ from telegrinder.tools.lifespan import Lifespan
6
15
  from telegrinder.types.objects import Update
7
16
 
8
- if typing.TYPE_CHECKING:
9
- from telegrinder.bot.rules.adapter.abc import ABCAdapter
17
+ ToEvent = typing.TypeVar("ToEvent", bound=Model, default=typing.Any)
18
+
19
+
20
+ async def run_middleware[Event: Model, R: bool | None](
21
+ method: typing.Callable[typing.Concatenate[Event, Context, ...], typing.Awaitable[R]],
22
+ api: API[typing.Any],
23
+ event: Event,
24
+ ctx: Context,
25
+ raw_event: Update | None = None,
26
+ adapter: "ABCAdapter[Update, Event] | None" = None,
27
+ *args: typing.Any,
28
+ **kwargs: typing.Any,
29
+ ) -> R:
30
+ if adapter is not None:
31
+ if raw_event is None:
32
+ raise RuntimeError("raw_event must be specified to apply adapter")
33
+ match await run_adapter(adapter, api, raw_event, ctx):
34
+ case Some(val):
35
+ event = val
36
+ case _:
37
+ return False # type: ignore
38
+
39
+ logger.debug("Running {}-middleware {!r}...", method.__name__, method.__qualname__.split(".")[0])
40
+ return await method(event, ctx, *args, **kwargs) # type: ignore
41
+
10
42
 
11
- Event = typing.TypeVar("Event", bound=Model)
43
+ class ABCMiddleware[Event: Model | BaseCute](ABC):
44
+ adapter: ABCAdapter[Update, Event] | None = None
12
45
 
46
+ def __repr__(self) -> str:
47
+ name = f"middleware {self.__class__.__name__!r}:"
48
+ has_pre = self.pre.__qualname__.split(".")[0] != "ABCMiddleware"
49
+ has_post = self.post.__qualname__.split(".")[0] != "ABCMiddleware"
13
50
 
14
- class ABCMiddleware(ABC, typing.Generic[Event]):
15
- adapter: "ABCAdapter[Update, Event] | None" = None
51
+ if has_post:
52
+ name = "post-" + name
53
+ if has_pre:
54
+ name = "pre-" + name
55
+
56
+ return "<{} with adapter={!r}>".format(name, self.adapter)
16
57
 
17
58
  async def pre(self, event: Event, ctx: Context) -> bool: ...
18
59
 
19
- async def post(self, event: Event, responses: list[typing.Any], ctx: Context) -> None: ...
60
+ async def post(self, event: Event, ctx: Context) -> None: ...
61
+
62
+ @typing.overload
63
+ def to_lifespan(self, event: Event, ctx: Context | None = None, *, api: API) -> Lifespan: ...
64
+
65
+ @typing.overload
66
+ def to_lifespan(self, event: Event, ctx: Context | None = None) -> Lifespan: ...
67
+
68
+ def to_lifespan(
69
+ self,
70
+ event: Event,
71
+ ctx: Context | None = None,
72
+ api: API | None = None,
73
+ **add_context: typing.Any,
74
+ ) -> Lifespan:
75
+ if api is None:
76
+ if not isinstance(event, BaseCute):
77
+ raise LookupError("Cannot get api, please pass as kwarg or provide BaseCute api-bound event")
78
+ api = event.api
79
+
80
+ ctx = ctx or Context()
81
+ ctx |= add_context
82
+ return Lifespan(
83
+ startup_tasks=[run_middleware(self.pre, api, event, raw_event=None, ctx=ctx, adapter=None)],
84
+ shutdown_tasks=[
85
+ run_middleware(
86
+ self.post,
87
+ api,
88
+ event,
89
+ raw_event=None,
90
+ ctx=ctx,
91
+ adapter=None,
92
+ )
93
+ ],
94
+ )
20
95
 
21
96
 
22
- __all__ = ("ABCMiddleware",)
97
+ __all__ = ("ABCMiddleware", "run_middleware")
@@ -0,0 +1,70 @@
1
+ import inspect
2
+ import typing
3
+ from contextlib import contextmanager
4
+
5
+ from telegrinder.api import API
6
+ from telegrinder.bot.cute_types.update import UpdateCute
7
+ from telegrinder.bot.dispatch.context import Context
8
+ from telegrinder.bot.dispatch.middleware.abc import ABCMiddleware
9
+ from telegrinder.bot.rules.abc import ABCRule, check_rule
10
+ from telegrinder.node import IsNode, compose_nodes
11
+ from telegrinder.tools.adapter.abc import ABCAdapter
12
+ from telegrinder.tools.adapter.raw_update import RawUpdateAdapter
13
+ from telegrinder.types import Update
14
+
15
+
16
+ class GlobalMiddleware(ABCMiddleware):
17
+ adapter = RawUpdateAdapter()
18
+
19
+ def __init__(self):
20
+ self.filters: set[ABCRule] = set()
21
+ self.source_filters: dict[ABCAdapter | IsNode, dict[typing.Any, ABCRule]] = {}
22
+
23
+ async def pre(self, event: UpdateCute, ctx: Context) -> bool:
24
+ for filter in self.filters:
25
+ if not await check_rule(event.api, filter, event, ctx):
26
+ return False
27
+
28
+ # Simple implication.... Grouped by source categories
29
+ for source, identifiers in self.source_filters.items():
30
+ if isinstance(source, ABCAdapter):
31
+ result = source.adapt(event.api, event, ctx)
32
+ if inspect.isawaitable(result):
33
+ result = await result
34
+
35
+ result = result.unwrap_or_none()
36
+ if result is None:
37
+ return True
38
+
39
+ else:
40
+ result = await compose_nodes({"value": source}, ctx, {Update: event, API: event.api})
41
+ if result := result.unwrap():
42
+ result = result.values["value"]
43
+ else:
44
+ return True
45
+
46
+ if result in identifiers:
47
+ return await check_rule(event.api, identifiers[result], event, ctx)
48
+
49
+ return True
50
+
51
+ @contextmanager
52
+ def apply_filters(
53
+ self,
54
+ *filters: ABCRule,
55
+ source_filter: tuple[ABCAdapter | IsNode, typing.Any, ABCRule] | None = None,
56
+ ):
57
+ if source_filter is not None:
58
+ self.source_filters.setdefault(source_filter[0], {})
59
+ self.source_filters[source_filter[0]].update({source_filter[1]: source_filter[2]})
60
+
61
+ self.filters |= set(filters)
62
+ yield
63
+ self.filters.difference_update(filters)
64
+
65
+ if source_filter is not None: # noqa: SIM102
66
+ if identifiers := self.source_filters.get(source_filter[0]):
67
+ identifiers.pop(source_filter[1], None)
68
+
69
+
70
+ __all__ = ("GlobalMiddleware",)
@@ -1,68 +1,41 @@
1
- import inspect
2
1
  import typing
3
2
 
4
- from fntypes.option import Nothing, Option, Some
5
- from fntypes.result import Error, Ok
3
+ from fntypes.option import Nothing, Some
6
4
 
7
5
  from telegrinder.api.api import API
8
6
  from telegrinder.bot.cute_types.update import UpdateCute
9
7
  from telegrinder.bot.dispatch.context import Context
10
- from telegrinder.bot.dispatch.middleware.abc import ABCMiddleware
8
+ from telegrinder.bot.dispatch.middleware.abc import ABCMiddleware, run_middleware
11
9
  from telegrinder.bot.dispatch.return_manager.abc import ABCReturnManager
12
10
  from telegrinder.model import Model
13
11
  from telegrinder.modules import logger
14
12
  from telegrinder.node.composer import CONTEXT_STORE_NODES_KEY, NodeScope, compose_nodes
13
+ from telegrinder.tools.adapter.abc import run_adapter
15
14
  from telegrinder.tools.i18n.abc import I18nEnum
16
15
  from telegrinder.types.objects import Update
17
16
 
18
17
  if typing.TYPE_CHECKING:
19
18
  from telegrinder.bot.dispatch.handler.abc import ABCHandler
20
19
  from telegrinder.bot.rules.abc import ABCRule
21
- from telegrinder.bot.rules.adapter.abc import ABCAdapter
22
20
 
23
- T = typing.TypeVar("T")
24
- Event = typing.TypeVar("Event", bound=Model)
25
21
 
26
-
27
- async def run_adapter(
28
- adapter: "ABCAdapter[Update, T]",
29
- api: API,
30
- update: Update,
31
- context: Context,
32
- ) -> Option[T]:
33
- adapt_result = adapter.adapt(api, update, context)
34
- match await adapt_result if inspect.isawaitable(adapt_result) else adapt_result:
35
- case Ok(value):
36
- return Some(value)
37
- case Error(err):
38
- logger.debug("Adapter failed with error message: {!r}", str(err))
39
- return Nothing()
40
-
41
-
42
- async def process_inner(
22
+ async def process_inner[Event: Model](
43
23
  api: API,
44
24
  event: Event,
45
25
  raw_event: Update,
26
+ ctx: Context,
46
27
  middlewares: list[ABCMiddleware[Event]],
47
28
  handlers: list["ABCHandler[Event]"],
48
29
  return_manager: ABCReturnManager[Event] | None = None,
49
30
  ) -> bool:
50
31
  logger.debug("Processing {!r}...", event.__class__.__name__)
51
- ctx = Context(raw_update=raw_event)
52
32
  ctx[CONTEXT_STORE_NODES_KEY] = {} # For per-event shared nodes
53
33
 
54
34
  logger.debug("Run pre middlewares...")
55
- for middleware in middlewares:
56
- if middleware.adapter is not None:
57
- match await run_adapter(middleware.adapter, api, raw_event, ctx):
58
- case Some(val):
59
- event = val
60
- case Nothing():
61
- return False
62
-
63
- middleware_result = await middleware.pre(event, ctx)
64
- logger.debug("Middleware {!r} returned: {!r}", middleware.__class__.__qualname__, middleware_result)
65
- if middleware_result is False:
35
+ for m in middlewares:
36
+ result = await run_middleware(m.pre, api, event, raw_event=raw_event, ctx=ctx, adapter=m.adapter)
37
+ logger.debug("Middleware {!r} returned: {!r}", m, result)
38
+ if result is False:
66
39
  return False
67
40
 
68
41
  found = False
@@ -70,23 +43,41 @@ async def process_inner(
70
43
  ctx_copy = ctx.copy()
71
44
 
72
45
  for handler in handlers:
46
+ adapted_event = event
47
+
73
48
  if await handler.check(api, raw_event, ctx):
74
- logger.debug("Handler {!r} matched, run...", handler)
49
+ if handler.adapter is not None:
50
+ match await run_adapter(handler.adapter, api, raw_event, ctx):
51
+ case Some(value):
52
+ adapted_event = value
53
+ case Nothing():
54
+ continue
55
+
75
56
  found = True
76
- response = await handler.run(api, event, ctx)
57
+ logger.debug("Handler {!r} matched, run...", handler)
58
+ response = await handler.run(api, adapted_event, ctx)
77
59
  logger.debug("Handler {!r} returned: {!r}", handler, response)
78
60
  responses.append(response)
61
+
79
62
  if return_manager is not None:
80
63
  await return_manager.run(response, event, ctx)
81
- if handler.is_blocking:
64
+ if handler.final:
82
65
  break
83
66
 
84
67
  ctx = ctx_copy
85
68
 
86
69
  logger.debug("Run post middlewares...")
87
- for middleware in middlewares:
88
- logger.debug("Run post middleware {!r}", middleware.__class__.__qualname__)
89
- await middleware.post(event, responses, ctx)
70
+ ctx.set("responses", responses)
71
+
72
+ for m in middlewares:
73
+ await run_middleware(
74
+ m.post,
75
+ api,
76
+ event,
77
+ raw_event=raw_event,
78
+ ctx=ctx,
79
+ adapter=m.adapter,
80
+ )
90
81
 
91
82
  for session in ctx.get(CONTEXT_STORE_NODES_KEY, {}).values():
92
83
  await session.close(scopes=(NodeScope.PER_EVENT,))
@@ -106,7 +97,9 @@ async def check_rule(
106
97
  ctx: Context,
107
98
  ) -> bool:
108
99
  """Checks requirements, adapts update.
109
- Returns check result."""
100
+ Returns check result.
101
+ """
102
+ update_cute = None if not isinstance(update, UpdateCute) else update
110
103
 
111
104
  # Running adapter
112
105
  match await run_adapter(rule.adapter, api, update, ctx):
@@ -116,17 +109,17 @@ async def check_rule(
116
109
  return False
117
110
 
118
111
  # Preparing update
119
- if isinstance(adapted_val := ctx.get(rule.adapter.ADAPTED_VALUE_KEY or ""), UpdateCute):
120
- update = adapted_val
121
- elif isinstance(adapted_value, UpdateCute):
122
- update = adapted_value
112
+ if isinstance(adapted_value, UpdateCute):
113
+ update_cute = adapted_value
114
+ elif isinstance(adapted_val := ctx.get(rule.adapter.ADAPTED_VALUE_KEY or ""), UpdateCute):
115
+ update_cute = adapted_val
123
116
  else:
124
- update = UpdateCute.from_update(update, bound_api=api)
117
+ update_cute = UpdateCute.from_update(update, bound_api=api)
125
118
 
126
119
  # Running subrules to fetch requirements
127
120
  ctx_copy = ctx.copy()
128
121
  for requirement in rule.requires:
129
- if not await check_rule(api, requirement, update, ctx_copy):
122
+ if not await check_rule(api, requirement, update_cute, ctx_copy):
130
123
  return False
131
124
 
132
125
  # Translating translatable rules
@@ -141,11 +134,12 @@ async def check_rule(
141
134
  if nodes:
142
135
  result = await compose_nodes(nodes, ctx, data={Update: update, API: api})
143
136
  if not result:
137
+ logger.debug(f"Cannot compose nodes for rule, error: {str(result.error)}")
144
138
  return False
145
139
  node_col = result.value
146
140
 
147
141
  # Running check
148
- result = await rule.bounding_check(adapted_value, ctx, node_col=node_col)
142
+ result = await rule.bounding_check(ctx, adapted_value=adapted_value, node_col=node_col)
149
143
 
150
144
  # Closing node sessions if there are any
151
145
  if node_col is not None:
@@ -7,6 +7,7 @@ from telegrinder.bot.dispatch.return_manager.abc import (
7
7
  from telegrinder.bot.dispatch.return_manager.callback_query import CallbackQueryReturnManager
8
8
  from telegrinder.bot.dispatch.return_manager.inline_query import InlineQueryReturnManager
9
9
  from telegrinder.bot.dispatch.return_manager.message import MessageReturnManager
10
+ from telegrinder.bot.dispatch.return_manager.pre_checkout_query import PreCheckoutQueryManager
10
11
 
11
12
  __all__ = (
12
13
  "ABCReturnManager",
@@ -15,5 +16,6 @@ __all__ = (
15
16
  "InlineQueryReturnManager",
16
17
  "Manager",
17
18
  "MessageReturnManager",
19
+ "PreCheckoutQueryManager",
18
20
  "register_manager",
19
21
  )