telegrinder 0.3.0.post1__py3-none-any.whl → 0.3.0.post2__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 (59) hide show
  1. telegrinder/__init__.py +1 -1
  2. telegrinder/bot/cute_types/callback_query.py +2 -14
  3. telegrinder/bot/cute_types/chat_join_request.py +1 -1
  4. telegrinder/bot/cute_types/chat_member_updated.py +1 -1
  5. telegrinder/bot/cute_types/inline_query.py +1 -6
  6. telegrinder/bot/cute_types/message.py +1 -21
  7. telegrinder/bot/cute_types/update.py +1 -1
  8. telegrinder/bot/dispatch/abc.py +45 -3
  9. telegrinder/bot/dispatch/dispatch.py +8 -8
  10. telegrinder/bot/dispatch/handler/func.py +8 -10
  11. telegrinder/bot/dispatch/process.py +1 -1
  12. telegrinder/bot/dispatch/view/base.py +31 -20
  13. telegrinder/bot/dispatch/view/raw.py +20 -16
  14. telegrinder/bot/dispatch/waiter_machine/actions.py +3 -0
  15. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +15 -18
  16. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +21 -13
  17. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +15 -16
  18. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +6 -6
  19. telegrinder/bot/dispatch/waiter_machine/machine.py +24 -30
  20. telegrinder/bot/dispatch/waiter_machine/short_state.py +10 -4
  21. telegrinder/bot/rules/abc.py +37 -7
  22. telegrinder/bot/rules/adapter/raw_update.py +1 -3
  23. telegrinder/bot/rules/inline.py +1 -2
  24. telegrinder/bot/rules/markup.py +5 -2
  25. telegrinder/bot/rules/start.py +1 -1
  26. telegrinder/bot/rules/text.py +5 -3
  27. telegrinder/bot/scenario/checkbox.py +1 -1
  28. telegrinder/msgspec_utils.py +11 -3
  29. telegrinder/node/attachment.py +6 -6
  30. telegrinder/node/base.py +17 -11
  31. telegrinder/node/callback_query.py +1 -1
  32. telegrinder/node/command.py +1 -1
  33. telegrinder/node/composer.py +5 -2
  34. telegrinder/node/container.py +1 -1
  35. telegrinder/node/event.py +1 -1
  36. telegrinder/node/message.py +1 -1
  37. telegrinder/node/polymorphic.py +6 -3
  38. telegrinder/node/rule.py +1 -1
  39. telegrinder/node/source.py +5 -7
  40. telegrinder/node/text.py +2 -2
  41. telegrinder/node/tools/generator.py +1 -2
  42. telegrinder/node/update.py +3 -3
  43. telegrinder/rules.py +2 -0
  44. telegrinder/tools/buttons.py +4 -4
  45. telegrinder/tools/error_handler/abc.py +7 -7
  46. telegrinder/tools/error_handler/error_handler.py +58 -47
  47. telegrinder/tools/formatting/html.py +0 -2
  48. telegrinder/tools/functional.py +3 -0
  49. telegrinder/tools/global_context/telegrinder_ctx.py +2 -0
  50. telegrinder/tools/i18n/__init__.py +1 -1
  51. telegrinder/tools/i18n/{base.py → abc.py} +0 -0
  52. telegrinder/tools/i18n/middleware/__init__.py +1 -1
  53. telegrinder/tools/i18n/middleware/{base.py → abc.py} +3 -2
  54. telegrinder/tools/i18n/simple.py +11 -12
  55. telegrinder/tools/keyboard.py +9 -9
  56. {telegrinder-0.3.0.post1.dist-info → telegrinder-0.3.0.post2.dist-info}/METADATA +1 -1
  57. {telegrinder-0.3.0.post1.dist-info → telegrinder-0.3.0.post2.dist-info}/RECORD +59 -59
  58. {telegrinder-0.3.0.post1.dist-info → telegrinder-0.3.0.post2.dist-info}/LICENSE +0 -0
  59. {telegrinder-0.3.0.post1.dist-info → telegrinder-0.3.0.post2.dist-info}/WHEEL +0 -0
