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
@@ -1,93 +1,94 @@
1
1
  import typing
2
2
 
3
3
  from telegrinder.api.api import API
4
+ from telegrinder.bot.cute_types.base import BaseCute
4
5
  from telegrinder.bot.cute_types.update import UpdateCute
6
+ from telegrinder.bot.dispatch.context import Context
5
7
  from telegrinder.bot.dispatch.handler.func import Func, FuncHandler
6
8
  from telegrinder.bot.dispatch.process import process_inner
7
9
  from telegrinder.bot.dispatch.view.abc import ABCEventRawView
8
- from telegrinder.bot.dispatch.view.base import BaseView, ErrorHandlerT
10
+ from telegrinder.bot.dispatch.view.base import BaseView
9
11
  from telegrinder.bot.rules.abc import ABCRule
10
12
  from telegrinder.tools.error_handler.error_handler import ABCErrorHandler, ErrorHandler
11
13
  from telegrinder.types.enums import UpdateType
12
14
  from telegrinder.types.objects import Update
13
15
 
14
- T = typing.TypeVar("T", contravariant=True)
15
- R = typing.TypeVar("R", covariant=True)
16
- P = typing.ParamSpec("P")
17
-
18
16
 
19
17
  class RawEventView(ABCEventRawView[UpdateCute], BaseView[UpdateCute]):
20
- def __init__(self) -> None:
21
- self.auto_rules = []
22
- self.handlers = []
23
- self.middlewares = []
24
- self.return_manager = None
25
-
26
18
  @typing.overload
