telegrinder 0.2.1__py3-none-any.whl → 0.2.2__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 (71) hide show
  1. telegrinder/__init__.py +22 -11
  2. telegrinder/bot/__init__.py +16 -4
  3. telegrinder/bot/cute_types/chat_member_updated.py +1 -1
  4. telegrinder/bot/cute_types/message.py +18 -11
  5. telegrinder/bot/dispatch/__init__.py +18 -2
  6. telegrinder/bot/dispatch/abc.py +1 -1
  7. telegrinder/bot/dispatch/context.py +2 -2
  8. telegrinder/bot/dispatch/dispatch.py +1 -1
  9. telegrinder/bot/dispatch/handler/__init__.py +17 -1
  10. telegrinder/bot/dispatch/handler/audio_reply.py +44 -0
  11. telegrinder/bot/dispatch/handler/base.py +57 -0
  12. telegrinder/bot/dispatch/handler/document_reply.py +44 -0
  13. telegrinder/bot/dispatch/handler/func.py +3 -3
  14. telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
  15. telegrinder/bot/dispatch/handler/message_reply.py +12 -35
  16. telegrinder/bot/dispatch/handler/photo_reply.py +44 -0
  17. telegrinder/bot/dispatch/handler/sticker_reply.py +37 -0
  18. telegrinder/bot/dispatch/handler/video_reply.py +44 -0
  19. telegrinder/bot/dispatch/process.py +2 -2
  20. telegrinder/bot/dispatch/return_manager/abc.py +11 -8
  21. telegrinder/bot/dispatch/return_manager/callback_query.py +2 -2
  22. telegrinder/bot/dispatch/return_manager/inline_query.py +2 -2
  23. telegrinder/bot/dispatch/return_manager/message.py +3 -3
  24. telegrinder/bot/dispatch/view/__init__.py +2 -1
  25. telegrinder/bot/dispatch/view/abc.py +2 -181
  26. telegrinder/bot/dispatch/view/base.py +200 -0
  27. telegrinder/bot/dispatch/view/callback_query.py +3 -3
  28. telegrinder/bot/dispatch/view/chat_join_request.py +2 -2
  29. telegrinder/bot/dispatch/view/chat_member.py +2 -3
  30. telegrinder/bot/dispatch/view/inline_query.py +2 -2
  31. telegrinder/bot/dispatch/view/message.py +5 -4
  32. telegrinder/bot/dispatch/view/raw.py +4 -3
  33. telegrinder/bot/dispatch/waiter_machine/machine.py +6 -7
  34. telegrinder/bot/dispatch/waiter_machine/middleware.py +0 -6
  35. telegrinder/bot/dispatch/waiter_machine/short_state.py +1 -1
  36. telegrinder/bot/polling/polling.py +5 -2
  37. telegrinder/bot/rules/__init__.py +3 -3
  38. telegrinder/bot/rules/abc.py +6 -5
  39. telegrinder/bot/rules/adapter/__init__.py +1 -1
  40. telegrinder/bot/rules/integer.py +1 -1
  41. telegrinder/bot/rules/is_from.py +19 -0
  42. telegrinder/bot/rules/state.py +9 -6
  43. telegrinder/bot/scenario/checkbox.py +3 -3
  44. telegrinder/bot/scenario/choice.py +2 -2
  45. telegrinder/client/aiohttp.py +5 -7
  46. telegrinder/model.py +1 -8
  47. telegrinder/modules.py +16 -25
  48. telegrinder/msgspec_utils.py +5 -5
  49. telegrinder/node/base.py +2 -2
  50. telegrinder/node/composer.py +5 -9
  51. telegrinder/node/container.py +6 -1
  52. telegrinder/node/polymorphic.py +7 -7
  53. telegrinder/node/rule.py +6 -4
  54. telegrinder/node/scope.py +3 -3
  55. telegrinder/node/source.py +4 -2
  56. telegrinder/node/tools/generator.py +7 -6
  57. telegrinder/rules.py +2 -2
  58. telegrinder/tools/__init__.py +10 -10
  59. telegrinder/tools/keyboard.py +6 -1
  60. telegrinder/tools/loop_wrapper/loop_wrapper.py +4 -5
  61. telegrinder/tools/magic.py +17 -19
  62. telegrinder/tools/state_storage/__init__.py +3 -3
  63. telegrinder/tools/state_storage/abc.py +12 -10
  64. telegrinder/tools/state_storage/memory.py +6 -3
  65. telegrinder/types/__init__.py +1 -0
  66. telegrinder/types/methods.py +10 -2
  67. telegrinder/types/objects.py +47 -5
  68. {telegrinder-0.2.1.dist-info → telegrinder-0.2.2.dist-info}/METADATA +3 -4
  69. {telegrinder-0.2.1.dist-info → telegrinder-0.2.2.dist-info}/RECORD +71 -63
  70. {telegrinder-0.2.1.dist-info → telegrinder-0.2.2.dist-info}/LICENSE +0 -0
  71. {telegrinder-0.2.1.dist-info → telegrinder-0.2.2.dist-info}/WHEEL +0 -0
