telegrinder 0.3.4__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 (192) hide show
  1. telegrinder/__init__.py +148 -149
  2. telegrinder/api/__init__.py +9 -8
  3. telegrinder/api/api.py +101 -93
  4. telegrinder/api/error.py +20 -16
  5. telegrinder/api/response.py +20 -20
  6. telegrinder/api/token.py +36 -36
  7. telegrinder/bot/__init__.py +72 -66
  8. telegrinder/bot/bot.py +83 -76
  9. telegrinder/bot/cute_types/__init__.py +19 -17
  10. telegrinder/bot/cute_types/base.py +184 -258
  11. telegrinder/bot/cute_types/callback_query.py +400 -385
  12. telegrinder/bot/cute_types/chat_join_request.py +62 -61
  13. telegrinder/bot/cute_types/chat_member_updated.py +157 -160
  14. telegrinder/bot/cute_types/inline_query.py +44 -43
  15. telegrinder/bot/cute_types/message.py +2590 -2637
  16. telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
  17. telegrinder/bot/cute_types/update.py +112 -104
  18. telegrinder/bot/cute_types/utils.py +62 -95
  19. telegrinder/bot/dispatch/__init__.py +59 -55
  20. telegrinder/bot/dispatch/abc.py +76 -77
  21. telegrinder/bot/dispatch/context.py +96 -98
  22. telegrinder/bot/dispatch/dispatch.py +254 -202
  23. telegrinder/bot/dispatch/handler/__init__.py +13 -13
  24. telegrinder/bot/dispatch/handler/abc.py +23 -24
  25. telegrinder/bot/dispatch/handler/audio_reply.py +44 -44
  26. telegrinder/bot/dispatch/handler/base.py +57 -57
  27. telegrinder/bot/dispatch/handler/document_reply.py +44 -44
  28. telegrinder/bot/dispatch/handler/func.py +129 -135
  29. telegrinder/bot/dispatch/handler/media_group_reply.py +44 -43
  30. telegrinder/bot/dispatch/handler/message_reply.py +36 -36
  31. telegrinder/bot/dispatch/handler/photo_reply.py +44 -44
  32. telegrinder/bot/dispatch/handler/sticker_reply.py +37 -37
  33. telegrinder/bot/dispatch/handler/video_reply.py +44 -44
  34. telegrinder/bot/dispatch/middleware/__init__.py +3 -3
  35. telegrinder/bot/dispatch/middleware/abc.py +97 -22
  36. telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
  37. telegrinder/bot/dispatch/process.py +151 -157
  38. telegrinder/bot/dispatch/return_manager/__init__.py +15 -13
  39. telegrinder/bot/dispatch/return_manager/abc.py +104 -108
  40. telegrinder/bot/dispatch/return_manager/callback_query.py +20 -20
  41. telegrinder/bot/dispatch/return_manager/inline_query.py +15 -15
  42. telegrinder/bot/dispatch/return_manager/message.py +36 -36
  43. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +20 -0
  44. telegrinder/bot/dispatch/view/__init__.py +15 -13
  45. telegrinder/bot/dispatch/view/abc.py +45 -41
  46. telegrinder/bot/dispatch/view/base.py +231 -200
  47. telegrinder/bot/dispatch/view/box.py +140 -129
  48. telegrinder/bot/dispatch/view/callback_query.py +16 -17
  49. telegrinder/bot/dispatch/view/chat_join_request.py +11 -16
  50. telegrinder/bot/dispatch/view/chat_member.py +37 -39
  51. telegrinder/bot/dispatch/view/inline_query.py +16 -17
  52. telegrinder/bot/dispatch/view/message.py +43 -44
  53. telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
  54. telegrinder/bot/dispatch/view/raw.py +116 -114
  55. telegrinder/bot/dispatch/waiter_machine/__init__.py +17 -17
  56. telegrinder/bot/dispatch/waiter_machine/actions.py +14 -13
  57. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +8 -8
  58. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +55 -55
  59. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +59 -57
  60. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +51 -51
  61. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +20 -19
  62. telegrinder/bot/dispatch/waiter_machine/machine.py +251 -172
  63. telegrinder/bot/dispatch/waiter_machine/middleware.py +94 -89
  64. telegrinder/bot/dispatch/waiter_machine/short_state.py +57 -68
  65. telegrinder/bot/polling/__init__.py +4 -4
  66. telegrinder/bot/polling/abc.py +25 -25
  67. telegrinder/bot/polling/polling.py +139 -131
  68. telegrinder/bot/rules/__init__.py +85 -62
  69. telegrinder/bot/rules/abc.py +213 -206
  70. telegrinder/bot/rules/callback_data.py +122 -163
  71. telegrinder/bot/rules/chat_join.py +45 -43
  72. telegrinder/bot/rules/command.py +126 -126
  73. telegrinder/bot/rules/enum_text.py +33 -36
  74. telegrinder/bot/rules/func.py +28 -26
  75. telegrinder/bot/rules/fuzzy.py +24 -24
  76. telegrinder/bot/rules/id.py +24 -0
  77. telegrinder/bot/rules/inline.py +58 -56
  78. telegrinder/bot/rules/integer.py +21 -20
  79. telegrinder/bot/rules/is_from.py +127 -127
  80. telegrinder/bot/rules/logic.py +18 -0
  81. telegrinder/bot/rules/markup.py +42 -43
  82. telegrinder/bot/rules/mention.py +14 -14
  83. telegrinder/bot/rules/message.py +15 -17
  84. telegrinder/bot/rules/message_entities.py +33 -35
  85. telegrinder/bot/rules/node.py +33 -27
  86. telegrinder/bot/rules/payload.py +81 -0
  87. telegrinder/bot/rules/payment_invoice.py +29 -0
  88. telegrinder/bot/rules/regex.py +36 -37
  89. telegrinder/bot/rules/rule_enum.py +72 -72
  90. telegrinder/bot/rules/start.py +42 -42
  91. telegrinder/bot/rules/state.py +35 -37
  92. telegrinder/bot/rules/text.py +38 -33
  93. telegrinder/bot/rules/update.py +15 -15
  94. telegrinder/bot/scenario/__init__.py +5 -5
  95. telegrinder/bot/scenario/abc.py +17 -19
  96. telegrinder/bot/scenario/checkbox.py +174 -176
  97. telegrinder/bot/scenario/choice.py +48 -51
  98. telegrinder/client/__init__.py +12 -4
  99. telegrinder/client/abc.py +100 -75
  100. telegrinder/client/aiohttp.py +134 -130
  101. telegrinder/client/form_data.py +31 -0
  102. telegrinder/client/sonic.py +212 -0
  103. telegrinder/model.py +208 -315
  104. telegrinder/modules.py +239 -237
  105. telegrinder/msgspec_json.py +14 -14
  106. telegrinder/msgspec_utils.py +478 -410
  107. telegrinder/node/__init__.py +86 -25
  108. telegrinder/node/attachment.py +163 -87
  109. telegrinder/node/base.py +288 -160
  110. telegrinder/node/callback_query.py +54 -53
  111. telegrinder/node/command.py +34 -33
  112. telegrinder/node/composer.py +163 -198
  113. telegrinder/node/container.py +33 -27
  114. telegrinder/node/either.py +82 -0
  115. telegrinder/node/event.py +54 -65
  116. telegrinder/node/file.py +51 -0
  117. telegrinder/node/me.py +15 -16
  118. telegrinder/node/payload.py +78 -0
  119. telegrinder/node/polymorphic.py +67 -48
  120. telegrinder/node/rule.py +72 -76
  121. telegrinder/node/scope.py +36 -38
  122. telegrinder/node/source.py +87 -71
  123. telegrinder/node/text.py +53 -41
  124. telegrinder/node/tools/__init__.py +3 -3
  125. telegrinder/node/tools/generator.py +36 -40
  126. telegrinder/py.typed +0 -0
  127. telegrinder/rules.py +1 -62
  128. telegrinder/tools/__init__.py +152 -93
  129. telegrinder/tools/adapter/__init__.py +19 -0
  130. telegrinder/tools/adapter/abc.py +49 -0
  131. telegrinder/tools/adapter/dataclass.py +56 -0
  132. telegrinder/{bot/rules → tools}/adapter/errors.py +5 -5
  133. telegrinder/{bot/rules → tools}/adapter/event.py +63 -65
  134. telegrinder/{bot/rules → tools}/adapter/node.py +46 -48
  135. telegrinder/{bot/rules → tools}/adapter/raw_event.py +27 -27
  136. telegrinder/{bot/rules → tools}/adapter/raw_update.py +30 -30
  137. telegrinder/tools/buttons.py +106 -80
  138. telegrinder/tools/callback_data_serilization/__init__.py +5 -0
  139. telegrinder/tools/callback_data_serilization/abc.py +51 -0
  140. telegrinder/tools/callback_data_serilization/json_ser.py +60 -0
  141. telegrinder/tools/callback_data_serilization/msgpack_ser.py +172 -0
  142. telegrinder/tools/error_handler/__init__.py +7 -7
  143. telegrinder/tools/error_handler/abc.py +30 -33
  144. telegrinder/tools/error_handler/error.py +9 -9
  145. telegrinder/tools/error_handler/error_handler.py +179 -193
  146. telegrinder/tools/formatting/__init__.py +83 -63
  147. telegrinder/tools/formatting/deep_links.py +541 -0
  148. telegrinder/tools/formatting/{html.py → html_formatter.py} +266 -294
  149. telegrinder/tools/formatting/spec_html_formats.py +71 -117
  150. telegrinder/tools/functional.py +8 -12
  151. telegrinder/tools/global_context/__init__.py +7 -7
  152. telegrinder/tools/global_context/abc.py +63 -63
  153. telegrinder/tools/global_context/global_context.py +387 -412
  154. telegrinder/tools/global_context/telegrinder_ctx.py +27 -27
  155. telegrinder/tools/i18n/__init__.py +7 -7
  156. telegrinder/tools/i18n/abc.py +30 -30
  157. telegrinder/tools/i18n/middleware/__init__.py +3 -3
  158. telegrinder/tools/i18n/middleware/abc.py +22 -25
  159. telegrinder/tools/i18n/simple.py +43 -43
  160. telegrinder/tools/input_file_directory.py +30 -0
  161. telegrinder/tools/keyboard.py +128 -128
  162. telegrinder/tools/lifespan.py +105 -0
  163. telegrinder/tools/limited_dict.py +32 -37
  164. telegrinder/tools/loop_wrapper/__init__.py +4 -4
  165. telegrinder/tools/loop_wrapper/abc.py +20 -15
  166. telegrinder/tools/loop_wrapper/loop_wrapper.py +169 -224
  167. telegrinder/tools/magic.py +307 -157
  168. telegrinder/tools/parse_mode.py +6 -6
  169. telegrinder/tools/state_storage/__init__.py +4 -4
  170. telegrinder/tools/state_storage/abc.py +31 -35
  171. telegrinder/tools/state_storage/memory.py +25 -25
  172. telegrinder/tools/strings.py +13 -0
  173. telegrinder/types/__init__.py +268 -260
  174. telegrinder/types/enums.py +711 -701
  175. telegrinder/types/input_file.py +51 -0
  176. telegrinder/types/methods.py +5055 -4633
  177. telegrinder/types/objects.py +7058 -6950
  178. telegrinder/verification_utils.py +30 -32
  179. {telegrinder-0.3.4.dist-info → telegrinder-0.4.0.dist-info}/LICENSE +22 -22
  180. telegrinder-0.4.0.dist-info/METADATA +144 -0
  181. telegrinder-0.4.0.dist-info/RECORD +182 -0
  182. {telegrinder-0.3.4.dist-info → telegrinder-0.4.0.dist-info}/WHEEL +1 -1
  183. telegrinder/bot/rules/adapter/__init__.py +0 -17
  184. telegrinder/bot/rules/adapter/abc.py +0 -31
  185. telegrinder/node/message.py +0 -14
  186. telegrinder/node/update.py +0 -15
  187. telegrinder/tools/formatting/links.py +0 -38
  188. telegrinder/tools/kb_set/__init__.py +0 -4
  189. telegrinder/tools/kb_set/base.py +0 -15
  190. telegrinder/tools/kb_set/yaml.py +0 -63
  191. telegrinder-0.3.4.dist-info/METADATA +0 -110
  192. telegrinder-0.3.4.dist-info/RECORD +0 -165