27
- def __call__(
19
+ def __call__[**P, R](
28
20
  self,
29
- update_type: UpdateType,
30
21
  *rules: ABCRule,
22
+ update_type: UpdateType,
23
+ final: bool = True,
31
24
  ) -> typing.Callable[
32
25
  [Func[P, R]],
33
- FuncHandler[UpdateCute, Func[P, R], ErrorHandler[UpdateCute]],
26
+ FuncHandler[BaseCute[typing.Any], Func[P, R], ErrorHandler[BaseCute[typing.Any]]],
34
27
  ]: ...
35
28
 
36
29
  @typing.overload
37
- def __call__(
30
+ def __call__[**P, Dataclass, R](
38
31
  self,
39
- update_type: UpdateType,
40
32
  *rules: ABCRule,
41
- dataclass: type[T],
42
- ) -> typing.Callable[[Func[P, R]], FuncHandler[UpdateCute, Func[P, R], ErrorHandler[T]]]: ...
33
+ dataclass: type[Dataclass],
34
+ final: bool = True,
35
+ ) -> typing.Callable[[Func[P, R]], FuncHandler[Dataclass, Func[P, R], ErrorHandler[Dataclass]]]: ...
43
36
 
44
37
  @typing.overload
45
- def __call__(
38
+ def __call__[**P, Dataclass, R](
46
39
  self,
40
+ *rules: ABCRule,
47
41
  update_type: UpdateType,
42
+ dataclass: type[Dataclass],
43
+ final: bool = True,
44
+ ) -> typing.Callable[[Func[P, R]], FuncHandler[Dataclass, Func[P, R], ErrorHandler[Dataclass]]]: ...
45
+
46
+ @typing.overload
47
+ def __call__[**P, ErrorHandlerT: ABCErrorHandler, R](
48
+ self,
48
49
  *rules: ABCRule,
49
50
  error_handler: ErrorHandlerT,
51
+ final: bool = True,
50
52
  ) -> typing.Callable[
51
53
  [Func[P, R]],
52
- FuncHandler[UpdateCute, Func[P, R], ErrorHandlerT],
54
+ FuncHandler[BaseCute[typing.Any], Func[P, R], ErrorHandlerT],
53
55
  ]: ...
54
56
 
55
57
  @typing.overload
56
- def __call__(
58
+ def __call__[**P, ErrorHandlerT: ABCErrorHandler, R](
57
59
  self,
58
- update_type: UpdateType,
59
60
  *rules: ABCRule,
60
- dataclass: type[typing.Any],
61
61
  error_handler: ErrorHandlerT,
62
- is_blocking: bool = True,
63
- ) -> typing.Callable[[Func[P, R]], FuncHandler[UpdateCute, Func[P, R], ErrorHandlerT]]: ...
64
-
65
- @typing.overload
66
- def __call__(
67
- self,
68
62
  update_type: UpdateType,
69
- *rules: ABCRule,
70
- dataclass: None = None,
71
- error_handler: None = None,
72
- is_blocking: bool = True,
63
+ final: bool = True,
73
64
  ) -> typing.Callable[
74
65
  [Func[P, R]],
75
- FuncHandler[UpdateCute, Func[P, R], ErrorHandler[UpdateCute]],
66
+ FuncHandler[BaseCute[typing.Any], Func[P, R], ErrorHandlerT],
76
67
  ]: ...
77
68
 
78
- def __call__( # type: ignore
69
+ @typing.overload
70
+ def __call__[**P, Dataclass, ErrorHandlerT: ABCErrorHandler, R](
79
71
  self,
72
+ *rules: ABCRule,
80
73
  update_type: UpdateType,
74
+ dataclass: type[Dataclass],
75
+ error_handler: ErrorHandlerT,
76
+ final: bool = True,
77
+ ) -> typing.Callable[[Func[P, R]], FuncHandler[Dataclass, Func[P, R], ErrorHandlerT]]: ...
78
+
79
+ def __call__(
80
+ self,
81
81
  *rules: ABCRule,
82
+ update_type: UpdateType | None = None,
82
83
  dataclass: type[typing.Any] | None = None,
83
84
  error_handler: ABCErrorHandler | None = None,
84
- is_blocking: bool = True,
85
- ):
86
- def wrapper(func):
85
+ final: bool = True,
86
+ ) -> typing.Callable[..., typing.Any]:
87
+ def wrapper(func: typing.Callable[..., typing.Any]):
87
88
  func_handler = FuncHandler(
88
89
  func,
89
90
  rules=[*self.auto_rules, *rules],
90
- is_blocking=is_blocking,
91
+ final=final,
91
92
  dataclass=dataclass,
92
93
  error_handler=error_handler or ErrorHandler(),
93
94
  update_type=update_type,
@@ -100,11 +101,12 @@ class RawEventView(ABCEventRawView[UpdateCute], BaseView[UpdateCute]):
100
101
  async def check(self, event: Update) -> bool:
101
102
  return bool(self.handlers) or bool(self.middlewares)
102
103
 
103
- async def process(self, event: Update, api: API) -> bool:
104
+ async def process(self, event: Update, api: API, context: Context) -> bool:
104
105
  return await process_inner(
105
106
  api,
106
107
  UpdateCute.from_update(event, bound_api=api),
107
108
  event,
109
+ context,
108
110
  self.middlewares,
109
111
  self.handlers,
110
112
  self.return_manager,
@@ -1,13 +1,14 @@
1
1
  import typing
2
2
 
3
+ from telegrinder.bot.cute_types import BaseCute
3
4
  from telegrinder.bot.dispatch.handler.abc import ABCHandler
4
5
 
5
- from .short_state import EventModel, ShortState
6
+ from .short_state import ShortState
6
7
 
7
8
 
8
- class WaiterActions(typing.TypedDict, typing.Generic[EventModel]):
9
- on_miss: typing.NotRequired[ABCHandler[EventModel]]
10
- on_drop: typing.NotRequired[typing.Callable[[ShortState[EventModel]], None]]
9
+ class WaiterActions[Event: BaseCute](typing.TypedDict):
10
+ on_miss: typing.NotRequired[ABCHandler[Event]]
11
+ on_drop: typing.NotRequired[typing.Callable[[ShortState[Event]], None]]
11
12
 
12
13
 
13
14
  __all__ = ("WaiterActions",)
@@ -7,12 +7,11 @@ from telegrinder.bot.cute_types import BaseCute
7
7
  from telegrinder.bot.dispatch.view.base import BaseView
8
8
  from telegrinder.tools.functional import from_optional
9
9
 
10
- T = typing.TypeVar("T")
11
- Event = typing.TypeVar("Event", bound=BaseCute)
12
- Data = typing.TypeVar("Data")
10
+ Event = typing.TypeVar("Event", bound=BaseCute, covariant=True)
11
+ Data = typing.TypeVar("Data", covariant=True)
13
12
 
14
13
 
15
- def _echo(__x: T) -> T:
14
+ def _echo[T](__x: T) -> T:
16
15
  return __x
17
16
 
18
17
 
@@ -40,17 +39,20 @@ class Hasher(typing.Generic[Event, Data]):
40
39
  def name(self) -> str:
41
40
  return f"{self.view_class.__name__}_{id(self)}"
42
41
 
43
- def get_hash_from_data(self, data: Data) -> Option[typing.Hashable]:
42
+ def get_hash_from_data[D](self: "Hasher[Event, D]", data: D) -> Option[typing.Hashable]:
44
43
  if self._get_hash_from_data is None:
45
44
  raise NotImplementedError
46
45
  return from_optional(self._get_hash_from_data(data))
47
46
 
48
- def get_data_from_event(self, event: Event) -> Option[Data]:
47
+ def get_data_from_event[E: BaseCute](self: "Hasher[E, Data]", event: E) -> Option[Data]:
49
48
  if not self._get_data_from_event:
50
49
  raise NotImplementedError
51
50
  return from_optional(self._get_data_from_event(event))
52
51
 
53
- def get_hash_from_data_from_event(self, event: Event) -> Option[typing.Hashable]:
52
+ def get_hash_from_data_from_event[E: BaseCute](
53
+ self: "Hasher[E, Data]",
54
+ event: E,
55
+ ) -> Option[typing.Hashable]:
54
56
  return self.get_data_from_event(event).and_then(self.get_hash_from_data) # type: ignore
55
57
 
56
58
 
File without changes
@@ -1,11 +1,12 @@
1
1
  from fntypes.option import Option
2
2
 
3
+ from telegrinder.bot.cute_types import BaseCute
3
4
  from telegrinder.bot.dispatch.view import BaseStateView
4
- from telegrinder.bot.dispatch.waiter_machine.hasher.hasher import ECHO, Event, Hasher
5
+ from telegrinder.bot.dispatch.waiter_machine.hasher.hasher import ECHO, Hasher
5
6
  from telegrinder.tools.functional import from_optional
6
7
 
7
8
 
8
- class StateViewHasher(Hasher[Event, int]):
9
+ class StateViewHasher[Event: BaseCute](Hasher[Event, int]):
9
10
  view: BaseStateView[Event]
10
11
 
11
12
  def __init__(self, view: BaseStateView[Event]) -> None:
@@ -2,29 +2,45 @@ import asyncio
2
2
  import datetime
3
3
  import typing
4
4
 
5
+ from telegrinder.bot.cute_types.base import BaseCute
5
6
  from telegrinder.bot.dispatch.abc import ABCDispatch
7
+ from telegrinder.bot.dispatch.context import Context
6
8
  from telegrinder.bot.dispatch.view.base import BaseStateView, BaseView
7
- from telegrinder.bot.dispatch.waiter_machine.middleware import WaiterMiddleware
9
+ from telegrinder.bot.dispatch.waiter_machine.middleware import INITIATOR_CONTEXT_KEY, WaiterMiddleware
8
10
  from telegrinder.bot.dispatch.waiter_machine.short_state import (
9
- EventModel,
10
11
  ShortState,
11
12
  ShortStateContext,
12
13
  )
13
14
  from telegrinder.bot.rules.abc import ABCRule
15
+ from telegrinder.tools.lifespan import Lifespan
14
16
  from telegrinder.tools.limited_dict import LimitedDict
15
17
 
16
18
  from .actions import WaiterActions
17
19
  from .hasher import Hasher, StateViewHasher
18
20
 
19
- HasherData = typing.TypeVar("HasherData")
20
-
21
- Storage: typing.TypeAlias = dict[
22
- Hasher[EventModel, HasherData], LimitedDict[typing.Hashable, ShortState[EventModel]]
21
+ type Storage[Event: BaseCute, HasherData] = dict[
22
+ Hasher[Event, HasherData],
23
+ LimitedDict[typing.Hashable, ShortState[Event]],
23
24
  ]
25
+ type HasherWithData[Event: BaseCute, Data] = tuple[Hasher[Event, Data], Data]
24
26
 
25
27
  WEEK: typing.Final[datetime.timedelta] = datetime.timedelta(days=7)
26
28
 
27
29
 
30
+ class ContextUnpackProto[*Ts](typing.Protocol):
31
+ __name__: str
32
+
33
+ def __call__(self, context: Context, /) -> tuple[*Ts]: ...
34
+
35
+
36
+ def unpack_to_context(context: Context) -> tuple[Context]:
37
+ return (context,)
38
+
39
+
40
+ def no_unpack(_: Context) -> tuple[()]:
41
+ return ()
42
+
43
+
28
44
  class WaiterMachine:
29
45
  def __init__(
30
46
  self,
@@ -39,20 +55,20 @@ class WaiterMachine:
39
55
  self.storage: Storage = {}
40
56
 
41
57
  def __repr__(self) -> str:
42
- return "<{}: max_storage_size={}, base_state_lifetime={!r}>".format(
58
+ return "<{}: with {} storage items and max_storage_size={}, base_state_lifetime={!r}>".format(
43
59
  self.__class__.__name__,
60
+ len(self.storage),
44
61
  self.max_storage_size,
45
62
  self.base_state_lifetime,
46
63
  )
47
64
 
48
- def create_middleware(self, view: BaseStateView[EventModel]) -> WaiterMiddleware[EventModel]:
65
+ def create_middleware[Event: BaseCute](self, view: BaseStateView[Event]) -> WaiterMiddleware[Event]:
49
66
  hasher = StateViewHasher(view)
50
67
  self.storage[hasher] = LimitedDict(maxlimit=self.max_storage_size)
51
68
  return WaiterMiddleware(self, hasher)
52
69
 
53
70
  async def drop_all(self) -> None:
54
71
  """Drops all waiters in storage."""
55
-
56
72
  for hasher in self.storage.copy():
57
73
  for ident, short_state in self.storage[hasher].items():
58
74
  if short_state.context:
@@ -62,17 +78,17 @@ class WaiterMachine:
62
78
 
63
79
  del self.storage[hasher]
64
80
 
65
- async def drop(
81
+ async def drop[Event: BaseCute, HasherData](
66
82
  self,
67
- hasher: Hasher[EventModel, HasherData],
68
- id: HasherData,
83
+ hasher: Hasher[Event, HasherData],
84
+ data: HasherData,
69
85
  **context: typing.Any,
70
86
  ) -> None:
71
87
  if hasher not in self.storage:
72
88
  raise LookupError("No record of hasher {!r} found.".format(hasher))
73
89
 
74
- waiter_id: typing.Hashable = hasher.get_hash_from_data(id).expect(
75
- RuntimeError("Couldn't create hash from data")
90
+ waiter_id: typing.Hashable = hasher.get_hash_from_data(data).expect(
91
+ RuntimeError("Couldn't create hash from data"),
76
92
  )
77
93
  short_state = self.storage[hasher].pop(waiter_id, None)
78
94
  if short_state is None:
@@ -85,16 +101,17 @@ class WaiterMachine:
85
101
 
86
102
  await short_state.cancel()
87
103
 
88
- async def wait_from_event(
104
+ async def wait_from_event[Event: BaseCute](
89
105
  self,
90
- view: BaseStateView[EventModel],
91
- event: EventModel,
106
+ view: BaseStateView[Event],
107
+ event: Event,
92
108
  *,
93
109
  filter: ABCRule | None = None,
94
110
  release: ABCRule | None = None,
95
111
  lifetime: datetime.timedelta | float | None = None,
96
- **actions: typing.Unpack[WaiterActions[EventModel]],
97
- ) -> ShortStateContext[EventModel]:
112
+ lifespan: Lifespan | None = None,
113
+ **actions: typing.Unpack[WaiterActions[Event]],
114
+ ) -> ShortStateContext[Event]:
98
115
  hasher = StateViewHasher(view)
99
116
  return await self.wait(
100
117
  hasher=hasher,
@@ -104,24 +121,27 @@ class WaiterMachine:
104
121
  filter=filter,
105
122
  release=release,
106
123
  lifetime=lifetime,
124
+ lifespan=lifespan or Lifespan(),
107
125
  **actions,
108
126
  )
109
127
 
110
- async def wait(
128
+ async def wait[Event: BaseCute, HasherData](
111
129
  self,
112
- hasher: Hasher[EventModel, HasherData],
130
+ hasher: Hasher[Event, HasherData],
113
131
  data: HasherData,
114
132
  *,
115
133
  filter: ABCRule | None = None,
116
134
  release: ABCRule | None = None,
117
135
  lifetime: datetime.timedelta | float | None = None,
118
- **actions: typing.Unpack[WaiterActions[EventModel]],
119
- ) -> ShortStateContext[EventModel]:
136
+ lifespan: Lifespan | None = None,
137
+ **actions: typing.Unpack[WaiterActions[Event]],
138
+ ) -> ShortStateContext[Event]:
120
139
  if isinstance(lifetime, int | float):
121
140
  lifetime = datetime.timedelta(seconds=lifetime)
122
141
 
142
+ lifespan = lifespan or Lifespan()
123
143
  event = asyncio.Event()
124
- short_state = ShortState[EventModel](
144
+ short_state = ShortState[Event](
125
145
  event,
126
146
  actions,
127
147
  release=release,
@@ -132,8 +152,8 @@ class WaiterMachine:
132
152
 
133
153
  if hasher not in self.storage:
134
154
  if self.dispatch:
135
- view: BaseView[EventModel] = self.dispatch.get_view(hasher.view_class).expect(
136
- RuntimeError(f"View {hasher.view_class.__name__!r} is not defined in dispatch.")
155
+ view: BaseView[Event] = self.dispatch.get_view(hasher.view_class).expect(
156
+ RuntimeError(f"View {hasher.view_class.__name__!r} is not defined in dispatch."),
137
157
  )
138
158
  view.middlewares.insert(0, WaiterMiddleware(self, hasher))
139
159
  self.storage[hasher] = LimitedDict(maxlimit=self.max_storage_size)
@@ -141,23 +161,82 @@ class WaiterMachine:
141
161
  if (deleted_short_state := self.storage[hasher].set(waiter_hash, short_state)) is not None:
142
162
  await deleted_short_state.cancel()
143
163
 
144
- await event.wait()
164
+ async with lifespan:
165
+ await event.wait()
166
+
145
167
  self.storage[hasher].pop(waiter_hash, None)
146
- assert short_state.context is not None
168
+
169
+ if short_state.context is None:
170
+ raise LookupError("No context in short_state.")
147
171
  return short_state.context
148
172
 
173
+ async def wait_many[RestEvent: BaseCute[typing.Any], Data, *Ts](
174
+ self,
175
+ *hashers: HasherWithData[RestEvent, Data],
176
+ filter: ABCRule | None = None,
177
+ release: ABCRule | None = None,
178
+ lifetime: datetime.timedelta | float | None = None,
179
+ lifespan: Lifespan | None = None,
180
+ unpack: ContextUnpackProto[*Ts] = unpack_to_context,
181
+ **actions: typing.Unpack[WaiterActions[BaseCute[typing.Any]]],
182
+ ) -> tuple[HasherWithData[RestEvent, Data], RestEvent, *Ts]:
183
+ if isinstance(lifetime, int | float):
184
+ lifetime = datetime.timedelta(seconds=lifetime)
185
+
186
+ lifespan = lifespan or Lifespan()
187
+ event = asyncio.Event()
188
+ short_state = ShortState(
189
+ event,
190
+ actions,
191
+ release=release,
192
+ filter=filter,
193
+ lifetime=lifetime or self.base_state_lifetime,
194
+ )
195
+ waiter_hashes: dict[Hasher[RestEvent, Data], typing.Hashable] = {}
196
+
197
+ for hasher, data in hashers:
198
+ waiter_hash = hasher.get_hash_from_data(data).expect(RuntimeError("Hasher couldn't create hash."))
199
+
200
+ if hasher not in self.storage:
201
+ if self.dispatch:
202
+ view = self.dispatch.get_view(hasher.view_class).expect(
203
+ RuntimeError(f"View {hasher.view_class.__name__!r} is not defined in dispatch."),
204
+ )
205
+ view.middlewares.insert(0, WaiterMiddleware(self, hasher))
206
+ self.storage[hasher] = LimitedDict(maxlimit=self.max_storage_size)
207
+
208
+ if (deleted_short_state := self.storage[hasher].set(waiter_hash, short_state)) is not None:
209
+ await deleted_short_state.cancel()
210
+
211
+ waiter_hashes[hasher] = waiter_hash
212
+
213
+ async with lifespan:
214
+ await event.wait()
215
+
216
+ if short_state.context is None:
217
+ raise LookupError("No context in short_state.")
218
+
219
+ initiator = short_state.context.context.get(INITIATOR_CONTEXT_KEY)
220
+ if initiator is None:
221
+ raise LookupError("Initiator not found in short_state context.")
222
+
223
+ for hasher, waiter_hash in waiter_hashes.items():
224
+ self.storage[hasher].pop(waiter_hash, None)
225
+
226
+ return (
227
+ initiator,
228
+ short_state.context.event, # type: ignore
229
+ *unpack(short_state.context.context),
230
+ )
231
+
149
232
  async def clear_storage(self) -> None:
150
233
  """Clears storage."""
234
+ now = datetime.datetime.now()
151
235
 
152
236
  for hasher in self.storage:
153
- now = datetime.datetime.now()
154
237
  for ident, short_state in self.storage.get(hasher, {}).copy().items():
155
238
  if short_state.expiration_date is not None and now > short_state.expiration_date:
156
- await self.drop(
157
- hasher,
158
- ident,
159
- force=True,
160
- )
239
+ await self.drop(hasher, data=ident, force=True)
161
240
 
162
241
 
163
242
  async def clear_wm_storage_worker(
@@ -15,10 +15,11 @@ if typing.TYPE_CHECKING:
15
15
  from .machine import WaiterMachine
16
16
  from .short_state import ShortState
17
17
 
18
- EventType = typing.TypeVar("EventType", bound=BaseCute)
19
18
 
19
+ INITIATOR_CONTEXT_KEY = "initiator"
20
20
 
21
- class WaiterMiddleware(ABCMiddleware[EventType]):
21
+
22
+ class WaiterMiddleware[Event: BaseCute](ABCMiddleware[Event]):
22
23
  def __init__(
23
24
  self,
24
25
  machine: "WaiterMachine",
@@ -27,16 +28,16 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
27
28
  self.machine = machine
28
29
  self.hasher = hasher
29
30
 
30
- async def pre(self, event: EventType, ctx: Context) -> bool:
31
+ async def pre(self, event: Event, ctx: Context) -> bool:
31
32
  if self.hasher not in self.machine.storage:
32
33
  return True
33
34
 
34
35
  key = self.hasher.get_hash_from_data_from_event(event)
35
- if key is None:
36
- logger.info(f"Unable to get hash from event with hasher {self.hasher}")
36
+ if not key:
37
+ logger.info(f"Unable to get hash from event with hasher {self.hasher!r}")
37
38
  return True
38
39
 
39
- short_state: "ShortState[EventType] | None" = self.machine.storage[self.hasher].get(key.unwrap())
40
+ short_state: "ShortState[Event] | None" = self.machine.storage[self.hasher].get(key.unwrap())
40
41
  if not short_state:
41
42
  return True
42
43
 
@@ -46,7 +47,10 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
46
47
 
47
48
  # Run filter rule
48
49
  if short_state.filter and not await check_rule(
49
- event.ctx_api, short_state.filter, ctx.raw_update, preset_context
50
+ event.ctx_api,
51
+ short_state.filter,
52
+ ctx.raw_update,
53
+ preset_context,
50
54
  ):
51
55
  logger.debug("Filter rule {!r} failed", short_state.filter)
52
56
  return True
@@ -62,9 +66,9 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
62
66
  handler = FuncHandler(
63
67
  self.pass_runtime,
64
68
  [short_state.release] if short_state.release else [],
65
- dataclass=None,
66
69
  preset_context=preset_context,
67
70
  )
71
+ handler.get_name_event_param = lambda event: "event" # FIXME: HOTFIX
68
72
  result = await handler.check(event.ctx_api, ctx.raw_update, ctx)
69
73
 
70
74
  if result is True:
@@ -78,10 +82,11 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
78
82
 
79
83
  async def pass_runtime(
80
84
  self,
81
- event: EventType,
82
- short_state: "ShortState[EventType]",
85
+ event: Event,
86
+ short_state: "ShortState[Event]",
83
87
  ctx: Context,
84
88
  ) -> None:
89
+ ctx.initiator = self.hasher
85
90
  short_state.context = ShortStateContext(event, ctx)
86
91
  short_state.event.set()
87
92
 
@@ -2,33 +2,25 @@ import asyncio
2
2
  import dataclasses
3
3
  import datetime
4
4
  import typing
5
- from contextlib import suppress
6
5
 
7
6
  from telegrinder.bot.cute_types import BaseCute
8
7
  from telegrinder.bot.dispatch.context import Context
9
- from telegrinder.bot.dispatch.handler.abc import ABCHandler
10
8
  from telegrinder.bot.rules.abc import ABCRule
11
- from telegrinder.model import Model
9
+ from telegrinder.tools.magic import cancel_future
12
10
 
13
11
  if typing.TYPE_CHECKING:
14
12
  from .actions import WaiterActions
15
13
 
16
14
 
17
- T = typing.TypeVar("T", bound=Model)
18
- EventModel = typing.TypeVar("EventModel", bound=BaseCute)
19
-
20
- Behaviour: typing.TypeAlias = ABCHandler[T] | None
21
-
22
-
23
- class ShortStateContext(typing.Generic[EventModel], typing.NamedTuple):
24
- event: EventModel
15
+ class ShortStateContext[Event: BaseCute](typing.NamedTuple):
16
+ event: Event
25
17
  context: Context
26
18
 
27
19
 
28
20
  @dataclasses.dataclass(slots=True)
29
- class ShortState(typing.Generic[EventModel]):
21
+ class ShortState[Event: BaseCute]:
30
22
  event: asyncio.Event
31
- actions: "WaiterActions[EventModel]"
23
+ actions: "WaiterActions[Event]"
32
24
 
33
25
  release: ABCRule | None = dataclasses.field(
34
26
  default=None,
@@ -46,7 +38,7 @@ class ShortState(typing.Generic[EventModel]):
46
38
 
47
39
  expiration_date: datetime.datetime | None = dataclasses.field(init=False, kw_only=True)
48
40
  creation_date: datetime.datetime = dataclasses.field(init=False)
49
- context: ShortStateContext[EventModel] | None = dataclasses.field(default=None, init=False, kw_only=True)
41
+ context: ShortStateContext[Event] | None = dataclasses.field(default=None, init=False, kw_only=True)
50
42
 
51
43
  def __post_init__(self, expiration: datetime.timedelta | None = None) -> None:
52
44
  self.creation_date = datetime.datetime.now()
@@ -54,15 +46,12 @@ class ShortState(typing.Generic[EventModel]):
54
46
 
55
47
  async def cancel(self) -> None:
56
48
  """Cancel schedule waiters."""
57
-
58
49
  waiters = typing.cast(
59
50
  typing.Iterable[asyncio.Future[typing.Any]],
60
51
  self.event._waiters, # type: ignore
61
52
  )
62
53
  for future in waiters:
63
- future.cancel()
64
- with suppress(asyncio.CancelledError):
65
- await future
54
+ await cancel_future(future)
66
55
 
67
56
 
68
57
  __all__ = ("ShortState", "ShortStateContext")