telegrinder/modules.py CHANGED
@@ -4,13 +4,6 @@ import typing
4
4
  from choicelib import choice_in_order
5
5
 
6
6
 
7
- @typing.runtime_checkable
8
- class JSONModule(typing.Protocol):
9
- def loads(self, s: str | bytes) -> typing.Any: ...
10
-
11
- def dumps(self, o: typing.Any) -> str: ...
12
-
13
-
14
7
  @typing.runtime_checkable
15
8
  class LoggerModule(typing.Protocol):
16
9
  def debug(self, __msg: object, *args: object, **kwargs: object) -> None: ...
@@ -25,25 +18,23 @@ class LoggerModule(typing.Protocol):
25
18
 
26
19
  def exception(self, __msg: object, *args: object, **kwargs: object) -> None: ...
27
20
 
28
- def set_level(
29
- self,
30
- level: typing.Literal[
31
- "DEBUG",
32
- "INFO",
33
- "WARNING",
34
- "ERROR",
35
- "CRITICAL",
36
- "EXCEPTION",
37
- ],
38
- ) -> None: ...
21
+ if typing.TYPE_CHECKING:
22
+
23
+ def set_level(
24
+ self,
25
+ level: typing.Literal[
26
+ "DEBUG",
27
+ "INFO",
28
+ "WARNING",
29
+ "ERROR",
30
+ "CRITICAL",
31
+ "EXCEPTION",
32
+ ],
33
+ /,
34
+ ) -> None: ...
39
35
 
40
36
 
41
37
  logger: LoggerModule
42
- json: JSONModule = choice_in_order(
43
- ["orjson", "ujson", "hyperjson"],
44
- default="telegrinder.msgspec_json",
45
- do_import=True,
46
- )
47
38
  logging_level = os.getenv("LOGGER_LEVEL", default="DEBUG").upper()
48
39
  logging_module = choice_in_order(["loguru"], default="logging")
49
40
  asyncio_module = choice_in_order(["uvloop"], default="asyncio")
@@ -227,7 +218,7 @@ if asyncio_module == "uvloop":
227
218
  asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) # type: ignore
228
219
 
229
220
 
230
- def _set_logger_level(level):
221
+ def _set_logger_level(level, /):
231
222
  level = level.upper()
232
223
  if logging_module == "logging":
233
224
  import logging
@@ -243,4 +234,4 @@ def _set_logger_level(level):
243
234
  setattr(logger, "set_level", staticmethod(_set_logger_level)) # type: ignore
244
235
 
245
236
 
246
- __all__ = ("json", "logger")
237
+ __all__ = ("LoggerModule", "logger")
@@ -29,7 +29,6 @@ else:
29
29
  def __instancecheck__(cls, __instance: typing.Any) -> bool:
30
30
  return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
31
31
 
32
-
33
32
  class Option(typing.Generic[Value], metaclass=OptionMeta):
34
33
  pass
35
34
 
@@ -63,9 +62,10 @@ def is_common_type(type_: typing.Any) -> typing.TypeGuard[type[typing.Any]]:
63
62
  def type_check(obj: typing.Any, t: typing.Any) -> bool:
64
63
  return (
65
64
  isinstance(obj, t)
66
- if isinstance(t, type)
67
- and issubclass(t, msgspec.Struct)
68
- else type(obj) in t if isinstance(t, tuple) else type(obj) is t
65
+ if isinstance(t, type) and issubclass(t, msgspec.Struct)
66
+ else type(obj) in t
67
+ if isinstance(t, tuple)
68
+ else type(obj) is t
69
69
  )
70
70
 
71
71
 
@@ -344,8 +344,8 @@ __all__ = (
344
344
  "datetime",
345
345
  "decoder",
346
346
  "encoder",
347
- "msgspec_convert",
348
347
  "get_class_annotations",
349
348
  "get_type_hints",
349
+ "msgspec_convert",
350
350
  "msgspec_to_builtins",
351
351
  )
telegrinder/node/base.py CHANGED
@@ -12,8 +12,8 @@ from telegrinder.tools.magic import (
12
12
  ComposeResult: typing.TypeAlias = typing.Awaitable[typing.Any] | typing.AsyncGenerator[typing.Any, None]
13
13
 
14
14
 
15
- def is_node(maybe_node: type[typing.Any]) -> typing.TypeGuard[type["Node"]]:
16
- maybe_node = typing.get_origin(maybe_node) or maybe_node
15
+ def is_node(maybe_node: typing.Any) -> typing.TypeGuard[type["Node"]]:
16
+ maybe_node = maybe_node if isinstance(maybe_node, type) else typing.get_origin(maybe_node)
17
17
  return (
18
18
  isinstance(maybe_node, type)
19
19
  and issubclass(maybe_node, Node)
@@ -4,7 +4,7 @@ import typing
4
4
  from fntypes import Error, Ok, Result
5
5
  from fntypes.error import UnwrapError
6
6
 
7
- from telegrinder.api import API
7
+ from telegrinder.api.api import API
8
8
  from telegrinder.bot.cute_types.update import Update, UpdateCute
9
9
  from telegrinder.bot.dispatch.context import Context
10
10
  from telegrinder.modules import logger
@@ -41,7 +41,7 @@ async def compose_node(
41
41
  async def compose_nodes(
42
42
  nodes: dict[str, type[Node]],
43
43
  ctx: Context,
44
- data: dict[type, typing.Any] | None = None,
44
+ data: dict[type[typing.Any], typing.Any] | None = None,
45
45
  ) -> Result["NodeCollection", ComposeError]:
46
46
  logger.debug("Composing nodes: {!r}...", nodes)
47
47
 
@@ -67,11 +67,7 @@ async def compose_nodes(
67
67
  local_nodes[node_t] = getattr(node_t, GLOBAL_VALUE_KEY)
68
68
  continue
69
69
 
70
- subnodes = {
71
- k: session.value
72
- for k, session in
73
- (local_nodes | event_nodes).items()
74
- }
70
+ subnodes = {k: session.value for k, session in (local_nodes | event_nodes).items()}
75
71
 
76
72
  try:
77
73
  local_nodes[node_t] = await compose_node(node_t, subnodes | data)
@@ -175,11 +171,11 @@ class Composition:
175
171
  case Ok(col):
176
172
  return col
177
173
  case Error(err):
178
- logger.debug(f"Composition failed with error: {err}")
174
+ logger.debug(f"Composition failed with error: {err!r}")
179
175
  return None
180
176
 
181
177
  async def __call__(self, **kwargs: typing.Any) -> typing.Any:
182
- return await self.func(**magic_bundle(self.func, kwargs, start_idx=0, bundle_ctx=False)) # type: ignore
178
+ return await self.func(**magic_bundle(self.func, kwargs, start_idx=0, bundle_ctx=False))
183
179
 
184
180
 
185
181
  __all__ = ("Composition", "NodeCollection", "NodeSession", "compose_node", "compose_nodes")
@@ -12,7 +12,12 @@ class ContainerNode(Node):
12
12
 
13
13
  @classmethod
14
14
  def get_subnodes(cls) -> dict[str, type[Node]]:
15
- return {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
15
+ subnodes = getattr(cls, "subnodes", None)
16
+ if subnodes is None:
17
+ subnodes_dct = {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
18
+ setattr(cls, "subnodes", subnodes_dct)
19
+ return subnodes_dct
20
+ return subnodes
16
21
 
17
22
  @classmethod
18
23
  def link_nodes(cls, linked_nodes: list[type[Node]]) -> type["ContainerNode"]:
@@ -13,21 +13,21 @@ 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__}")
16
+ logger.debug(f"Composing polimorphic 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
 
20
- for i, impl in enumerate(get_impls(cls)):
21
- logger.debug("Checking impl {}", impl.__name__)
22
- composition = Composition(impl, True)
20
+ for i, impl_ in enumerate(get_impls(cls)):
21
+ logger.debug("Checking impl {!r}...", impl_.__name__)
22
+ composition = Composition(impl_, True)
23
23
  node_collection = await composition.compose_nodes(update, context)
24
24
  if node_collection is None:
25
- logger.debug("Impl {!r} composition failed", impl.__name__)
25
+ logger.debug("Impl {!r} composition failed!", impl_.__name__)
26
26
  continue
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("Morph is already cached as per_event node, using its value. Impl {!r} succeeded!", impl_.__name__)
31
31
  res: NodeSession = node_ctx[(cls, i)]
32
32
  await node_collection.close_all()
33
33
  return res.value
@@ -40,7 +40,7 @@ class Polymorphic(Node):
40
40
  node_ctx[(cls, i)] = NodeSession(cls, result, {})
41
41
 
42
42
  await node_collection.close_all(with_value=result)
43
- logger.debug("Impl {!r} succeeded with value {}", impl.__name__, result)
43
+ logger.debug("Impl {!r} succeeded with value: {}", impl_.__name__, result)
44
44
  return result
45
45
 
46
46
  raise ComposeError("No implementation found.")
telegrinder/node/rule.py CHANGED
@@ -11,7 +11,7 @@ if typing.TYPE_CHECKING:
11
11
  from telegrinder.bot.rules.abc import ABCRule
12
12
 
13
13
 
14
- class RuleChain(dict[str, typing.Any]):
14
+ class RuleChain(dict[str, typing.Any], Node):
15
15
  dataclass = dict
16
16
  rules: tuple["ABCRule", ...] = ()
17
17
 
@@ -28,7 +28,6 @@ class RuleChain(dict[str, typing.Any]):
28
28
  def __class_getitem__(cls, items: "ABCRule | tuple[ABCRule, ...]", /) -> typing.Self:
29
29
  if not isinstance(items, tuple):
30
30
  items = (items,)
31
- assert all(isinstance(rule, "ABCRule") for rule in items), "All items must be instances of 'ABCRule'."
32
31
  return cls(*items)
33
32
 
34
33
  @staticmethod
@@ -37,6 +36,7 @@ class RuleChain(dict[str, typing.Any]):
37
36
 
38
37
  @classmethod
39
38
  async def compose(cls, update: UpdateNode) -> typing.Any:
39
+ # Hack to avoid circular import
40
40
  globalns = globals()
41
41
  if "check_rule" not in globalns:
42
42
  globalns.update(
@@ -54,7 +54,9 @@ class RuleChain(dict[str, typing.Any]):
54
54
  raise ComposeError(f"Rule {rule!r} failed!")
55
55
 
56
56
  try:
57
- return cls.dataclass(**ctx) # type: ignore
57
+ if dataclasses.is_dataclass(cls.dataclass):
58
+ return cls.dataclass(**{k: ctx[k] for k in cls.__annotations__})
59
+ return cls.dataclass(**ctx)
58
60
  except Exception as exc:
59
61
  raise ComposeError(f"Dataclass validation error: {exc}")
60
62
 
@@ -63,7 +65,7 @@ class RuleChain(dict[str, typing.Any]):
63
65
  return cls
64
66
 
65
67
  @classmethod
66
- def get_subnodes(cls) -> dict:
68
+ def get_subnodes(cls) -> dict[typing.Literal["update"], type[UpdateNode]]:
67
69
  return {"update": UpdateNode}
68
70
 
69
71
  @classmethod
telegrinder/node/scope.py CHANGED
@@ -34,11 +34,11 @@ def global_node(node: T) -> T:
34
34
 
35
35
 
36
36
  __all__ = (
37
+ "GLOBAL",
37
38
  "NodeScope",
38
- "PER_EVENT",
39
39
  "PER_CALL",
40
+ "PER_EVENT",
41
+ "global_node",
40
42
  "per_call",
41
43
  "per_event",
42
- "global_node",
43
- "GLOBAL",
44
44
  )
@@ -39,7 +39,9 @@ class Source(Polymorphic, DataNode):
39
39
  )
40
40
 
41
41
  @impl
42
- async def compose_chat_join_request(cls, chat_join_request: EventNode[ChatJoinRequestCute]) -> typing.Self:
42
+ async def compose_chat_join_request(
43
+ cls, chat_join_request: EventNode[ChatJoinRequestCute]
44
+ ) -> typing.Self:
43
45
  return cls(
44
46
  api=chat_join_request.ctx_api,
45
47
  chat=chat_join_request.chat,
@@ -68,4 +70,4 @@ class UserSource(ScalarNode, User):
68
70
  return source.from_user
69
71
 
70
72
 
71
- __all__ = ("Source", "ChatSource", "UserSource")
73
+ __all__ = ("ChatSource", "Source", "UserSource")
@@ -20,13 +20,13 @@ def error_on_none(value: T | None) -> T:
20
20
 
21
21
 
22
22
  def generate_node(
23
- subnodes: tuple[type["Node"], ...],
24
- func: typing.Callable[..., T],
23
+ subnodes: tuple[type[Node], ...],
24
+ func: typing.Callable[..., typing.Any],
25
25
  casts: tuple[typing.Callable[[typing.Any], typing.Any], ...] = (cast_false_to_none, error_on_none),
26
- ) -> type["Node"]:
27
- async def compose(**kw: typing.Any) -> typing.Any:
26
+ ) -> type[Node]:
27
+ async def compose(cls, **kw) -> typing.Any:
28
28
  args = await ContainerNode.compose(**kw)
29
- result = func(*args)
29
+ result = func(*args) # type: ignore
30
30
  if inspect.isawaitable(result):
31
31
  result = await result
32
32
  for cast in casts:
@@ -34,7 +34,8 @@ def generate_node(
34
34
  return result
35
35
 
36
36
  container = ContainerNode.link_nodes(list(subnodes))
37
- return type("_ContainerNode", (container,), {"compose": compose})
37
+ compose.__annotations__ = container.get_subnodes()
38
+ return type("_ContainerNode", (container,), {"compose": classmethod(compose)})
38
39
 
39
40
 
40
41
  __all__ = ("generate_node",)
telegrinder/rules.py CHANGED
@@ -26,7 +26,6 @@ __all__ = (
26
26
  "InlineQueryMarkup",
27
27
  "InlineQueryRule",
28
28
  "InlineQueryText",
29
- "IsInteger",
30
29
  "IntegerInRange",
31
30
  "InviteLinkByCreator",
32
31
  "InviteLinkName",
@@ -39,6 +38,7 @@ __all__ = (
39
38
  "IsForward",
40
39
  "IsForwardType",
41
40
  "IsGroup",
41
+ "IsInteger",
42
42
  "IsLanguageCode",
43
43
  "IsPremium",
44
44
  "IsPrivate",
@@ -50,11 +50,11 @@ __all__ = (
50
50
  "Markup",
51
51
  "MessageEntities",
52
52
  "MessageRule",
53
+ "NodeRule",
53
54
  "NotRule",
54
55
  "OrRule",
55
56
  "Regex",
56
57
  "RuleEnum",
57
58
  "StartCommand",
58
59
  "Text",
59
- "NodeRule",
60
60
  )
@@ -74,6 +74,7 @@ __all__ = (
74
74
  "ABCGlobalContext",
75
75
  "ABCI18n",
76
76
  "ABCLoopWrapper",
77
+ "ABCStateStorage",
77
78
  "ABCTranslator",
78
79
  "ABCTranslatorMiddleware",
79
80
  "AnyMarkup",
@@ -101,6 +102,7 @@ __all__ = (
101
102
  "LimitedDict",
102
103
  "Link",
103
104
  "LoopWrapper",
105
+ "MemoryStateStorage",
104
106
  "Mention",
105
107
  "ParseMode",
106
108
  "PreCode",
@@ -111,8 +113,14 @@ __all__ = (
111
113
  "SpecialFormat",
112
114
  "StartBotLink",
113
115
  "StartGroupLink",
116
+ "StateData",
114
117
  "TelegrinderContext",
115
118
  "TgEmoji",
119
+ "block_quote",
120
+ "bold",
121
+ "channel_boost_link",
122
+ "code_inline",
123
+ "ctx_var",
116
124
  "escape",
117
125
  "get_channel_boost_link",
118
126
  "get_invite_chat_link",
@@ -120,12 +128,14 @@ __all__ = (
120
128
  "get_resolve_domain_link",
121
129
  "get_start_bot_link",
122
130
  "get_start_group_link",
131
+ "impl",
123
132
  "invite_chat_link",
124
133
  "italic",
125
134
  "link",
126
135
  "magic_bundle",
127
136
  "mention",
128
137
  "pre_code",
138
+ "resolve_arg_names",
129
139
  "resolve_domain",
130
140
  "spoiler",
131
141
  "start_bot_link",
@@ -133,14 +143,4 @@ __all__ = (
133
143
  "strike",
134
144
  "tg_emoji",
135
145
  "underline",
136
- "bold",
137
- "channel_boost_link",
138
- "code_inline",
139
- "ctx_var",
140
- "block_quote",
141
- "impl",
142
- "resolve_arg_names",
143
- "ABCStateStorage",
144
- "MemoryStateStorage",
145
- "StateData",
146
146
  )
@@ -26,6 +26,11 @@ class KeyboardModel:
26
26
  keyboard: list[list[DictStrAny]]
27
27
 
28
28
 
29
+
30
+ def copy_keyboard(keyboard: list[list[DictStrAny]]) -> list[list[DictStrAny]]:
31
+ return [row.copy() for row in keyboard]
32
+
33
+
29
34
  class ABCMarkup(ABC, typing.Generic[ButtonT]):
30
35
  BUTTON: type[ButtonT]
31
36
  keyboard: list[list[DictStrAny]]
@@ -73,7 +78,7 @@ class ABCMarkup(ABC, typing.Generic[ButtonT]):
73
78
  return copy_keyboard
74
79
 
75
80
  def merge(self, other: typing.Self) -> typing.Self:
76
- self.keyboard.extend(other.keyboard)
81
+ self.keyboard.extend(copy_keyboard(other.keyboard))
77
82
  return self
78
83
 
79
84
 
@@ -44,17 +44,16 @@ class DelayedTask(typing.Generic[CoroFunc]):
44
44
  def is_cancelled(self) -> bool:
45
45
  return self._cancelled
46
46
 
47
- async def __call__(self, *args, **kwargs) -> None:
47
+ async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
48
48
  while not self.is_cancelled:
49
49
  await asyncio.sleep(self.seconds)
50
50
  if self.is_cancelled:
51
51
  break
52
52
  try:
53
53
  await self.handler(*args, **kwargs)
54
- except Exception as e:
55
- logger.exception("Error in delayed task: {}", str(e))
56
- if not self.repeat:
57
- break
54
+ finally:
55
+ if not self.repeat:
56
+ break
58
57
 
59
58
  def cancel(self) -> None:
60
59
  if not self._cancelled:
@@ -6,7 +6,7 @@ from functools import wraps
6
6
 
7
7
  if typing.TYPE_CHECKING:
8
8
  from telegrinder.bot.rules.abc import ABCRule
9
- from telegrinder.node.base import Node
9
+ from telegrinder.node.polymorphic import Polymorphic
10
10
 
11
11
  T = typing.TypeVar("T", bound=ABCRule)
12
12
  F = typing.TypeVar(
@@ -23,7 +23,6 @@ IMPL_MARK: typing.Final[str] = "_is_impl"
23
23
 
24
24
  def cache_magic_value(mark_key: str, /):
25
25
  def inner(func: "F") -> "F":
26
-
27
26
  @wraps(func)
28
27
  def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
29
28
  if mark_key not in args[0].__dict__:
@@ -31,6 +30,7 @@ def cache_magic_value(mark_key: str, /):
31
30
  return args[0].__dict__[mark_key]
32
31
 
33
32
  return wrapper # type: ignore
33
+
34
34
  return inner
35
35
 
36
36
 
@@ -43,13 +43,13 @@ def get_default_args(func: FuncType) -> dict[str, typing.Any]:
43
43
  fspec = inspect.getfullargspec(func)
44
44
  if not fspec.defaults:
45
45
  return {}
46
- return dict(zip(fspec.args[-len(fspec.defaults):], fspec.defaults))
46
+ return dict(zip(fspec.args[-len(fspec.defaults) :], fspec.defaults))
47
47
 
48
48
 
49
49
  def get_annotations(func: FuncType, *, return_type: bool = False) -> dict[str, typing.Any]:
50
50
  annotations = func.__annotations__
51
51
  if not return_type and "return" in func.__annotations__:
52
- annotations.pop("return")
52
+ annotations.pop("return")
53
53
  return annotations
54
54
 
55
55
 
@@ -60,13 +60,11 @@ def to_str(s: str | enum.Enum) -> str:
60
60
 
61
61
 
62
62
  @typing.overload
63
- def magic_bundle(handler: FuncType, kw: dict[str, typing.Any]) -> dict[str, typing.Any]:
64
- ...
63
+ def magic_bundle(handler: FuncType, kw: dict[str, typing.Any]) -> dict[str, typing.Any]: ...
65
64
 
66
65
 
67
66
  @typing.overload
68
- def magic_bundle(handler: FuncType, kw: dict[enum.Enum, typing.Any]) -> dict[str, typing.Any]:
69
- ...
67
+ def magic_bundle(handler: FuncType, kw: dict[enum.Enum, typing.Any]) -> dict[str, typing.Any]: ...
70
68
 
71
69
 
72
70
  @typing.overload
@@ -76,8 +74,7 @@ def magic_bundle(
76
74
  *,
77
75
  start_idx: int = 1,
78
76
  bundle_ctx: bool = True,
79
- ) -> dict[str, typing.Any]:
80
- ...
77
+ ) -> dict[str, typing.Any]: ...
81
78
 
82
79
 
83
80
  @typing.overload
@@ -87,8 +84,7 @@ def magic_bundle(
87
84
  *,
88
85
  start_idx: int = 1,
89
86
  bundle_ctx: bool = True,
90
- ) -> dict[str, typing.Any]:
91
- ...
87
+ ) -> dict[str, typing.Any]: ...
92
88
 
93
89
 
94
90
  @typing.overload
@@ -97,8 +93,7 @@ def magic_bundle(
97
93
  kw: dict[type, typing.Any],
98
94
  *,
99
95
  typebundle: typing.Literal[True] = True,
100
- ) -> dict[str, typing.Any]:
101
- ...
96
+ ) -> dict[str, typing.Any]: ...
102
97
 
103
98
 
104
99
  def magic_bundle(
@@ -109,7 +104,6 @@ def magic_bundle(
109
104
  bundle_ctx: bool = True,
110
105
  typebundle: bool = False,
111
106
  ) -> dict[str, typing.Any]:
112
-
113
107
  if typebundle:
114
108
  types = get_annotations(handler, return_type=False)
115
109
  bundle: dict[str, typing.Any] = {}
@@ -141,13 +135,17 @@ def impl(method: typing.Callable[..., typing.Any]):
141
135
  return classmethod(method)
142
136
 
143
137
 
144
- def get_impls(cls: type["Node"]) -> list[typing.Callable[..., typing.Any]]:
145
- return [
138
+ def get_impls(cls: type["Polymorphic"]) -> list[typing.Callable[..., typing.Any]]:
139
+ moprh_impls = getattr(cls, "__morph_impls__", None)
140
+ if moprh_impls is not None:
141
+ return moprh_impls
142
+ impls = [
146
143
  func.__func__
147
144
  for func in vars(cls).values()
148
145
  if isinstance(func, classmethod) and getattr(func.__func__, IMPL_MARK, False)
149
146
  ]
150
-
147
+ setattr(cls, "__morph_impls__", impls)
148
+ return impls
151
149
 
152
150
 
153
151
  __all__ = (
@@ -158,8 +156,8 @@ __all__ = (
158
156
  "get_cached_translation",
159
157
  "get_default_args",
160
158
  "get_default_args",
161
- "impl",
162
159
  "get_impls",
160
+ "impl",
163
161
  "magic_bundle",
164
162
  "resolve_arg_names",
165
163
  "to_str",
@@ -1,4 +1,4 @@
1
- from .abc import ABCStateStorage, StateData
2
- from .memory import MemoryStateStorage
1
+ from telegrinder.tools.state_storage.abc import ABCStateStorage, StateData
2
+ from telegrinder.tools.state_storage.memory import MemoryStateStorage
3
3
 
4
- __all__ = ("ABCStateStorage", "StateData", "MemoryStateStorage")
4
+ __all__ = ("ABCStateStorage", "MemoryStateStorage", "StateData")
@@ -1,4 +1,5 @@
1
1
  import abc
2
+ import enum
2
3
  import typing
3
4
  from dataclasses import dataclass
4
5
 
@@ -9,25 +10,26 @@ from telegrinder.bot.rules.state import State, StateMeta
9
10
  Payload = typing.TypeVar("Payload")
10
11
 
11
12
 
12
- @dataclass
13
+ @dataclass(frozen=True, slots=True)
13
14
  class StateData(typing.Generic[Payload]):
14
- key: str
15
+ key: str | enum.Enum
15
16
  payload: Payload
16
17
 
17
18
 
18
19
  class ABCStateStorage(abc.ABC, typing.Generic[Payload]):
19
20
  @abc.abstractmethod
20
- async def get(self, user_id: int) -> Option[StateData[Payload]]:
21
- ...
21
+ async def get(self, user_id: int) -> Option[StateData[Payload]]: ...
22
22
 
23
23
  @abc.abstractmethod
24
- async def delete(self, user_id: int) -> None:
25
- ...
24
+ async def delete(self, user_id: int) -> None: ...
26
25
 
27
26
  @abc.abstractmethod
28
- async def set(self, user_id: int, key: str, payload: Payload) -> None:
29
- ...
27
+ async def set(self, user_id: int, key: str | enum.Enum, payload: Payload) -> None: ...
28
+
29
+ def State(self, key: str | StateMeta | enum.Enum = StateMeta.ANY, /) -> State[Payload]: # noqa: N802
30
+ """Can be used as a shortcut to get a state rule dependant on current storage."""
30
31
 
31
- def State(self, key: str | StateMeta = StateMeta.ANY) -> State: # noqa: N802
32
- """Can be used as a shortcut to get a state rule dependant on current storage"""
33
32
  return State(storage=self, key=key)
33
+
34
+
35
+ __all__ = ("ABCStateStorage", "StateData")
@@ -2,13 +2,13 @@ import typing
2
2
 
3
3
  from fntypes import Nothing, Option, Some
4
4
 
5
- from .abc import ABCStateStorage, StateData
5
+ from telegrinder.tools.state_storage.abc import ABCStateStorage, StateData
6
6
 
7
- Payload = dict[str, typing.Any]
7
+ Payload: typing.TypeAlias = dict[str, typing.Any]
8
8
 
9
9
 
10
10
  class MemoryStateStorage(ABCStateStorage[Payload]):
11
- def __init__(self):
11
+ def __init__(self) -> None:
12
12
  self.storage: dict[int, StateData[Payload]] = {}
13
13
 
14
14
  async def get(self, user_id: int) -> Option[StateData[Payload]]:
@@ -20,3 +20,6 @@ class MemoryStateStorage(ABCStateStorage[Payload]):
20
20
 
21
21
  async def delete(self, user_id: int) -> None:
22
22
  self.storage.pop(user_id)
23
+
24
+
25
+ __all__ = ("MemoryStateStorage",)
@@ -178,6 +178,7 @@ __all__ = (
178
178
  "PaidMediaInfo",
179
179
  "PaidMediaPhoto",
180
180
  "PaidMediaPreview",
181
+ "PaidMediaPurchased",
181
182
  "PaidMediaVideo",
182
183
  "PassportData",
183
184
  "PassportElementError",