@@ -1,19 +1,20 @@
1
- from fntypes.option import Option
2
-
3
- from telegrinder.bot.dispatch.view import BaseStateView
4
- from telegrinder.bot.dispatch.waiter_machine.hasher.hasher import ECHO, Event, Hasher
5
- from telegrinder.tools.functional import from_optional
6
-
7
-
8
- class StateViewHasher(Hasher[Event, int]):
9
- view: BaseStateView[Event]
10
-
11
- def __init__(self, view: BaseStateView[Event]) -> None:
12
- self.view = view
13
- super().__init__(view.__class__, get_hash_from_data=ECHO)
14
-
15
- def get_data_from_event(self, event: Event) -> Option[int]:
16
- return from_optional(self.view.get_state_key(event))
17
-
18
-
19
- __all__ = ("StateViewHasher",)
1
+ from fntypes.option import Option
2
+
3
+ from telegrinder.bot.cute_types import BaseCute
4
+ from telegrinder.bot.dispatch.view import BaseStateView
5
+ from telegrinder.bot.dispatch.waiter_machine.hasher.hasher import ECHO, Hasher
6
+ from telegrinder.tools.functional import from_optional
7
+
8
+
9
+ class StateViewHasher[Event: BaseCute](Hasher[Event, int]):
10
+ view: BaseStateView[Event]
11
+
12
+ def __init__(self, view: BaseStateView[Event]) -> None:
13
+ self.view = view
14
+ super().__init__(view.__class__, get_hash_from_data=ECHO)
15
+
16
+ def get_data_from_event(self, event: Event) -> Option[int]:
17
+ return from_optional(self.view.get_state_key(event))
18
+
19
+
20
+ __all__ = ("StateViewHasher",)
@@ -1,172 +1,251 @@
1
- import asyncio
2
- import datetime
3
- import typing
4
-
5
- from telegrinder.bot.dispatch.abc import ABCDispatch
6
- from telegrinder.bot.dispatch.view.base import BaseStateView, BaseView
7
- from telegrinder.bot.dispatch.waiter_machine.middleware import WaiterMiddleware
8
- from telegrinder.bot.dispatch.waiter_machine.short_state import (
9
- EventModel,
10
- ShortState,
11
- ShortStateContext,
12
- )
13
- from telegrinder.bot.rules.abc import ABCRule
14
- from telegrinder.tools.limited_dict import LimitedDict
15
-
16
- from .actions import WaiterActions
17
- from .hasher import Hasher, StateViewHasher
18
-
19
- HasherData = typing.TypeVar("HasherData")
20
-
21
- Storage: typing.TypeAlias = dict[
22
- Hasher[EventModel, HasherData], LimitedDict[typing.Hashable, ShortState[EventModel]]
23
- ]
24
-
25
- WEEK: typing.Final[datetime.timedelta] = datetime.timedelta(days=7)
26
-
27
-
28
- class WaiterMachine:
29
- def __init__(
30
- self,
31
- dispatch: ABCDispatch | None = None,
32
- *,
33
- max_storage_size: int = 1000,
34
- base_state_lifetime: datetime.timedelta = WEEK,
35
- ) -> None:
36
- self.dispatch = dispatch
37
- self.max_storage_size = max_storage_size
38
- self.base_state_lifetime = base_state_lifetime
39
- self.storage: Storage = {}
40
-
41
- def __repr__(self) -> str:
42
- return "<{}: max_storage_size={}, base_state_lifetime={!r}>".format(
43
- self.__class__.__name__,
44
- self.max_storage_size,
45
- self.base_state_lifetime,
46
- )
47
-
48
- def create_middleware(self, view: BaseStateView[EventModel]) -> WaiterMiddleware[EventModel]:
49
- hasher = StateViewHasher(view)
50
- self.storage[hasher] = LimitedDict(maxlimit=self.max_storage_size)
51
- return WaiterMiddleware(self, hasher)
52
-
53
- async def drop_all(self) -> None:
54
- """Drops all waiters in storage."""
55
-
56
- for hasher in self.storage.copy():
57
- for ident, short_state in self.storage[hasher].items():
58
- if short_state.context:
59
- await self.drop(hasher, ident)
60
- else:
61
- await short_state.cancel()
62
-
63
- del self.storage[hasher]
64
-
65
- async def drop(
66
- self,
67
- hasher: Hasher[EventModel, HasherData],
68
- id: HasherData,
69
- **context: typing.Any,
70
- ) -> None:
71
- if hasher not in self.storage:
72
- raise LookupError("No record of hasher {!r} found.".format(hasher))
73
-
74
- waiter_id: typing.Hashable = hasher.get_hash_from_data(id).expect(
75
- RuntimeError("Couldn't create hash from data")
76
- )
77
- short_state = self.storage[hasher].pop(waiter_id, None)
78
- if short_state is None:
79
- raise LookupError(
80
- "Waiter with identificator {} is not found for hasher {!r}.".format(waiter_id, hasher)
81
- )
82
-
83
- if on_drop := short_state.actions.get("on_drop"):
84
- on_drop(short_state, **context)
85
-
86
- await short_state.cancel()
87
-
88
- async def wait_from_event(
89
- self,
90
- view: BaseStateView[EventModel],
91
- event: EventModel,
92
- *,
93
- filter: ABCRule | None = None,
94
- release: ABCRule | None = None,
95
- lifetime: datetime.timedelta | float | None = None,
96
- **actions: typing.Unpack[WaiterActions[EventModel]],
97
- ) -> ShortStateContext[EventModel]:
98
- hasher = StateViewHasher(view)
99
- return await self.wait(
100
- hasher=hasher,
101
- data=hasher.get_data_from_event(event).expect(
102
- RuntimeError("Hasher couldn't create data from event."),
103
- ),
104
- filter=filter,
105
- release=release,
106
- lifetime=lifetime,
107
- **actions,
108
- )
109
-
110
- async def wait(
111
- self,
112
- hasher: Hasher[EventModel, HasherData],
113
- data: HasherData,
114
- *,
115
- filter: ABCRule | None = None,
116
- release: ABCRule | None = None,
117
- lifetime: datetime.timedelta | float | None = None,
118
- **actions: typing.Unpack[WaiterActions[EventModel]],
119
- ) -> ShortStateContext[EventModel]:
120
- if isinstance(lifetime, int | float):
121
- lifetime = datetime.timedelta(seconds=lifetime)
122
-
123
- event = asyncio.Event()
124
- short_state = ShortState[EventModel](
125
- event,
126
- actions,
127
- release=release,
128
- filter=filter,
129
- lifetime=lifetime or self.base_state_lifetime,
130
- )
131
- waiter_hash = hasher.get_hash_from_data(data).expect(RuntimeError("Hasher couldn't create hash."))
132
-
133
- if hasher not in self.storage:
134
- 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.")
137
- )
138
- view.middlewares.insert(0, WaiterMiddleware(self, hasher))
139
- self.storage[hasher] = LimitedDict(maxlimit=self.max_storage_size)
140
-
141
- if (deleted_short_state := self.storage[hasher].set(waiter_hash, short_state)) is not None:
142
- await deleted_short_state.cancel()
143
-
144
- await event.wait()
145
- self.storage[hasher].pop(waiter_hash, None)
146
- assert short_state.context is not None
147
- return short_state.context
148
-
149
- async def clear_storage(self) -> None:
150
- """Clears storage."""
151
-
152
- for hasher in self.storage:
153
- now = datetime.datetime.now()
154
- for ident, short_state in self.storage.get(hasher, {}).copy().items():
155
- 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
- )
161
-
162
-
163
- async def clear_wm_storage_worker(
164
- wm: WaiterMachine,
165
- interval_seconds: int = 60,
166
- ) -> typing.NoReturn:
167
- while True:
168
- await wm.clear_storage()
169
- await asyncio.sleep(interval_seconds)
170
-
171
-
172
- __all__ = ("WaiterMachine",)
1
+ import asyncio
2
+ import datetime
3
+ import typing
4
+
5
+ from telegrinder.bot.cute_types.base import BaseCute
6
+ from telegrinder.bot.dispatch.abc import ABCDispatch
7
+ from telegrinder.bot.dispatch.context import Context
8
+ from telegrinder.bot.dispatch.view.base import BaseStateView, BaseView
9
+ from telegrinder.bot.dispatch.waiter_machine.middleware import INITIATOR_CONTEXT_KEY, WaiterMiddleware
10
+ from telegrinder.bot.dispatch.waiter_machine.short_state import (
11
+ ShortState,
12
+ ShortStateContext,
13
+ )
14
+ from telegrinder.bot.rules.abc import ABCRule
15
+ from telegrinder.tools.lifespan import Lifespan
16
+ from telegrinder.tools.limited_dict import LimitedDict
17
+
18
+ from .actions import WaiterActions
19
+ from .hasher import Hasher, StateViewHasher
20
+
21
+ type Storage[Event: BaseCute, HasherData] = dict[
22
+ Hasher[Event, HasherData],
23
+ LimitedDict[typing.Hashable, ShortState[Event]],
24
+ ]
25
+ type HasherWithData[Event: BaseCute, Data] = tuple[Hasher[Event, Data], Data]
26
+
27
+ WEEK: typing.Final[datetime.timedelta] = datetime.timedelta(days=7)
28
+
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
+
44
+ class WaiterMachine:
45
+ def __init__(
46
+ self,
47
+ dispatch: ABCDispatch | None = None,
48
+ *,
49
+ max_storage_size: int = 1000,
50
+ base_state_lifetime: datetime.timedelta = WEEK,
51
+ ) -> None:
52
+ self.dispatch = dispatch
53
+ self.max_storage_size = max_storage_size
54
+ self.base_state_lifetime = base_state_lifetime
55
+ self.storage: Storage = {}
56
+
57
+ def __repr__(self) -> str:
58
+ return "<{}: with {} storage items and max_storage_size={}, base_state_lifetime={!r}>".format(
59
+ self.__class__.__name__,
60
+ len(self.storage),
61
+ self.max_storage_size,
62
+ self.base_state_lifetime,
63
+ )
64
+
65
+ def create_middleware[Event: BaseCute](self, view: BaseStateView[Event]) -> WaiterMiddleware[Event]:
66
+ hasher = StateViewHasher(view)
67
+ self.storage[hasher] = LimitedDict(maxlimit=self.max_storage_size)
68
+ return WaiterMiddleware(self, hasher)
69
+
70
+ async def drop_all(self) -> None:
71
+ """Drops all waiters in storage."""
72
+ for hasher in self.storage.copy():
73
+ for ident, short_state in self.storage[hasher].items():
74
+ if short_state.context:
75
+ await self.drop(hasher, ident)
76
+ else:
77
+ await short_state.cancel()
78
+
79
+ del self.storage[hasher]
80
+
81
+ async def drop[Event: BaseCute, HasherData](
82
+ self,
83
+ hasher: Hasher[Event, HasherData],
84
+ data: HasherData,
85
+ **context: typing.Any,
86
+ ) -> None:
87
+ if hasher not in self.storage:
88
+ raise LookupError("No record of hasher {!r} found.".format(hasher))
89
+
90
+ waiter_id: typing.Hashable = hasher.get_hash_from_data(data).expect(
91
+ RuntimeError("Couldn't create hash from data"),
92
+ )
93
+ short_state = self.storage[hasher].pop(waiter_id, None)
94
+ if short_state is None:
95
+ raise LookupError(
96
+ "Waiter with identificator {} is not found for hasher {!r}.".format(waiter_id, hasher)
97
+ )
98
+
99
+ if on_drop := short_state.actions.get("on_drop"):
100
+ on_drop(short_state, **context)
101
+
102
+ await short_state.cancel()
103
+
104
+ async def wait_from_event[Event: BaseCute](
105
+ self,
106
+ view: BaseStateView[Event],
107
+ event: Event,
108
+ *,
109
+ filter: ABCRule | None = None,
110
+ release: ABCRule | None = None,
111
+ lifetime: datetime.timedelta | float | None = None,
112
+ lifespan: Lifespan | None = None,
113
+ **actions: typing.Unpack[WaiterActions[Event]],
114
+ ) -> ShortStateContext[Event]:
115
+ hasher = StateViewHasher(view)
116
+ return await self.wait(
117
+ hasher=hasher,
118
+ data=hasher.get_data_from_event(event).expect(
119
+ RuntimeError("Hasher couldn't create data from event."),
120
+ ),
121
+ filter=filter,
122
+ release=release,
123
+ lifetime=lifetime,
124
+ lifespan=lifespan or Lifespan(),
125
+ **actions,
126
+ )
127
+
128
+ async def wait[Event: BaseCute, HasherData](
129
+ self,
130
+ hasher: Hasher[Event, HasherData],
131
+ data: HasherData,
132
+ *,
133
+ filter: ABCRule | None = None,
134
+ release: ABCRule | None = None,
135
+ lifetime: datetime.timedelta | float | None = None,
136
+ lifespan: Lifespan | None = None,
137
+ **actions: typing.Unpack[WaiterActions[Event]],
138
+ ) -> ShortStateContext[Event]:
139
+ if isinstance(lifetime, int | float):
140
+ lifetime = datetime.timedelta(seconds=lifetime)
141
+
142
+ lifespan = lifespan or Lifespan()
143
+ event = asyncio.Event()
144
+ short_state = ShortState[Event](
145
+ event,
146
+ actions,
147
+ release=release,
148
+ filter=filter,
149
+ lifetime=lifetime or self.base_state_lifetime,
150
+ )
151
+ waiter_hash = hasher.get_hash_from_data(data).expect(RuntimeError("Hasher couldn't create hash."))
152
+
153
+ if hasher not in self.storage:
154
+ if self.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."),
157
+ )
158
+ view.middlewares.insert(0, WaiterMiddleware(self, hasher))
159
+ self.storage[hasher] = LimitedDict(maxlimit=self.max_storage_size)
160
+
161
+ if (deleted_short_state := self.storage[hasher].set(waiter_hash, short_state)) is not None:
162
+ await deleted_short_state.cancel()
163
+
164
+ async with lifespan:
165
+ await event.wait()
166
+
167
+ self.storage[hasher].pop(waiter_hash, None)
168
+
169
+ if short_state.context is None:
170
+ raise LookupError("No context in short_state.")
171
+ return short_state.context
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
+
232
+ async def clear_storage(self) -> None:
233
+ """Clears storage."""
234
+ now = datetime.datetime.now()
235
+
236
+ for hasher in self.storage:
237
+ for ident, short_state in self.storage.get(hasher, {}).copy().items():
238
+ if short_state.expiration_date is not None and now > short_state.expiration_date:
239
+ await self.drop(hasher, data=ident, force=True)
240
+
241
+
242
+ async def clear_wm_storage_worker(
243
+ wm: WaiterMachine,
244
+ interval_seconds: int = 60,
245
+ ) -> typing.NoReturn:
246
+ while True:
247
+ await wm.clear_storage()
248
+ await asyncio.sleep(interval_seconds)
249
+
250
+
251
+ __all__ = ("WaiterMachine",)
@@ -1,89 +1,94 @@
1
- import datetime
2
- import typing
3
-
4
- from telegrinder.bot.cute_types.base import BaseCute
5
- from telegrinder.bot.dispatch.context import Context
6
- from telegrinder.bot.dispatch.handler.func import FuncHandler
7
- from telegrinder.bot.dispatch.middleware.abc import ABCMiddleware
8
- from telegrinder.bot.dispatch.process import check_rule
9
- from telegrinder.bot.dispatch.waiter_machine.short_state import ShortStateContext
10
- from telegrinder.modules import logger
11
-
12
- from .hasher import Hasher
13
-
14
- if typing.TYPE_CHECKING:
15
- from .machine import WaiterMachine
16
- from .short_state import ShortState
17
-
18
- EventType = typing.TypeVar("EventType", bound=BaseCute)
19
-
20
-
21
- class WaiterMiddleware(ABCMiddleware[EventType]):
22
- def __init__(
23
- self,
24
- machine: "WaiterMachine",
25
- hasher: Hasher,
26
- ) -> None:
27
- self.machine = machine
28
- self.hasher = hasher
29
-
30
- async def pre(self, event: EventType, ctx: Context) -> bool:
31
- if self.hasher not in self.machine.storage:
32
- return True
33
-
34
- 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}")
37
- return True
38
-
39
- short_state: "ShortState[EventType] | None" = self.machine.storage[self.hasher].get(key.unwrap())
40
- if not short_state:
41
- return True
42
-
43
- preset_context = Context(short_state=short_state)
44
- if short_state.context is not None:
45
- preset_context.update(short_state.context.context)
46
-
47
- # Run filter rule
48
- if short_state.filter and not await check_rule(
49
- event.ctx_api, short_state.filter, ctx.raw_update, preset_context
50
- ):
51
- logger.debug("Filter rule {!r} failed", short_state.filter)
52
- return True
53
-
54
- if short_state.expiration_date is not None and datetime.datetime.now() >= short_state.expiration_date:
55
- await self.machine.drop(
56
- self.hasher,
57
- self.hasher.get_data_from_event(event).unwrap(),
58
- **preset_context.copy(),
59
- )
60
- return True
61
-
62
- handler = FuncHandler(
63
- self.pass_runtime,
64
- [short_state.release] if short_state.release else [],
65
- dataclass=None,
66
- preset_context=preset_context,
67
- )
68
- result = await handler.check(event.ctx_api, ctx.raw_update, ctx)
69
-
70
- if result is True:
71
- await handler.run(event.api, event, ctx)
72
-
73
- elif on_miss := short_state.actions.get("on_miss"): # noqa: SIM102
74
- if await on_miss.check(event.ctx_api, ctx.raw_update, ctx):
75
- await on_miss.run(event.ctx_api, event, ctx)
76
-
77
- return False
78
-
79
- async def pass_runtime(
80
- self,
81
- event: EventType,
82
- short_state: "ShortState[EventType]",
83
- ctx: Context,
84
- ) -> None:
85
- short_state.context = ShortStateContext(event, ctx)
86
- short_state.event.set()
87
-
88
-
89
- __all__ = ("WaiterMiddleware",)
1
+ import datetime
2
+ import typing
3
+
4
+ from telegrinder.bot.cute_types.base import BaseCute
5
+ from telegrinder.bot.dispatch.context import Context
6
+ from telegrinder.bot.dispatch.handler.func import FuncHandler
7
+ from telegrinder.bot.dispatch.middleware.abc import ABCMiddleware
8
+ from telegrinder.bot.dispatch.process import check_rule
9
+ from telegrinder.bot.dispatch.waiter_machine.short_state import ShortStateContext
10
+ from telegrinder.modules import logger
11
+
12
+ from .hasher import Hasher
13
+
14
+ if typing.TYPE_CHECKING:
15
+ from .machine import WaiterMachine
16
+ from .short_state import ShortState
17
+
18
+
19
+ INITIATOR_CONTEXT_KEY = "initiator"
20
+
21
+
22
+ class WaiterMiddleware[Event: BaseCute](ABCMiddleware[Event]):
23
+ def __init__(
24
+ self,
25
+ machine: "WaiterMachine",
26
+ hasher: Hasher,
27
+ ) -> None:
28
+ self.machine = machine
29
+ self.hasher = hasher
30
+
31
+ async def pre(self, event: Event, ctx: Context) -> bool:
32
+ if self.hasher not in self.machine.storage:
33
+ return True
34
+
35
+ key = self.hasher.get_hash_from_data_from_event(event)
36
+ if not key:
37
+ logger.info(f"Unable to get hash from event with hasher {self.hasher!r}")
38
+ return True
39
+
40
+ short_state: "ShortState[Event] | None" = self.machine.storage[self.hasher].get(key.unwrap())
41
+ if not short_state:
42
+ return True
43
+
44
+ preset_context = Context(short_state=short_state)
45
+ if short_state.context is not None:
46
+ preset_context.update(short_state.context.context)
47
+
48
+ # Run filter rule
49
+ if short_state.filter and not await check_rule(
50
+ event.ctx_api,
51
+ short_state.filter,
52
+ ctx.raw_update,
53
+ preset_context,
54
+ ):
55
+ logger.debug("Filter rule {!r} failed", short_state.filter)
56
+ return True
57
+
58
+ if short_state.expiration_date is not None and datetime.datetime.now() >= short_state.expiration_date:
59
+ await self.machine.drop(
60
+ self.hasher,
61
+ self.hasher.get_data_from_event(event).unwrap(),
62
+ **preset_context.copy(),
63
+ )
64
+ return True
65
+
66
+ handler = FuncHandler(
67
+ self.pass_runtime,
68
+ [short_state.release] if short_state.release else [],
69
+ preset_context=preset_context,
70
+ )
71
+ handler.get_name_event_param = lambda event: "event" # FIXME: HOTFIX
72
+ result = await handler.check(event.ctx_api, ctx.raw_update, ctx)
73
+
74
+ if result is True:
75
+ await handler.run(event.api, event, ctx)
76
+
77
+ elif on_miss := short_state.actions.get("on_miss"): # noqa: SIM102
78
+ if await on_miss.check(event.ctx_api, ctx.raw_update, ctx):
79
+ await on_miss.run(event.ctx_api, event, ctx)
80
+
81
+ return False
82
+
83
+ async def pass_runtime(
84
+ self,
85
+ event: Event,
86
+ short_state: "ShortState[Event]",
87
+ ctx: Context,
88
+ ) -> None:
89
+ ctx.initiator = self.hasher
90
+ short_state.context = ShortStateContext(event, ctx)
91
+ short_state.event.set()
92
+
93
+
94
+ __all__ = ("WaiterMiddleware",)