telegrinder 0.2.1__py3-none-any.whl → 0.3.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 (85) hide show
  1. telegrinder/__init__.py +45 -12
  2. telegrinder/bot/__init__.py +32 -4
  3. telegrinder/bot/cute_types/callback_query.py +60 -146
  4. telegrinder/bot/cute_types/chat_join_request.py +12 -16
  5. telegrinder/bot/cute_types/chat_member_updated.py +15 -105
  6. telegrinder/bot/cute_types/inline_query.py +5 -14
  7. telegrinder/bot/cute_types/message.py +623 -1238
  8. telegrinder/bot/dispatch/__init__.py +40 -3
  9. telegrinder/bot/dispatch/abc.py +8 -1
  10. telegrinder/bot/dispatch/context.py +2 -2
  11. telegrinder/bot/dispatch/dispatch.py +8 -1
  12. telegrinder/bot/dispatch/handler/__init__.py +17 -1
  13. telegrinder/bot/dispatch/handler/audio_reply.py +44 -0
  14. telegrinder/bot/dispatch/handler/base.py +57 -0
  15. telegrinder/bot/dispatch/handler/document_reply.py +44 -0
  16. telegrinder/bot/dispatch/handler/func.py +3 -3
  17. telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
  18. telegrinder/bot/dispatch/handler/message_reply.py +12 -35
  19. telegrinder/bot/dispatch/handler/photo_reply.py +44 -0
  20. telegrinder/bot/dispatch/handler/sticker_reply.py +37 -0
  21. telegrinder/bot/dispatch/handler/video_reply.py +44 -0
  22. telegrinder/bot/dispatch/process.py +2 -2
  23. telegrinder/bot/dispatch/return_manager/abc.py +11 -8
  24. telegrinder/bot/dispatch/return_manager/callback_query.py +2 -2
  25. telegrinder/bot/dispatch/return_manager/inline_query.py +2 -2
  26. telegrinder/bot/dispatch/return_manager/message.py +3 -3
  27. telegrinder/bot/dispatch/view/__init__.py +2 -1
  28. telegrinder/bot/dispatch/view/abc.py +2 -181
  29. telegrinder/bot/dispatch/view/base.py +200 -0
  30. telegrinder/bot/dispatch/view/callback_query.py +3 -3
  31. telegrinder/bot/dispatch/view/chat_join_request.py +2 -2
  32. telegrinder/bot/dispatch/view/chat_member.py +2 -3
  33. telegrinder/bot/dispatch/view/inline_query.py +2 -2
  34. telegrinder/bot/dispatch/view/message.py +5 -4
  35. telegrinder/bot/dispatch/view/raw.py +4 -3
  36. telegrinder/bot/dispatch/waiter_machine/__init__.py +18 -0
  37. telegrinder/bot/dispatch/waiter_machine/actions.py +10 -0
  38. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +15 -0
  39. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +60 -0
  40. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +49 -0
  41. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +54 -0
  42. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +19 -0
  43. telegrinder/bot/dispatch/waiter_machine/machine.py +88 -101
  44. telegrinder/bot/dispatch/waiter_machine/middleware.py +23 -41
  45. telegrinder/bot/dispatch/waiter_machine/short_state.py +9 -9
  46. telegrinder/bot/polling/polling.py +5 -2
  47. telegrinder/bot/rules/__init__.py +3 -3
  48. telegrinder/bot/rules/abc.py +6 -5
  49. telegrinder/bot/rules/adapter/__init__.py +1 -1
  50. telegrinder/bot/rules/integer.py +1 -1
  51. telegrinder/bot/rules/is_from.py +19 -0
  52. telegrinder/bot/rules/state.py +9 -6
  53. telegrinder/bot/scenario/checkbox.py +5 -5
  54. telegrinder/bot/scenario/choice.py +2 -2
  55. telegrinder/client/aiohttp.py +5 -7
  56. telegrinder/model.py +6 -11
  57. telegrinder/modules.py +16 -25
  58. telegrinder/msgspec_json.py +1 -1
  59. telegrinder/msgspec_utils.py +56 -5
  60. telegrinder/node/base.py +2 -2
  61. telegrinder/node/composer.py +5 -9
  62. telegrinder/node/container.py +6 -1
  63. telegrinder/node/event.py +2 -0
  64. telegrinder/node/polymorphic.py +7 -7
  65. telegrinder/node/rule.py +6 -4
  66. telegrinder/node/scope.py +3 -3
  67. telegrinder/node/source.py +4 -2
  68. telegrinder/node/tools/generator.py +7 -6
  69. telegrinder/rules.py +2 -2
  70. telegrinder/tools/__init__.py +10 -10
  71. telegrinder/tools/functional.py +9 -0
  72. telegrinder/tools/keyboard.py +6 -1
  73. telegrinder/tools/loop_wrapper/loop_wrapper.py +4 -5
  74. telegrinder/tools/magic.py +17 -19
  75. telegrinder/tools/state_storage/__init__.py +3 -3
  76. telegrinder/tools/state_storage/abc.py +12 -10
  77. telegrinder/tools/state_storage/memory.py +9 -6
  78. telegrinder/types/__init__.py +1 -0
  79. telegrinder/types/methods.py +10 -2
  80. telegrinder/types/objects.py +47 -5
  81. {telegrinder-0.2.1.dist-info → telegrinder-0.3.0.dist-info}/METADATA +4 -5
  82. telegrinder-0.3.0.dist-info/RECORD +164 -0
  83. telegrinder-0.2.1.dist-info/RECORD +0 -149
  84. {telegrinder-0.2.1.dist-info → telegrinder-0.3.0.dist-info}/LICENSE +0 -0
  85. {telegrinder-0.2.1.dist-info → telegrinder-0.3.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,49 @@
1
+ import typing
2
+
3
+ from fntypes import Option
4
+
5
+ from telegrinder.bot.cute_types import BaseCute
6
+ from telegrinder.bot.dispatch.view.base import BaseView
7
+ from telegrinder.tools.functional import from_optional
8
+
9
+ Event = typing.TypeVar("Event", bound=BaseCute)
10
+ Data = typing.TypeVar("Data")
11
+
12
+ ECHO = lambda x: x
13
+
14
+
15
+ class Hasher(typing.Generic[Event, Data]):
16
+ def __init__(
17
+ self,
18
+ view: type[BaseView[Event]],
19
+ get_hash_from_data: typing.Callable[[Data], typing.Hashable | None] | None = None,
20
+ get_data_from_event: typing.Callable[[Event], Data | None] | None = None,
21
+ ):
22
+ self.view = view
23
+ self._get_hash_from_data = get_hash_from_data
24
+ self._get_data_from_event = get_data_from_event
25
+
26
+ def get_name(self) -> str:
27
+ return f"{self.view.__class__.__name__}_{id(self)}"
28
+
29
+ def get_hash_from_data(self, data: Data) -> Option[typing.Hashable]:
30
+ if self._get_hash_from_data is None:
31
+ raise NotImplementedError
32
+ return from_optional(self._get_hash_from_data(data))
33
+
34
+ def get_data_from_event(self, event: Event) -> Option[Data]:
35
+ if not self._get_data_from_event:
36
+ raise NotImplementedError
37
+ return from_optional(self._get_data_from_event(event))
38
+
39
+ def get_hash_from_data_from_event(self, event: Event) -> Option[typing.Hashable]:
40
+ return self.get_data_from_event(event).and_then(self.get_hash_from_data) # type: ignore
41
+
42
+ def __hash__(self) -> int:
43
+ return hash(self.get_name())
44
+
45
+ def __repr__(self) -> str:
46
+ return f"<Hasher {self.get_name()}>"
47
+
48
+
49
+ __all__ = ("Hasher",)
@@ -0,0 +1,54 @@
1
+ from telegrinder.bot.cute_types import MessageCute as Message
2
+ from telegrinder.bot.dispatch.view import MessageView
3
+
4
+ from .hasher import Hasher
5
+
6
+
7
+ def from_chat_hash(chat_id: int) -> int:
8
+ return chat_id
9
+
10
+
11
+ def get_chat_from_event(event: Message) -> int:
12
+ return event.chat.id
13
+
14
+
15
+ MESSAGE_IN_CHAT = Hasher(
16
+ view=MessageView, get_hash_from_data=from_chat_hash, get_data_from_event=get_chat_from_event
17
+ )
18
+
19
+
20
+ def from_user_hash(from_id: int) -> int:
21
+ return from_id
22
+
23
+
24
+ def get_user_from_event(event: Message) -> int:
25
+ return event.from_user.id
26
+
27
+
28
+ MESSAGE_FROM_USER = Hasher(
29
+ view=MessageView,
30
+ get_hash_from_data=from_user_hash,
31
+ get_data_from_event=get_user_from_event,
32
+ )
33
+
34
+
35
+ def from_user_in_chat_hash(chat_and_user: tuple[int, int]) -> str:
36
+ return f"{chat_and_user[0]}_{chat_and_user[1]}"
37
+
38
+
39
+ def get_user_in_chat_from_event(event: Message) -> tuple[int, int]:
40
+ return event.chat.id, event.from_user.id
41
+
42
+
43
+ MESSAGE_FROM_USER_IN_CHAT = Hasher(
44
+ view=MessageView,
45
+ get_hash_from_data=from_user_in_chat_hash,
46
+ get_data_from_event=get_user_in_chat_from_event,
47
+ )
48
+
49
+
50
+ __all__ = (
51
+ "MESSAGE_FROM_USER",
52
+ "MESSAGE_FROM_USER_IN_CHAT",
53
+ "MESSAGE_IN_CHAT",
54
+ )
@@ -0,0 +1,19 @@
1
+ from fntypes import Option
2
+
3
+ from telegrinder.bot.dispatch.view import BaseStateView
4
+ from telegrinder.tools.functional import from_optional
5
+
6
+ from .hasher import ECHO, Event, Hasher
7
+
8
+
9
+ class StateViewHasher(Hasher[Event, int]):
10
+ view: BaseStateView
11
+
12
+ def __init__(self, view: type[BaseStateView[Event]]):
13
+ super().__init__(view, 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",)
@@ -2,34 +2,42 @@ import asyncio
2
2
  import datetime
3
3
  import typing
4
4
 
5
- from telegrinder.api import API
6
- from telegrinder.bot.dispatch.context import Context
7
- from telegrinder.bot.dispatch.view.abc import ABCStateView, BaseStateView
5
+ from telegrinder.bot.dispatch.abc import ABCDispatch
6
+ from telegrinder.bot.dispatch.view.base import BaseStateView, BaseView
8
7
  from telegrinder.bot.dispatch.waiter_machine.middleware import WaiterMiddleware
9
8
  from telegrinder.bot.dispatch.waiter_machine.short_state import (
10
- Behaviour,
11
9
  EventModel,
12
10
  ShortState,
13
11
  ShortStateContext,
14
12
  )
15
13
  from telegrinder.bot.rules.abc import ABCRule
16
14
  from telegrinder.tools.limited_dict import LimitedDict
17
- from telegrinder.types import Update
18
15
 
19
- if typing.TYPE_CHECKING:
20
- from telegrinder.bot.dispatch import Dispatch
16
+ from .actions import WaiterActions
17
+ from .hasher import Hasher, StateViewHasher
21
18
 
22
19
  T = typing.TypeVar("T")
20
+ HasherData = typing.TypeVar("HasherData")
23
21
 
24
- Identificator: typing.TypeAlias = str | int
25
- Storage: typing.TypeAlias = dict[str, LimitedDict[Identificator, ShortState[EventModel]]]
22
+
23
+ Storage: typing.TypeAlias = dict[
24
+ Hasher[EventModel, HasherData], LimitedDict[typing.Hashable, ShortState[EventModel]]
25
+ ]
26
26
 
27
27
  WEEK: typing.Final[datetime.timedelta] = datetime.timedelta(days=7)
28
28
 
29
29
 
30
30
  class WaiterMachine:
31
- def __init__(self, *, max_storage_size: int = 1000) -> None:
31
+ def __init__(
32
+ self,
33
+ dispatch: ABCDispatch | None = None,
34
+ *,
35
+ max_storage_size: int = 1000,
36
+ base_state_lifetime: datetime.timedelta = WEEK,
37
+ ) -> None:
38
+ self.dispatch = dispatch
32
39
  self.max_storage_size = max_storage_size
40
+ self.base_state_lifetime = base_state_lifetime
33
41
  self.storage: Storage = {}
34
42
 
35
43
  def __repr__(self) -> str:
@@ -45,144 +53,123 @@ class WaiterMachine:
45
53
 
46
54
  async def drop(
47
55
  self,
48
- state_view: "ABCStateView[EventModel] | str",
49
- id: Identificator,
50
- event: EventModel,
51
- update: Update,
56
+ hasher: Hasher[EventModel, HasherData],
57
+ id: HasherData,
52
58
  **context: typing.Any,
53
59
  ) -> None:
54
- view_name = state_view if isinstance(state_view, str) else state_view.__class__.__name__
55
- if view_name not in self.storage:
56
- raise LookupError("No record of view {!r} found.".format(view_name))
60
+ if hasher not in self.storage:
61
+ raise LookupError("No record of hasher {!r} found.".format(hasher))
57
62
 
58
- short_state = self.storage[view_name].pop(id, None)
63
+ waiter_id: typing.Hashable = hasher.get_hash_from_data(id).expect(
64
+ RuntimeError("Couldn't create hash from data")
65
+ )
66
+ short_state = self.storage[hasher].pop(waiter_id, None)
59
67
  if short_state is None:
60
- raise LookupError("Waiter with identificator {} is not found for view {!r}".format(id, view_name))
68
+ raise LookupError(
69
+ "Waiter with identificator {} is not found for hasher {!r}".format(waiter_id, hasher)
70
+ )
71
+
72
+ if on_drop := short_state.actions.get("on_drop"):
73
+ on_drop(short_state, **context)
61
74
 
62
75
  short_state.cancel()
63
- await self.call_behaviour(
64
- event,
65
- update,
66
- behaviour=short_state.on_drop_behaviour,
67
- **context,
68
- )
69
76
 
70
77
  async def drop_all(self) -> None:
71
- """Drops all waiters in storage"""
72
- for view_name in self.storage:
73
- for ident, short_state in self.storage[view_name].items():
78
+ """Drops all waiters in storage."""
79
+
80
+ for hasher in self.storage:
81
+ for ident, short_state in self.storage[hasher].items():
74
82
  if short_state.context:
75
83
  await self.drop(
76
- view_name,
84
+ hasher,
77
85
  ident,
78
- short_state.context.event,
79
- short_state.context.context.raw_update
80
86
  )
81
87
  else:
82
88
  short_state.cancel()
83
89
 
84
- async def wait(
90
+ async def wait_from_event(
85
91
  self,
86
- state_view: "BaseStateView[EventModel]",
87
- linked: EventModel | tuple[API, Identificator],
88
- *rules: ABCRule,
89
- default: Behaviour[EventModel] | None = None,
90
- on_drop: Behaviour[EventModel] | None = None,
91
- exit: Behaviour[EventModel] | None = None,
92
- expiration: datetime.timedelta | float | None = None,
92
+ view: BaseStateView[EventModel],
93
+ event: EventModel,
94
+ *,
95
+ filter: ABCRule | None = None,
96
+ release: ABCRule | None = None,
97
+ lifetime: datetime.timedelta | float | None = None,
98
+ **actions: typing.Unpack[WaiterActions],
93
99
  ) -> ShortStateContext[EventModel]:
94
- if isinstance(expiration, int | float):
95
- expiration = datetime.timedelta(seconds=expiration)
100
+ hasher = StateViewHasher(view.__class__)
101
+ return await self.wait(
102
+ hasher=hasher,
103
+ data=hasher.get_data_from_event(event).expect(
104
+ RuntimeError("Hasher couldn't create data from event.")
105
+ ),
106
+ filter=filter,
107
+ release=release,
108
+ lifetime=lifetime,
109
+ **actions,
110
+ )
96
111
 
97
- api: API
98
- key: Identificator
99
- api, key = linked if isinstance(linked, tuple) else (linked.ctx_api, state_view.get_state_key(linked)) # type: ignore
100
- if not key:
101
- raise RuntimeError("Unable to get state key.")
112
+ async def wait(
113
+ self,
114
+ hasher: Hasher[EventModel, HasherData],
115
+ data: HasherData,
116
+ *,
117
+ filter: ABCRule | None = None,
118
+ release: ABCRule | None = None,
119
+ lifetime: datetime.timedelta | float | None = None,
120
+ **actions: typing.Unpack[WaiterActions],
121
+ ) -> ShortStateContext[EventModel]:
122
+ if isinstance(lifetime, int | float):
123
+ lifetime = datetime.timedelta(seconds=lifetime)
102
124
 
103
- view_name = state_view.__class__.__name__
104
125
  event = asyncio.Event()
105
126
  short_state = ShortState[EventModel](
106
- key,
107
- api,
108
- event,
109
- rules,
110
- expiration=expiration,
111
- default_behaviour=default,
112
- on_drop_behaviour=on_drop,
113
- exit_behaviour=exit,
127
+ filter=filter,
128
+ release=release,
129
+ event=event,
130
+ lifetime=lifetime or self.base_state_lifetime,
131
+ actions=actions,
114
132
  )
133
+ waiter_hash = hasher.get_hash_from_data(data).expect(RuntimeError("Hasher couldn't create hash."))
115
134
 
116
- if view_name not in self.storage:
117
- state_view.middlewares.insert(0, WaiterMiddleware(self, state_view))
118
- self.storage[view_name] = LimitedDict(maxlimit=self.max_storage_size)
135
+ if hasher not in self.storage:
136
+ if self.dispatch:
137
+ view: BaseView[EventModel] = self.dispatch.get_view(hasher.view).expect(
138
+ RuntimeError(f"View {hasher.view.__name__} is not defined in dispatch")
139
+ )
140
+ view.middlewares.insert(0, WaiterMiddleware(self, hasher))
141
+ self.storage[hasher] = LimitedDict(maxlimit=self.max_storage_size)
119
142
 
120
- if (deleted_short_state := self.storage[view_name].set(key, short_state)) is not None:
143
+ if (deleted_short_state := self.storage[hasher].set(waiter_hash, short_state)) is not None:
121
144
  deleted_short_state.cancel()
122
145
 
123
146
  await event.wait()
124
- self.storage[view_name].pop(key, None)
147
+ self.storage[hasher].pop(waiter_hash, None)
125
148
  assert short_state.context is not None
126
149
  return short_state.context
127
150
 
128
- async def call_behaviour(
129
- self,
130
- event: EventModel,
131
- update: Update,
132
- behaviour: Behaviour[EventModel] | None = None,
133
- **context: typing.Any,
134
- ) -> bool:
135
- # TODO: support view as a behaviour
136
-
137
- if behaviour is None:
138
- return False
139
-
140
- ctx = Context(**context)
141
- if await behaviour.check(event.api, update, ctx):
142
- await behaviour.run(event.api, event, ctx)
143
- return True
144
-
145
- return False
146
-
147
151
  async def clear_storage(
148
152
  self,
149
- views: typing.Iterable[ABCStateView[EventModel]],
150
- absolutely_dead_time: datetime.timedelta = WEEK,
151
153
  ) -> None:
152
- """Clears storage.
154
+ """Clears storage."""
153
155
 
154
- :param absolutely_dead_time: timedelta when state can be forgotten.
155
- """
156
-
157
- for view in views:
158
- view_name = view.__class__.__name__
156
+ for hasher in self.storage:
159
157
  now = datetime.datetime.now()
160
- for ident, short_state in self.storage.get(view_name, {}).copy().items():
158
+ for ident, short_state in self.storage.get(hasher, {}).copy().items():
161
159
  if short_state.expiration_date is not None and now > short_state.expiration_date:
162
- assert short_state.context # FIXME: why???
163
160
  await self.drop(
164
- view,
161
+ hasher,
165
162
  ident,
166
- event=short_state.context.event,
167
- update=short_state.context.context.raw_update,
168
163
  force=True,
169
164
  )
170
- elif short_state.creation_date + absolutely_dead_time < now:
171
- short_state.cancel()
172
- del self.storage[view_name][short_state.key]
173
165
 
174
166
 
175
167
  async def clear_wm_storage_worker(
176
168
  wm: WaiterMachine,
177
- dp: "Dispatch",
178
169
  interval_seconds: int = 60,
179
- absolutely_dead_time: datetime.timedelta = WEEK,
180
170
  ) -> typing.NoReturn:
181
171
  while True:
182
- await wm.clear_storage(
183
- views=[view for view in dp.get_views().values() if isinstance(view, ABCStateView)],
184
- absolutely_dead_time=absolutely_dead_time,
185
- )
172
+ await wm.clear_storage()
186
173
  await asyncio.sleep(interval_seconds)
187
174
 
188
175
 
@@ -5,8 +5,11 @@ from telegrinder.bot.cute_types.base import BaseCute
5
5
  from telegrinder.bot.dispatch.context import Context
6
6
  from telegrinder.bot.dispatch.handler.func import FuncHandler
7
7
  from telegrinder.bot.dispatch.middleware.abc import ABCMiddleware
8
- from telegrinder.bot.dispatch.view.abc import ABCStateView
8
+ from telegrinder.bot.dispatch.process import check_rule
9
9
  from telegrinder.bot.dispatch.waiter_machine.short_state import ShortStateContext
10
+ from telegrinder.modules import logger
11
+
12
+ from .hasher import Hasher
10
13
 
11
14
  if typing.TYPE_CHECKING:
12
15
  from .machine import WaiterMachine
@@ -19,27 +22,21 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
19
22
  def __init__(
20
23
  self,
21
24
  machine: "WaiterMachine",
22
- view: ABCStateView[EventType],
25
+ hasher: Hasher,
23
26
  ) -> None:
24
27
  self.machine = machine
25
- self.view = view
28
+ self.hasher = hasher
26
29
 
27
30
  async def pre(self, event: EventType, ctx: Context) -> bool:
28
- if not self.view or not hasattr(self.view, "get_state_key"):
29
- raise RuntimeError(
30
- "WaiterMiddleware cannot be used inside a view which doesn't "
31
- "provide get_state_key (ABCStateView interface)."
32
- )
33
-
34
- view_name = self.view.__class__.__name__
35
- if view_name not in self.machine.storage:
31
+ if self.hasher not in self.machine.storage:
36
32
  return True
37
33
 
38
- key = self.view.get_state_key(event)
34
+ key = self.hasher.get_hash_from_data_from_event(event)
39
35
  if key is None:
40
- raise RuntimeError("Unable to get state key.")
36
+ logger.info(f"Unable to get hash from event with hasher {self.hasher}")
37
+ return True
41
38
 
42
- short_state: "ShortState[EventType] | None" = self.machine.storage[view_name].get(key)
39
+ short_state: "ShortState[EventType] | None" = self.machine.storage[self.hasher].get(key.unwrap())
43
40
  if not short_state:
44
41
  return True
45
42
 
@@ -47,35 +44,24 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
47
44
  if short_state.context is not None:
48
45
  preset_context.update(short_state.context.context)
49
46
 
50
- if short_state.expiration_date is not None and datetime.datetime.now() >= short_state.expiration_date:
51
- await self.machine.drop(
52
- self.view,
53
- short_state.key,
54
- event,
55
- ctx.raw_update,
56
- **preset_context.copy(),
57
- )
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)
58
52
  return True
59
53
 
60
- # before running the handler we check if the user wants to exit waiting
61
- if short_state.exit_behaviour is not None and await self.machine.call_behaviour(
62
- event,
63
- ctx.raw_update,
64
- behaviour=short_state.exit_behaviour,
65
- **preset_context,
66
- ):
54
+ if short_state.expiration_date is not None and datetime.datetime.now() >= short_state.expiration_date:
67
55
  await self.machine.drop(
68
- self.view,
69
- short_state.key,
70
- event,
71
- ctx.raw_update,
56
+ self.hasher,
57
+ self.hasher.get_data_from_event(event).unwrap(),
72
58
  **preset_context.copy(),
73
59
  )
74
60
  return True
75
61
 
76
62
  handler = FuncHandler(
77
63
  self.pass_runtime,
78
- list(short_state.rules),
64
+ [short_state.release] if short_state.release else [],
79
65
  dataclass=None,
80
66
  preset_context=preset_context,
81
67
  )
@@ -84,13 +70,9 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
84
70
  if result is True:
85
71
  await handler.run(event.api, event, ctx)
86
72
 
87
- elif short_state.default_behaviour is not None:
88
- await self.machine.call_behaviour(
89
- event,
90
- ctx.raw_update,
91
- behaviour=short_state.default_behaviour,
92
- **handler.preset_context,
93
- )
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)
94
76
 
95
77
  return False
96
78
 
@@ -3,7 +3,6 @@ import dataclasses
3
3
  import datetime
4
4
  import typing
5
5
 
6
- from telegrinder.api import API
7
6
  from telegrinder.bot.cute_types import BaseCute
8
7
  from telegrinder.bot.dispatch.context import Context
9
8
  from telegrinder.bot.dispatch.handler.abc import ABCHandler
@@ -11,7 +10,8 @@ from telegrinder.bot.rules.abc import ABCRule
11
10
  from telegrinder.model import Model
12
11
 
13
12
  if typing.TYPE_CHECKING:
14
- from .machine import Identificator
13
+ from .actions import WaiterActions
14
+
15
15
 
16
16
  T = typing.TypeVar("T", bound=Model)
17
17
  EventModel = typing.TypeVar("EventModel", bound=BaseCute)
@@ -26,19 +26,19 @@ class ShortStateContext(typing.Generic[EventModel], typing.NamedTuple):
26
26
 
27
27
  @dataclasses.dataclass(slots=True)
28
28
  class ShortState(typing.Generic[EventModel]):
29
- key: "Identificator"
30
- ctx_api: API
31
29
  event: asyncio.Event
32
- rules: tuple[ABCRule, ...]
33
- expiration: dataclasses.InitVar[datetime.timedelta | None] = dataclasses.field(
30
+ actions: "WaiterActions"
31
+
32
+ release: ABCRule | None = None
33
+ filter: ABCRule | None = None
34
+ lifetime: dataclasses.InitVar[datetime.timedelta | None] = dataclasses.field(
34
35
  default=None,
35
36
  kw_only=True,
36
37
  )
37
- default_behaviour: Behaviour[EventModel] | None = dataclasses.field(default=None, kw_only=True)
38
- on_drop_behaviour: Behaviour[EventModel] | None = dataclasses.field(default=None, kw_only=True)
39
- exit_behaviour: Behaviour[EventModel] | None = dataclasses.field(default=None, kw_only=True)
38
+
40
39
  expiration_date: datetime.datetime | None = dataclasses.field(init=False, kw_only=True)
41
40
  creation_date: datetime.datetime = dataclasses.field(init=False)
41
+
42
42
  context: ShortStateContext[EventModel] | None = dataclasses.field(default=None, init=False, kw_only=True)
43
43
 
44
44
  def __post_init__(self, expiration: datetime.timedelta | None = None) -> None:
@@ -5,7 +5,7 @@ import aiohttp
5
5
  import msgspec
6
6
  from fntypes.result import Error, Ok
7
7
 
8
- from telegrinder.api import API
8
+ from telegrinder.api.api import API
9
9
  from telegrinder.api.error import InvalidTokenError
10
10
  from telegrinder.bot.polling.abc import ABCPolling
11
11
  from telegrinder.modules import logger
@@ -72,7 +72,10 @@ class Polling(ABCPolling):
72
72
  async def get_updates(self) -> msgspec.Raw | None:
73
73
  raw_updates = await self.api.request_raw(
74
74
  "getUpdates",
75
- {"offset": self.offset, "allowed_updates": self.allowed_updates},
75
+ {
76
+ "offset": self.offset,
77
+ "allowed_updates": self.allowed_updates,
78
+ },
76
79
  )
77
80
  match raw_updates:
78
81
  case Ok(value):
@@ -83,7 +83,6 @@ __all__ = (
83
83
  "InlineQueryMarkup",
84
84
  "InlineQueryRule",
85
85
  "InlineQueryText",
86
- "IsInteger",
87
86
  "IntegerInRange",
88
87
  "InviteLinkByCreator",
89
88
  "InviteLinkName",
@@ -96,6 +95,7 @@ __all__ = (
96
95
  "IsForward",
97
96
  "IsForwardType",
98
97
  "IsGroup",
98
+ "IsInteger",
99
99
  "IsLanguageCode",
100
100
  "IsPremium",
101
101
  "IsPrivate",
@@ -107,13 +107,13 @@ __all__ = (
107
107
  "Markup",
108
108
  "MessageEntities",
109
109
  "MessageRule",
110
+ "NodeRule",
110
111
  "NotRule",
111
112
  "OrRule",
112
113
  "Regex",
113
114
  "RuleEnum",
114
115
  "StartCommand",
115
- "Text",
116
- "NodeRule",
117
116
  "State",
118
117
  "StateMeta",
118
+ "Text",
119
119
  )
@@ -118,12 +118,13 @@ class ABCRule(ABC, typing.Generic[AdaptTo]):
118
118
  ctx: Context,
119
119
  node_col: "NodeCollection | None" = None,
120
120
  ) -> bool:
121
+ bound_check_rule = self.check
121
122
  kw = {}
122
123
  node_col_values = node_col.values if node_col is not None else {}
123
- temp_ctx = get_default_args(self.check) | ctx
124
+ temp_ctx = get_default_args(bound_check_rule) | ctx
124
125
 
125
- for i, (k, v) in enumerate(get_annotations(self.check).items()):
126
- if (isinstance(adapted_value, Event) and not i) or (
126
+ for i, (k, v) in enumerate(get_annotations(bound_check_rule).items()):
127
+ if (isinstance(adapted_value, Event) and i == 0) or ( # First arg is Event
127
128
  isinstance(v, type) and isinstance(adapted_value, v)
128
129
  ):
129
130
  kw[k] = adapted_value if not isinstance(adapted_value, Event) else adapted_value.obj
@@ -140,14 +141,14 @@ class ABCRule(ABC, typing.Generic[AdaptTo]):
140
141
  "because it cannot be resolved."
141
142
  )
142
143
 
143
- return await self.check(**kw)
144
+ return await bound_check_rule(**kw)
144
145
 
145
146
  async def translate(self, translator: ABCTranslator) -> typing.Self:
146
147
  return self
147
148
 
148
149
 
149
150
  class AndRule(ABCRule):
150
- def __init__(self, *rules: ABCRule[AdaptTo]) -> None:
151
+ def __init__(self, *rules: ABCRule) -> None:
151
152
  self.rules = rules
152
153
 
153
154
  async def check(self, event: Update, ctx: Context) -> bool:
@@ -7,8 +7,8 @@ from telegrinder.bot.rules.adapter.raw_update import RawUpdateAdapter
7
7
  __all__ = (
8
8
  "ABCAdapter",
9
9
  "AdapterError",
10
+ "Event",
10
11
  "EventAdapter",
11
12
  "NodeAdapter",
12
13
  "RawUpdateAdapter",
13
- "Event",
14
14
  )
@@ -17,4 +17,4 @@ class IntegerInRange(ABCRule):
17
17
  return integer in self.rng
18
18
 
19
19
 
20
- __all__ = ("IsInteger", "IntegerInRange")
20
+ __all__ = ("IntegerInRange", "IsInteger")