@@ -1,7 +1,6 @@
1
1
  from telegrinder.bot.cute_types import MessageCute as Message
2
2
  from telegrinder.bot.dispatch.view import MessageView
3
-
4
- from .hasher import Hasher
3
+ from telegrinder.bot.dispatch.waiter_machine.hasher.hasher import Hasher
5
4
 
6
5
 
7
6
  def from_chat_hash(chat_id: int) -> int:
@@ -12,9 +11,12 @@ def get_chat_from_event(event: Message) -> int:
12
11
  return event.chat.id
13
12
 
14
13
 
15
- MESSAGE_IN_CHAT = Hasher(
16
- view=MessageView, get_hash_from_data=from_chat_hash, get_data_from_event=get_chat_from_event
17
- )
14
+ def from_user_in_chat_hash(chat_and_user: tuple[int, int]) -> str:
15
+ return f"{chat_and_user[0]}_{chat_and_user[1]}"
16
+
17
+
18
+ def get_user_in_chat_from_event(event: Message) -> tuple[int, int]:
19
+ return event.chat.id, event.from_user.id
18
20
 
19
21
 
20
22
  def from_user_hash(from_id: int) -> int:
@@ -25,23 +27,20 @@ def get_user_from_event(event: Message) -> int:
25
27
  return event.from_user.id
26
28
 
27
29
 
30
+ MESSAGE_IN_CHAT = Hasher(
31
+ view_class=MessageView,
32
+ get_hash_from_data=from_chat_hash,
33
+ get_data_from_event=get_chat_from_event,
34
+ )
35
+
28
36
  MESSAGE_FROM_USER = Hasher(
29
- view=MessageView,
37
+ view_class=MessageView,
30
38
  get_hash_from_data=from_user_hash,
31
39
  get_data_from_event=get_user_from_event,
32
40
  )
33
41
 
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
42
  MESSAGE_FROM_USER_IN_CHAT = Hasher(
44
- view=MessageView,
43
+ view_class=MessageView,
45
44
  get_hash_from_data=from_user_in_chat_hash,
46
45
  get_data_from_event=get_user_in_chat_from_event,
47
46
  )
@@ -1,16 +1,16 @@
1
- from fntypes import Option
1
+ from fntypes.option import Option
2
2
 
3
3
  from telegrinder.bot.dispatch.view import BaseStateView
4
+ from telegrinder.bot.dispatch.waiter_machine.hasher.hasher import ECHO, Event, Hasher
4
5
  from telegrinder.tools.functional import from_optional
5
6
 
6
- from .hasher import ECHO, Event, Hasher
7
-
8
7
 
9
8
  class StateViewHasher(Hasher[Event, int]):
10
- view: BaseStateView
9
+ view: BaseStateView[Event]
11
10
 
12
- def __init__(self, view: type[BaseStateView[Event]]):
13
- super().__init__(view, get_hash_from_data=ECHO)
11
+ def __init__(self, view: BaseStateView[Event]) -> None:
12
+ self.view = view
13
+ super().__init__(view.__class__, get_hash_from_data=ECHO)
14
14
 
15
15
  def get_data_from_event(self, event: Event) -> Option[int]:
16
16
  return from_optional(self.view.get_state_key(event))
@@ -41,16 +41,25 @@ class WaiterMachine:
41
41
  self.storage: Storage = {}
42
42
 
43
43
  def __repr__(self) -> str:
44
- return "<{}: max_storage_size={}, {}>".format(
44
+ return "<{}: max_storage_size={}, base_state_lifetime={!r}>".format(
45
45
  self.__class__.__name__,
46
46
  self.max_storage_size,
47
- ", ".join(
48
- f"{view_name}: {len(self.storage[view_name].values())} shortstates"
49
- for view_name in self.storage
50
- )
51
- or "empty",
47
+ self.base_state_lifetime,
52
48
  )
53
49
 
50
+ async def drop_all(self) -> None:
51
+ """Drops all waiters in storage."""
52
+
53
+ for hasher in self.storage:
54
+ for ident, short_state in self.storage[hasher].items():
55
+ if short_state.context:
56
+ await self.drop(
57
+ hasher,
58
+ ident,
59
+ )
60
+ else:
61
+ short_state.cancel()
62
+
54
63
  async def drop(
55
64
  self,
56
65
  hasher: Hasher[EventModel, HasherData],
@@ -66,7 +75,7 @@ class WaiterMachine:
66
75
  short_state = self.storage[hasher].pop(waiter_id, None)
67
76
  if short_state is None:
68
77
  raise LookupError(
69
- "Waiter with identificator {} is not found for hasher {!r}".format(waiter_id, hasher)
78
+ "Waiter with identificator {} is not found for hasher {!r}.".format(waiter_id, hasher)
70
79
  )
71
80
 
72
81
  if on_drop := short_state.actions.get("on_drop"):
@@ -74,19 +83,6 @@ class WaiterMachine:
74
83
 
75
84
  short_state.cancel()
76
85
 
77
- async def drop_all(self) -> None:
78
- """Drops all waiters in storage."""
79
-
80
- for hasher in self.storage:
81
- for ident, short_state in self.storage[hasher].items():
82
- if short_state.context:
83
- await self.drop(
84
- hasher,
85
- ident,
86
- )
87
- else:
88
- short_state.cancel()
89
-
90
86
  async def wait_from_event(
91
87
  self,
92
88
  view: BaseStateView[EventModel],
@@ -97,11 +93,11 @@ class WaiterMachine:
97
93
  lifetime: datetime.timedelta | float | None = None,
98
94
  **actions: typing.Unpack[WaiterActions[EventModel]],
99
95
  ) -> ShortStateContext[EventModel]:
100
- hasher = StateViewHasher(view.__class__)
96
+ hasher = StateViewHasher(view)
101
97
  return await self.wait(
102
98
  hasher=hasher,
103
99
  data=hasher.get_data_from_event(event).expect(
104
- RuntimeError("Hasher couldn't create data from event.")
100
+ RuntimeError("Hasher couldn't create data from event."),
105
101
  ),
106
102
  filter=filter,
107
103
  release=release,
@@ -124,18 +120,18 @@ class WaiterMachine:
124
120
 
125
121
  event = asyncio.Event()
126
122
  short_state = ShortState[EventModel](
127
- filter=filter,
123
+ event,
124
+ actions,
128
125
  release=release,
129
- event=event,
126
+ filter=filter,
130
127
  lifetime=lifetime or self.base_state_lifetime,
131
- actions=actions,
132
128
  )
133
129
  waiter_hash = hasher.get_hash_from_data(data).expect(RuntimeError("Hasher couldn't create hash."))
134
130
 
135
131
  if hasher not in self.storage:
136
132
  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")
133
+ view: BaseView[EventModel] = self.dispatch.get_view(hasher.view_class).expect(
134
+ RuntimeError(f"View {hasher.view_class.__name__!r} is not defined in dispatch.")
139
135
  )
140
136
  view.middlewares.insert(0, WaiterMiddleware(self, hasher))
141
137
  self.storage[hasher] = LimitedDict(maxlimit=self.max_storage_size)
@@ -148,9 +144,7 @@ class WaiterMachine:
148
144
  assert short_state.context is not None
149
145
  return short_state.context
150
146
 
151
- async def clear_storage(
152
- self,
153
- ) -> None:
147
+ async def clear_storage(self) -> None:
154
148
  """Clears storage."""
155
149
 
156
150
  for hasher in self.storage:
@@ -27,10 +27,17 @@ class ShortStateContext(typing.Generic[EventModel], typing.NamedTuple):
27
27
  @dataclasses.dataclass(slots=True)
28
28
  class ShortState(typing.Generic[EventModel]):
29
29
  event: asyncio.Event
30
- actions: "WaiterActions"
30
+ actions: "WaiterActions[EventModel]"
31
+
32
+ release: ABCRule | None = dataclasses.field(
33
+ default=None,
34
+ kw_only=True,
35
+ )
36
+ filter: ABCRule | None = dataclasses.field(
37
+ default=None,
38
+ kw_only=True,
39
+ )
31
40
 
32
- release: ABCRule | None = None
33
- filter: ABCRule | None = None
34
41
  lifetime: dataclasses.InitVar[datetime.timedelta | None] = dataclasses.field(
35
42
  default=None,
36
43
  kw_only=True,
@@ -38,7 +45,6 @@ class ShortState(typing.Generic[EventModel]):
38
45
 
39
46
  expiration_date: datetime.datetime | None = dataclasses.field(init=False, kw_only=True)
40
47
  creation_date: datetime.datetime = dataclasses.field(init=False)
41
-
42
48
  context: ShortStateContext[EventModel] | None = dataclasses.field(default=None, init=False, kw_only=True)
43
49
 
44
50
  def __post_init__(self, expiration: datetime.timedelta | None = None) -> None:
@@ -10,7 +10,7 @@ from telegrinder.bot.dispatch.process import check_rule
10
10
  from telegrinder.bot.rules.adapter import ABCAdapter, RawUpdateAdapter
11
11
  from telegrinder.bot.rules.adapter.node import Event
12
12
  from telegrinder.node.base import Node, get_nodes, is_node
13
- from telegrinder.tools.i18n.base import ABCTranslator
13
+ from telegrinder.tools.i18n.abc import ABCTranslator
14
14
  from telegrinder.tools.magic import (
15
15
  cache_translation,
16
16
  get_annotations,
@@ -22,7 +22,7 @@ from telegrinder.types.objects import Update as UpdateObject
22
22
  if typing.TYPE_CHECKING:
23
23
  from telegrinder.node.composer import NodeCollection
24
24
 
25
- AdaptTo = typing.TypeVar("AdaptTo", default=typing.Any)
25
+ AdaptTo = typing.TypeVar("AdaptTo", default=typing.Any, contravariant=True)
26
26
 
27
27
  Message: typing.TypeAlias = MessageCute
28
28
  Update: typing.TypeAlias = UpdateCute
@@ -45,12 +45,42 @@ class ABCRule(ABC, typing.Generic[AdaptTo]):
45
45
  adapter: ABCAdapter[UpdateObject, AdaptTo]
46
46
  requires: list["ABCRule"] = []
47
47
 
48
- if not typing.TYPE_CHECKING:
48
+ if typing.TYPE_CHECKING:
49
+
50
+ @typing.overload
51
+ async def check(self) -> bool: ...
52
+
53
+ @typing.overload
54
+ async def check(self, event: AdaptTo, /) -> bool: ...
55
+
56
+ @typing.overload
57
+ async def check(self, event: AdaptTo, ctx: Context, /) -> bool: ...
58
+
59
+ @typing.overload
60
+ async def check(
61
+ self,
62
+ event: AdaptTo,
63
+ ctx: Context,
64
+ /,
65
+ *args: typing.Any,
66
+ **kwargs: typing.Any,
67
+ ) -> bool: ...
68
+
69
+ @typing.overload
70
+ async def check(self, event: AdaptTo, /, *args: typing.Any, **kwargs: typing.Any) -> bool: ...
71
+
72
+ @typing.overload
73
+ async def check(self, ctx: Context, /, *args: typing.Any, **kwargs: typing.Any) -> bool: ...
74
+
75
+ @abstractmethod
76
+ async def check(self, *args: typing.Any, **kwargs: typing.Any) -> bool:
77
+ pass
78
+ else:
49
79
  adapter = RawUpdateAdapter()
50
80
 
51
- @abstractmethod
52
- async def check(self, event: AdaptTo, *, ctx: Context) -> bool:
53
- pass
81
+ @abstractmethod
82
+ async def check(self, *args, **kwargs):
83
+ pass
54
84
 
55
85
  def __init_subclass__(cls, requires: list["ABCRule"] | None = None) -> None:
56
86
  """Merges requirements from inherited classes and rule-specific requirements."""
@@ -141,7 +171,7 @@ class ABCRule(ABC, typing.Generic[AdaptTo]):
141
171
  "because it cannot be resolved."
142
172
  )
143
173
 
144
- return await bound_check_rule(**kw)
174
+ return await bound_check_rule(**kw) # type: ignore
145
175
 
146
176
  async def translate(self, translator: ABCTranslator) -> typing.Self:
147
177
  return self
@@ -22,9 +22,7 @@ class RawUpdateAdapter(ABCAdapter[Update, UpdateCute]):
22
22
  ) -> Result[UpdateCute, AdapterError]:
23
23
  if self.ADAPTED_VALUE_KEY not in context:
24
24
  context[self.ADAPTED_VALUE_KEY] = (
25
- UpdateCute.from_update(update, api)
26
- if not isinstance(update, UpdateCute)
27
- else update
25
+ UpdateCute.from_update(update, api) if not isinstance(update, UpdateCute) else update
28
26
  )
29
27
  return Ok(context[self.ADAPTED_VALUE_KEY])
30
28
 
@@ -16,8 +16,7 @@ class InlineQueryRule(ABCRule[InlineQuery], abc.ABC):
16
16
  adapter: EventAdapter[InlineQuery] = EventAdapter(UpdateType.INLINE_QUERY, InlineQuery)
17
17
 
18
18
  @abc.abstractmethod
19
- async def check(self, query: InlineQuery, ctx: Context) -> bool:
20
- ...
19
+ async def check(self, query: InlineQuery, ctx: Context) -> bool: ...
21
20
 
22
21
 
23
22
  class HasLocation(InlineQueryRule):
@@ -24,13 +24,16 @@ def check_string(patterns: list[vbml.Pattern], s: str, ctx: Context) -> bool:
24
24
 
25
25
 
26
26
  class Markup(ABCRule):
27
- """Markup Language. See [VBML docs](https://github.com/tesseradecade/vbml/blob/master/docs/index.md)"""
27
+ """Markup Language. See the [vbml documentation](https://github.com/tesseradecade/vbml/blob/master/docs/index.md)."""
28
28
 
29
29
  def __init__(self, patterns: PatternLike | list[PatternLike], /) -> None:
30
30
  if not isinstance(patterns, list):
31
31
  patterns = [patterns]
32
32
  self.patterns = [
33
- vbml.Pattern(pattern) if isinstance(pattern, str) else pattern for pattern in patterns
33
+ vbml.Pattern(pattern, flags=global_ctx.vbml_pattern_flags)
34
+ if isinstance(pattern, str)
35
+ else pattern
36
+ for pattern in patterns
34
37
  ]
35
38
 
36
39
  async def check(self, text: Text, ctx: Context) -> bool:
@@ -12,7 +12,7 @@ from .message_entities import MessageEntities
12
12
  class StartCommand(
13
13
  MessageRule,
14
14
  requires=[
15
- IsPrivate(),
15
+ IsPrivate(),
16
16
  MessageEntities(MessageEntityType.BOT_COMMAND),
17
17
  Markup(["/start <param>", "/start"]),
18
18
  ],
@@ -1,5 +1,7 @@
1
+ import typing
2
+
1
3
  from telegrinder import node
2
- from telegrinder.tools.i18n.base import ABCTranslator
4
+ from telegrinder.tools.i18n.abc import ABCTranslator
3
5
 
4
6
  from .abc import ABCRule, with_caching_translations
5
7
  from .node import NodeRule
@@ -21,8 +23,8 @@ class Text(ABCRule):
21
23
  return (text if not self.ignore_case else text.lower()) in self.texts
22
24
 
23
25
  @with_caching_translations
24
- async def translate(self, translator: ABCTranslator) -> "Text":
25
- return Text(
26
+ async def translate(self, translator: ABCTranslator) -> typing.Self:
27
+ return self.__class__(
26
28
  texts=[translator.get(text) for text in self.texts],
27
29
  ignore_case=self.ignore_case,
28
30
  )
@@ -124,7 +124,7 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
124
124
  ).unwrap()
125
125
 
126
126
  while True:
127
- q, _ = await self.waiter_machine.wait(StateViewHasher(view.__class__), message.message_id)
127
+ q, _ = await self.waiter_machine.wait(StateViewHasher(view), message.message_id)
128
128
  should_continue = await self.handle(q)
129
129
  await q.answer(self.CALLBACK_ANSWER)
130
130
  if not should_continue:
@@ -210,12 +210,18 @@ class Decoder:
210
210
 
211
211
  @typing.overload
212
212
  def __call__(
213
- self, type: type[T], *, strict: bool = True
213
+ self,
214
+ type: type[T],
215
+ *,
216
+ strict: bool = True,
214
217
  ) -> typing.ContextManager[msgspec.json.Decoder[T]]: ...
215
218
 
216
219
  @typing.overload
217
220
  def __call__(
218
- self, type: typing.Any, *, strict: bool = True
221
+ self,
222
+ type: typing.Any,
223
+ *,
224
+ strict: bool = True,
219
225
  ) -> typing.ContextManager[msgspec.json.Decoder[typing.Any]]: ...
220
226
 
221
227
  @contextmanager
@@ -223,7 +229,9 @@ class Decoder:
223
229
  """Context manager returns the `msgspec.json.Decoder` object with the `dec_hook`."""
224
230
 
225
231
  dec_obj = msgspec.json.Decoder(
226
- type=typing.Any if type is object else type, strict=strict, dec_hook=self.dec_hook
232
+ type=typing.Any if type is object else type,
233
+ strict=strict,
234
+ dec_hook=self.dec_hook,
227
235
  )
228
236
  yield dec_obj
229
237
 
@@ -31,7 +31,7 @@ class Attachment(DataNode):
31
31
  )
32
32
 
33
33
  @classmethod
34
- async def compose(cls, message: MessageNode) -> "Attachment":
34
+ def compose(cls, message: MessageNode) -> "Attachment":
35
35
  for attachment_type in ("audio", "document", "photo", "poll", "video"):
36
36
  match getattr(message, attachment_type, Nothing()):
37
37
  case Some(attachment):
@@ -44,7 +44,7 @@ class Photo(DataNode):
44
44
  sizes: list[telegrinder.types.PhotoSize]
45
45
 
46
46
  @classmethod
47
- async def compose(cls, attachment: Attachment) -> typing.Self:
47
+ def compose(cls, attachment: Attachment) -> typing.Self:
48
48
  if not attachment.photo:
49
49
  raise ComposeError("Attachment is not a photo.")
50
50
  return cls(attachment.photo.unwrap())
@@ -52,7 +52,7 @@ class Photo(DataNode):
52
52
 
53
53
  class Video(ScalarNode, telegrinder.types.Video):
54
54
  @classmethod
55
- async def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
55
+ def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
56
56
  if not attachment.video:
57
57
  raise ComposeError("Attachment is not a video.")
58
58
  return attachment.video.unwrap()
@@ -60,7 +60,7 @@ class Video(ScalarNode, telegrinder.types.Video):
60
60
 
61
61
  class Audio(ScalarNode, telegrinder.types.Audio):
62
62
  @classmethod
63
- async def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
63
+ def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
64
64
  if not attachment.audio:
65
65
  raise ComposeError("Attachment is not an audio.")
66
66
  return attachment.audio.unwrap()
@@ -68,7 +68,7 @@ class Audio(ScalarNode, telegrinder.types.Audio):
68
68
 
69
69
  class Document(ScalarNode, telegrinder.types.Document):
70
70
  @classmethod
71
- async def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
71
+ def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
72
72
  if not attachment.document:
73
73
  raise ComposeError("Attachment is not a document.")
74
74
  return attachment.document.unwrap()
@@ -76,7 +76,7 @@ class Document(ScalarNode, telegrinder.types.Document):
76
76
 
77
77
  class Poll(ScalarNode, telegrinder.types.Poll):
78
78
  @classmethod
79
- async def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
79
+ def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
80
80
  if not attachment.poll:
81
81
  raise ComposeError("Attachment is not a poll.")
82
82
  return attachment.poll.unwrap()
telegrinder/node/base.py CHANGED
@@ -4,12 +4,11 @@ import typing
4
4
  from types import AsyncGeneratorType
5
5
 
6
6
  from telegrinder.node.scope import NodeScope
7
- from telegrinder.tools.magic import (
8
- cache_magic_value,
9
- get_annotations,
10
- )
7
+ from telegrinder.tools.magic import cache_magic_value, get_annotations
11
8
 
12
- ComposeResult: typing.TypeAlias = typing.Awaitable[typing.Any] | typing.AsyncGenerator[typing.Any, None]
9
+ ComposeResult: typing.TypeAlias = (
10
+ typing.Awaitable[typing.Any] | typing.AsyncGenerator[typing.Any, None] | typing.Any
11
+ )
13
12
 
14
13
 
15
14
  def is_node(maybe_node: typing.Any) -> typing.TypeGuard[type["Node"]]:
@@ -28,7 +27,9 @@ def get_nodes(function: typing.Callable[..., typing.Any]) -> dict[str, type["Nod
28
27
 
29
28
 
30
29
  @cache_magic_value("__is_generator__")
31
- def is_generator(function: typing.Callable[..., typing.Any]) -> typing.TypeGuard[AsyncGeneratorType[typing.Any, None]]:
30
+ def is_generator(
31
+ function: typing.Callable[..., typing.Any],
32
+ ) -> typing.TypeGuard[AsyncGeneratorType[typing.Any, None]]:
32
33
  return inspect.isasyncgenfunction(function)
33
34
 
34
35
 
@@ -103,20 +104,25 @@ if typing.TYPE_CHECKING:
103
104
  pass
104
105
 
105
106
  else:
107
+
106
108
  def __init_subclass__(cls, *args, **kwargs): # noqa: N807
107
- if any(issubclass(base, ScalarNode) for base in cls.__bases__ if base is not ScalarNode):
109
+ if any(issubclass(base, ScalarNodeProto) for base in cls.__bases__ if base is not ScalarNode):
108
110
  raise RuntimeError("Scalar nodes do not support inheritance.")
109
111
 
110
- def create_node(cls, bases, dct):
111
- dct.update(cls.__dict__)
112
- return type(cls.__name__, bases, dct)
112
+ def _as_node(cls, bases, dct):
113
+ if not hasattr(cls, "_scalar_node_type"):
114
+ dct.update(cls.__dict__)
115
+ scalar_node_type = type(cls.__name__, bases, dct)
116
+ setattr(cls, "_scalar_node_type", scalar_node_type)
117
+ return scalar_node_type
118
+ return getattr(cls, "_scalar_node_type")
113
119
 
114
120
  def create_class(name, bases, dct):
115
121
  return type(
116
122
  "Scalar",
117
123
  (SCALAR_NODE,),
118
124
  {
119
- "as_node": classmethod(lambda cls: create_node(cls, bases, dct)),
125
+ "as_node": classmethod(lambda cls: _as_node(cls, bases, dct)),
120
126
  "scope": Node.scope,
121
127
  "__init_subclass__": __init_subclass__,
122
128
  },
@@ -5,7 +5,7 @@ from telegrinder.node.update import UpdateNode
5
5
 
6
6
  class CallbackQueryNode(ScalarNode, CallbackQueryCute):
7
7
  @classmethod
8
- async def compose(cls, update: UpdateNode) -> CallbackQueryCute:
8
+ def compose(cls, update: UpdateNode) -> CallbackQueryCute:
9
9
  if not update.callback_query:
10
10
  raise ComposeError("Update is not a callback_query.")
11
11
  return update.callback_query.unwrap()
@@ -24,7 +24,7 @@ class CommandInfo(DataNode):
24
24
  mention: Option[str] = field(default_factory=Nothing)
25
25
 
26
26
  @classmethod
27
- async def compose(cls, text: Text) -> typing.Self:
27
+ def compose(cls, text: Text) -> typing.Self:
28
28
  name, arguments = single_split(text, separator=" ")
29
29
  name, mention = cut_mention(name)
30
30
  return cls(name, arguments, mention)
@@ -1,8 +1,9 @@
1
1
  import dataclasses
2
+ import inspect
2
3
  import typing
3
4
 
4
- from fntypes import Error, Ok, Result
5
5
  from fntypes.error import UnwrapError
6
+ from fntypes.result import Error, Ok, Result
6
7
 
7
8
  from telegrinder.api.api import API
8
9
  from telegrinder.bot.cute_types.update import Update, UpdateCute
@@ -33,7 +34,9 @@ async def compose_node(
33
34
  value = await generator.asend(None)
34
35
  else:
35
36
  generator = None
36
- value = await typing.cast(typing.Awaitable[typing.Any], node.compose(**kwargs))
37
+ value = typing.cast(typing.Awaitable[typing.Any] | typing.Any, node.compose(**kwargs))
38
+ if inspect.isawaitable(value):
39
+ value = await value
37
40
 
38
41
  return NodeSession(_node, value, {}, generator)
39
42
 
@@ -7,7 +7,7 @@ class ContainerNode(Node):
7
7
  linked_nodes: typing.ClassVar[list[type[Node]]]
8
8
 
9
9
  @classmethod
10
- async def compose(cls, **kw) -> tuple[Node, ...]:
10
+ def compose(cls, **kw) -> tuple[Node, ...]:
11
11
  return tuple(t[1] for t in sorted(kw.items(), key=lambda t: t[0]))
12
12
 
13
13
  @classmethod
telegrinder/node/event.py CHANGED
@@ -31,7 +31,7 @@ class _EventNode(Node):
31
31
  return cls(dataclass)
32
32
 
33
33
  @classmethod
34
- async def compose(cls, raw_update: UpdateNode, ctx: Context, api: API) -> "DataclassType":
34
+ def compose(cls, raw_update: UpdateNode, ctx: Context, api: API) -> "DataclassType":
35
35
  dataclass_type = typing.get_origin(cls.dataclass) or cls.dataclass
36
36
 
37
37
  try:
@@ -5,7 +5,7 @@ from telegrinder.node.update import UpdateNode
5
5
 
6
6
  class MessageNode(ScalarNode, MessageCute):
7
7
  @classmethod
8
- async def compose(cls, update: UpdateNode) -> MessageCute:
8
+ def compose(cls, update: UpdateNode) -> MessageCute:
9
9
  if not update.message:
10
10
  raise ComposeError("Update is not a message.")
11
11
  return update.message.unwrap()
@@ -13,7 +13,7 @@ from telegrinder.tools.magic import get_impls, impl
13
13
  class Polymorphic(Node):
14
14
  @classmethod
15
15
  async def compose(cls, update: UpdateNode, context: Context) -> typing.Any:
16
- logger.debug(f"Composing polimorphic node {cls.__name__!r}...")
16
+ logger.debug(f"Composing polymorphic node {cls.__name__!r}...")
17
17
  scope = getattr(cls, "scope", None)
18
18
  node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
19
19
 
@@ -27,7 +27,10 @@ class Polymorphic(Node):
27
27
 
28
28
  # To determine whether this is a right morph, all subnodes must be resolved
29
29
  if scope is NodeScope.PER_EVENT and (cls, i) in node_ctx:
30
- logger.debug("Morph is already cached as per_event node, using its value. Impl {!r} succeeded!", impl_.__name__)
30
+ logger.debug(
31
+ "Morph is already cached as per_event node, using its value. Impl {!r} succeeded!",
32
+ impl_.__name__,
33
+ )
31
34
  res: NodeSession = node_ctx[(cls, i)]
32
35
  await node_collection.close_all()
33
36
  return res.value
@@ -40,7 +43,7 @@ class Polymorphic(Node):
40
43
  node_ctx[(cls, i)] = NodeSession(cls, result, {})
41
44
 
42
45
  await node_collection.close_all(with_value=result)
43
- logger.debug("Impl {!r} succeeded with value: {}", impl_.__name__, result)
46
+ logger.debug("Impl {!r} succeeded with value: {!r}", impl_.__name__, result)
44
47
  return result
45
48
 
46
49
  raise ComposeError("No implementation found.")
telegrinder/node/rule.py CHANGED
@@ -12,7 +12,7 @@ if typing.TYPE_CHECKING:
12
12
 
13
13
 
14
14
  class RuleChain(dict[str, typing.Any], Node):
15
- dataclass = dict
15
+ dataclass: type[typing.Any] = dict
16
16
  rules: tuple["ABCRule", ...] = ()
17
17
 
18
18
  def __init_subclass__(cls, *args: typing.Any, **kwargs: typing.Any) -> None: