telegrinder 0.1.dev171__py3-none-any.whl → 0.2.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 (92) hide show
  1. telegrinder/__init__.py +2 -2
  2. telegrinder/api/__init__.py +1 -2
  3. telegrinder/api/api.py +3 -3
  4. telegrinder/api/token.py +36 -0
  5. telegrinder/bot/__init__.py +12 -6
  6. telegrinder/bot/bot.py +12 -5
  7. telegrinder/bot/cute_types/__init__.py +7 -7
  8. telegrinder/bot/cute_types/base.py +7 -32
  9. telegrinder/bot/cute_types/callback_query.py +5 -6
  10. telegrinder/bot/cute_types/chat_join_request.py +4 -5
  11. telegrinder/bot/cute_types/chat_member_updated.py +3 -4
  12. telegrinder/bot/cute_types/inline_query.py +3 -4
  13. telegrinder/bot/cute_types/message.py +9 -10
  14. telegrinder/bot/cute_types/update.py +8 -9
  15. telegrinder/bot/cute_types/utils.py +1 -1
  16. telegrinder/bot/dispatch/__init__.py +9 -9
  17. telegrinder/bot/dispatch/abc.py +2 -2
  18. telegrinder/bot/dispatch/context.py +11 -2
  19. telegrinder/bot/dispatch/dispatch.py +18 -33
  20. telegrinder/bot/dispatch/handler/__init__.py +3 -3
  21. telegrinder/bot/dispatch/handler/abc.py +3 -3
  22. telegrinder/bot/dispatch/handler/func.py +17 -12
  23. telegrinder/bot/dispatch/handler/message_reply.py +6 -7
  24. telegrinder/bot/dispatch/middleware/__init__.py +1 -1
  25. telegrinder/bot/dispatch/process.py +30 -11
  26. telegrinder/bot/dispatch/return_manager/__init__.py +4 -4
  27. telegrinder/bot/dispatch/return_manager/callback_query.py +1 -2
  28. telegrinder/bot/dispatch/return_manager/inline_query.py +1 -2
  29. telegrinder/bot/dispatch/return_manager/message.py +1 -2
  30. telegrinder/bot/dispatch/view/__init__.py +8 -8
  31. telegrinder/bot/dispatch/view/abc.py +9 -4
  32. telegrinder/bot/dispatch/view/box.py +2 -2
  33. telegrinder/bot/dispatch/view/callback_query.py +1 -2
  34. telegrinder/bot/dispatch/view/chat_join_request.py +1 -2
  35. telegrinder/bot/dispatch/view/chat_member.py +16 -2
  36. telegrinder/bot/dispatch/view/inline_query.py +1 -2
  37. telegrinder/bot/dispatch/view/message.py +1 -2
  38. telegrinder/bot/dispatch/view/raw.py +8 -10
  39. telegrinder/bot/dispatch/waiter_machine/__init__.py +3 -3
  40. telegrinder/bot/dispatch/waiter_machine/machine.py +10 -6
  41. telegrinder/bot/dispatch/waiter_machine/short_state.py +2 -2
  42. telegrinder/bot/polling/abc.py +1 -1
  43. telegrinder/bot/polling/polling.py +3 -3
  44. telegrinder/bot/rules/__init__.py +20 -20
  45. telegrinder/bot/rules/abc.py +50 -40
  46. telegrinder/bot/rules/adapter/__init__.py +5 -5
  47. telegrinder/bot/rules/adapter/abc.py +6 -3
  48. telegrinder/bot/rules/adapter/errors.py +2 -1
  49. telegrinder/bot/rules/adapter/event.py +27 -15
  50. telegrinder/bot/rules/adapter/node.py +28 -22
  51. telegrinder/bot/rules/adapter/raw_update.py +13 -5
  52. telegrinder/bot/rules/callback_data.py +4 -4
  53. telegrinder/bot/rules/chat_join.py +4 -4
  54. telegrinder/bot/rules/func.py +1 -1
  55. telegrinder/bot/rules/inline.py +3 -3
  56. telegrinder/bot/rules/markup.py +3 -1
  57. telegrinder/bot/rules/message_entities.py +1 -1
  58. telegrinder/bot/rules/text.py +1 -2
  59. telegrinder/bot/rules/update.py +1 -2
  60. telegrinder/bot/scenario/abc.py +2 -2
  61. telegrinder/bot/scenario/checkbox.py +1 -2
  62. telegrinder/bot/scenario/choice.py +1 -2
  63. telegrinder/model.py +6 -1
  64. telegrinder/msgspec_utils.py +55 -55
  65. telegrinder/node/__init__.py +1 -3
  66. telegrinder/node/base.py +14 -86
  67. telegrinder/node/composer.py +71 -74
  68. telegrinder/node/container.py +3 -3
  69. telegrinder/node/event.py +40 -31
  70. telegrinder/node/polymorphic.py +12 -6
  71. telegrinder/node/rule.py +1 -9
  72. telegrinder/node/scope.py +9 -1
  73. telegrinder/node/source.py +11 -0
  74. telegrinder/node/update.py +6 -2
  75. telegrinder/rules.py +59 -0
  76. telegrinder/tools/error_handler/abc.py +2 -2
  77. telegrinder/tools/error_handler/error_handler.py +5 -5
  78. telegrinder/tools/global_context/global_context.py +1 -1
  79. telegrinder/tools/keyboard.py +1 -1
  80. telegrinder/tools/loop_wrapper/loop_wrapper.py +9 -9
  81. telegrinder/tools/magic.py +64 -19
  82. telegrinder/types/__init__.py +1 -0
  83. telegrinder/types/enums.py +1 -0
  84. telegrinder/types/methods.py +78 -11
  85. telegrinder/types/objects.py +46 -24
  86. telegrinder/verification_utils.py +1 -3
  87. {telegrinder-0.1.dev171.dist-info → telegrinder-0.2.0.dist-info}/METADATA +1 -1
  88. telegrinder-0.2.0.dist-info/RECORD +145 -0
  89. telegrinder/api/abc.py +0 -79
  90. telegrinder-0.1.dev171.dist-info/RECORD +0 -145
  91. {telegrinder-0.1.dev171.dist-info → telegrinder-0.2.0.dist-info}/LICENSE +0 -0
  92. {telegrinder-0.1.dev171.dist-info → telegrinder-0.2.0.dist-info}/WHEEL +0 -0
telegrinder/node/event.py CHANGED
@@ -2,9 +2,11 @@ import typing
2
2
 
3
3
  import msgspec
4
4
 
5
+ from telegrinder.api import API
6
+ from telegrinder.bot.cute_types import BaseCute
5
7
  from telegrinder.bot.dispatch.context import Context
6
8
  from telegrinder.msgspec_utils import DataclassInstance
7
- from telegrinder.node.base import BaseNode, ComposeError, DataNode
9
+ from telegrinder.node.base import ComposeError, DataNode, Node
8
10
  from telegrinder.node.update import UpdateNode
9
11
 
10
12
  if typing.TYPE_CHECKING:
@@ -15,45 +17,52 @@ if typing.TYPE_CHECKING:
15
17
  EVENT_NODE_KEY = "_event_node"
16
18
 
17
19
 
18
- if typing.TYPE_CHECKING:
19
- EventNode: typing.TypeAlias = typing.Annotated["Dataclass", ...]
20
+ from telegrinder.msgspec_utils import decoder
20
21
 
21
- else:
22
- from telegrinder.msgspec_utils import decoder
23
22
 
24
- class EventNode(BaseNode):
25
- dataclass: type["DataclassType"]
23
+ class _EventNode(Node):
24
+ dataclass: type["DataclassType"]
25
+
26
+ def __new__(cls, dataclass: type["DataclassType"], /) -> type[typing.Self]:
27
+ namespace = dict(**cls.__dict__)
28
+ namespace.pop("__new__", None)
29
+ new_cls = type("EventNode", (cls,), {"dataclass": dataclass, **namespace})
30
+ return new_cls # type: ignore
26
31
 
27
- def __new__(cls, dataclass: type["DataclassType"], /) -> type[typing.Self]:
28
- namespace = dict(**cls.__dict__)
29
- namespace.pop("__new__", None)
30
- new_cls = type("EventNode", (cls,), {"dataclass": dataclass, **namespace})
31
- return new_cls # type: ignore
32
+ def __class_getitem__(cls, dataclass: type["DataclassType"], /) -> typing.Self:
33
+ return cls(dataclass)
32
34
 
33
- def __class_getitem__(cls, dataclass: type["DataclassType"], /) -> type[typing.Self]:
34
- return cls(dataclass)
35
+ @classmethod
36
+ async def compose(cls, raw_update: UpdateNode, ctx: Context, api: API) -> "DataclassType":
37
+ dataclass_type = typing.get_origin(cls.dataclass) or cls.dataclass
35
38
 
36
- @classmethod
37
- async def compose(cls, raw_update: UpdateNode, ctx: Context) -> "DataclassType":
38
- dataclass_type = typing.get_origin(cls.dataclass) or cls.dataclass
39
+ try:
40
+ if issubclass(dataclass_type, dict):
41
+ dataclass = cls.dataclass(**raw_update.incoming_update.to_full_dict())
39
42
 
40
- try:
41
- if issubclass(dataclass_type, dict):
42
- dataclass = cls.dataclass(**raw_update.incoming_update.to_full_dict())
43
+ elif issubclass(dataclass_type, BaseCute):
44
+ dataclass = dataclass_type.from_update(raw_update.incoming_update, bound_api=api)
43
45
 
44
- elif issubclass(dataclass_type, msgspec.Struct | DataclassInstance):
45
- dataclass = decoder.convert(
46
- raw_update.incoming_update.to_full_dict(),
47
- type=cls.dataclass,
48
- )
46
+ elif issubclass(dataclass_type, (msgspec.Struct, DataclassInstance)): # type: ignore
47
+ # FIXME: must be used with encode_name
48
+ dataclass = decoder.convert(
49
+ raw_update.incoming_update.to_full_dict(),
50
+ type=cls.dataclass,
51
+ )
49
52
 
50
- else:
51
- dataclass = cls.dataclass(**raw_update.incoming_update.to_dict())
53
+ else:
54
+ dataclass = cls.dataclass(**raw_update.incoming_update.to_dict())
52
55
 
53
- ctx[EVENT_NODE_KEY] = cls
54
- return dataclass
55
- except Exception:
56
- raise ComposeError(f"Cannot parse update to {cls.dataclass.__name__!r}.")
56
+ ctx[EVENT_NODE_KEY] = cls
57
+ return dataclass
58
+ except Exception as exc:
59
+ raise ComposeError(f"Cannot parse update into {cls.dataclass.__name__!r}, error: {exc}")
57
60
 
58
61
 
62
+ if typing.TYPE_CHECKING:
63
+ EventNode: typing.TypeAlias = typing.Annotated["Dataclass", ...]
64
+ else:
65
+ class EventNode(_EventNode):
66
+ pass
67
+
59
68
  __all__ = ("EventNode",)
@@ -2,32 +2,37 @@ import inspect
2
2
  import typing
3
3
 
4
4
  from telegrinder.bot.dispatch.context import Context
5
- from telegrinder.node.base import BaseNode, ComposeError
5
+ from telegrinder.modules import logger
6
+ from telegrinder.node.base import ComposeError, Node
6
7
  from telegrinder.node.composer import CONTEXT_STORE_NODES_KEY, Composition, NodeSession
7
8
  from telegrinder.node.scope import NodeScope
8
9
  from telegrinder.node.update import UpdateNode
9
- from telegrinder.tools.magic import IMPL_MARK, get_impls_by_key, impl
10
+ from telegrinder.tools.magic import get_impls, impl
10
11
 
11
12
 
12
- class Polymorphic(BaseNode):
13
+ class Polymorphic(Node):
13
14
  @classmethod
14
15
  async def compose(cls, update: UpdateNode, context: Context) -> typing.Any:
16
+ logger.debug(f"Composing polimorphic node {cls.__name__}")
15
17
  scope = getattr(cls, "scope", None)
16
18
  node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
17
19
 
18
- for i, impl in enumerate(get_impls_by_key(cls, IMPL_MARK).values()):
19
- composition = Composition(impl, True, node_class=cls)
20
+ for i, impl in enumerate(get_impls(cls)):
21
+ logger.debug("Checking impl {}", impl.__name__)
22
+ composition = Composition(impl, True)
20
23
  node_collection = await composition.compose_nodes(update, context)
21
24
  if node_collection is None:
25
+ logger.debug("Impl {} composition failed", impl.__name__)
22
26
  continue
23
27
 
24
28
  # To determine whether this is a right morph, all subnodes must be resolved
25
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 {} succeeded", impl.__name__)
26
31
  res: NodeSession = node_ctx[(cls, i)]
27
32
  await node_collection.close_all()
28
33
  return res.value
29
34
 
30
- result = composition.func(cls, **node_collection.values())
35
+ result = composition.func(cls, **node_collection.values)
31
36
  if inspect.isawaitable(result):
32
37
  result = await result
33
38
 
@@ -35,6 +40,7 @@ class Polymorphic(BaseNode):
35
40
  node_ctx[(cls, i)] = NodeSession(cls, result, {})
36
41
 
37
42
  await node_collection.close_all(with_value=result)
43
+ logger.debug("Impl {} succeeded with value {}", impl.__name__, result)
38
44
  return result
39
45
 
40
46
  raise ComposeError("No implementation found.")
telegrinder/node/rule.py CHANGED
@@ -63,17 +63,9 @@ class RuleChain(dict[str, typing.Any]):
63
63
  return cls
64
64
 
65
65
  @classmethod
66
- def get_sub_nodes(cls) -> dict:
66
+ def get_subnodes(cls) -> dict:
67
67
  return {"update": UpdateNode}
68
68
 
69
- @classmethod
70
- def get_compose_annotations(cls) -> dict[str, typing.Any]:
71
- return {}
72
-
73
- @classmethod
74
- def get_node_impls(cls) -> dict[str, typing.Callable[..., typing.Any]]:
75
- return {}
76
-
77
69
  @classmethod
78
70
  def is_generator(cls) -> typing.Literal[False]:
79
71
  return False
telegrinder/node/scope.py CHANGED
@@ -33,4 +33,12 @@ def global_node(node: T) -> T:
33
33
  return node
34
34
 
35
35
 
36
- __all__ = ("NodeScope", "PER_EVENT", "PER_CALL", "per_call", "per_event", "global_node", "GLOBAL")
36
+ __all__ = (
37
+ "NodeScope",
38
+ "PER_EVENT",
39
+ "PER_CALL",
40
+ "per_call",
41
+ "per_event",
42
+ "global_node",
43
+ "GLOBAL",
44
+ )
@@ -4,8 +4,10 @@ import typing
4
4
  from fntypes.option import Nothing, Option
5
5
 
6
6
  from telegrinder.api.api import API
7
+ from telegrinder.bot.cute_types import ChatJoinRequestCute
7
8
  from telegrinder.node.base import ComposeError, DataNode, ScalarNode
8
9
  from telegrinder.node.callback_query import CallbackQueryNode
10
+ from telegrinder.node.event import EventNode
9
11
  from telegrinder.node.message import MessageNode
10
12
  from telegrinder.node.polymorphic import Polymorphic, impl
11
13
  from telegrinder.types.objects import Chat, Message, User
@@ -36,6 +38,15 @@ class Source(Polymorphic, DataNode):
36
38
  thread_id=callback_query.message_thread_id,
37
39
  )
38
40
 
41
+ @impl
42
+ async def compose_chat_join_request(cls, chat_join_request: EventNode[ChatJoinRequestCute]) -> typing.Self:
43
+ return cls(
44
+ api=chat_join_request.ctx_api,
45
+ chat=chat_join_request.chat,
46
+ from_user=chat_join_request.from_user,
47
+ thread_id=Nothing(),
48
+ )
49
+
39
50
  async def send(self, text: str) -> Message:
40
51
  result = await self.api.send_message(
41
52
  chat_id=self.chat.id,
@@ -1,11 +1,15 @@
1
+ from telegrinder.api import API
1
2
  from telegrinder.bot.cute_types import UpdateCute
2
3
  from telegrinder.node.base import ScalarNode
4
+ from telegrinder.types import Update
3
5
 
4
6
 
5
7
  class UpdateNode(ScalarNode, UpdateCute):
6
8
  @classmethod
7
- async def compose(cls, update: UpdateCute) -> UpdateCute:
8
- return update
9
+ async def compose(cls, update: Update, api: API) -> UpdateCute:
10
+ if isinstance(update, UpdateCute):
11
+ return update
12
+ return UpdateCute.from_update(update, api)
9
13
 
10
14
 
11
15
  __all__ = ("UpdateNode",)
telegrinder/rules.py CHANGED
@@ -1 +1,60 @@
1
1
  from .bot.rules import * # noqa: F403
2
+
3
+ __all__ = (
4
+ "ABCRule",
5
+ "AndRule",
6
+ "Argument",
7
+ "CallbackDataEq",
8
+ "CallbackDataJsonEq",
9
+ "CallbackDataJsonModel",
10
+ "CallbackDataMap",
11
+ "CallbackDataMarkup",
12
+ "CallbackQueryDataRule",
13
+ "CallbackQueryRule",
14
+ "ChatJoinRequestRule",
15
+ "Command",
16
+ "EnumTextRule",
17
+ "FuncRule",
18
+ "FuzzyText",
19
+ "HasData",
20
+ "HasEntities",
21
+ "HasInviteLink",
22
+ "HasLocation",
23
+ "HasMention",
24
+ "HasText",
25
+ "InlineQueryChatType",
26
+ "InlineQueryMarkup",
27
+ "InlineQueryRule",
28
+ "InlineQueryText",
29
+ "IsInteger",
30
+ "IntegerInRange",
31
+ "InviteLinkByCreator",
32
+ "InviteLinkName",
33
+ "IsBot",
34
+ "IsChat",
35
+ "IsChatId",
36
+ "IsDice",
37
+ "IsDiceEmoji",
38
+ "IsForum",
39
+ "IsForward",
40
+ "IsForwardType",
41
+ "IsGroup",
42
+ "IsLanguageCode",
43
+ "IsPremium",
44
+ "IsPrivate",
45
+ "IsReply",
46
+ "IsSuperGroup",
47
+ "IsUpdateType",
48
+ "IsUser",
49
+ "IsUserId",
50
+ "Markup",
51
+ "MessageEntities",
52
+ "MessageRule",
53
+ "NotRule",
54
+ "OrRule",
55
+ "Regex",
56
+ "RuleEnum",
57
+ "StartCommand",
58
+ "Text",
59
+ "NodeRule",
60
+ )
@@ -3,7 +3,7 @@ from abc import ABC, abstractmethod
3
3
 
4
4
  from fntypes.result import Result
5
5
 
6
- from telegrinder.api import ABCAPI
6
+ from telegrinder.api import API
7
7
  from telegrinder.bot.dispatch.context import Context
8
8
 
9
9
  EventT = typing.TypeVar("EventT")
@@ -24,7 +24,7 @@ class ABCErrorHandler(ABC, typing.Generic[EventT]):
24
24
  self,
25
25
  handler: Handler[EventT],
26
26
  event: EventT,
27
- api: ABCAPI,
27
+ api: API,
28
28
  ctx: Context,
29
29
  ) -> Result[typing.Any, typing.Any]:
30
30
  """Run error handler."""
@@ -3,7 +3,7 @@ import typing
3
3
 
4
4
  from fntypes.result import Error, Ok, Result
5
5
 
6
- from telegrinder.api import ABCAPI
6
+ from telegrinder.api import API
7
7
  from telegrinder.bot.dispatch.context import Context
8
8
  from telegrinder.modules import logger
9
9
  from telegrinder.tools.magic import magic_bundle
@@ -39,7 +39,7 @@ class Catcher(typing.Generic[EventT]):
39
39
  self,
40
40
  handler: Handler[EventT],
41
41
  event: EventT,
42
- api: ABCAPI,
42
+ api: API,
43
43
  ctx: Context,
44
44
  ) -> Result[typing.Any, BaseException]:
45
45
  try:
@@ -49,7 +49,7 @@ class Catcher(typing.Generic[EventT]):
49
49
 
50
50
  async def process_exception(
51
51
  self,
52
- api: ABCAPI,
52
+ api: API,
53
53
  event: EventT,
54
54
  ctx: Context,
55
55
  exception: BaseException,
@@ -125,7 +125,7 @@ class ErrorHandler(ABCErrorHandler[EventT]):
125
125
  self,
126
126
  handler: Handler[EventT],
127
127
  event: EventT,
128
- api: ABCAPI,
128
+ api: API,
129
129
  ctx: Context,
130
130
  ) -> Result[typing.Any, BaseException]:
131
131
  assert self.catcher is not None
@@ -159,7 +159,7 @@ class ErrorHandler(ABCErrorHandler[EventT]):
159
159
  self,
160
160
  handler: Handler[EventT],
161
161
  event: EventT,
162
- api: ABCAPI,
162
+ api: API,
163
163
  ctx: Context,
164
164
  ) -> Result[typing.Any, BaseException]:
165
165
  if not self.catcher:
@@ -334,7 +334,7 @@ class GlobalContext(ABCGlobalContext, typing.Generic[CtxValueT], dict[str, Globa
334
334
  def get(self, var_name, var_value_type=object): # type: ignore
335
335
  """Get context variable by name."""
336
336
 
337
- var_value_type = typing.Any if var_value_type is object else type
337
+ var_value_type = typing.Any if var_value_type is object else var_value_type
338
338
  generic_types = typing.get_args(get_orig_class(self))
339
339
  if generic_types and var_value_type is object:
340
340
  var_value_type = generic_types[0]
@@ -94,7 +94,7 @@ class Keyboard(ABCMarkup[Button], KeyboardModel):
94
94
  self.keyboard = [row for row in self.keyboard if row]
95
95
  return {
96
96
  k: v.unwrap() if v and isinstance(v, Some) else v
97
- for k, v in self.__dict__.items()
97
+ for k, v in dataclasses.asdict(self).items()
98
98
  if type(v) not in (NoneType, Nothing)
99
99
  }
100
100
 
@@ -61,23 +61,23 @@ class DelayedTask(typing.Generic[CoroFunc]):
61
61
  self._cancelled = True
62
62
 
63
63
 
64
- @dataclasses.dataclass(kw_only=True, slots=True)
64
+ @dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
65
65
  class Lifespan:
66
66
  startup_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
67
67
  shutdown_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
68
68
 
69
- def on_startup(self, task_or_func: Task) -> Task:
70
- self.startup_tasks.append(to_coroutine_task(task_or_func))
71
- return task_or_func
69
+ def on_startup(self, task: Task, /) -> Task:
70
+ self.startup_tasks.append(to_coroutine_task(task))
71
+ return task
72
72
 
73
- def on_shutdown(self, task_or_func: Task) -> Task:
74
- self.shutdown_tasks.append(to_coroutine_task(task_or_func))
75
- return task_or_func
73
+ def on_shutdown(self, task: Task, /) -> Task:
74
+ self.shutdown_tasks.append(to_coroutine_task(task))
75
+ return task
76
76
 
77
- def start(self, loop: asyncio.AbstractEventLoop) -> None:
77
+ def start(self, loop: asyncio.AbstractEventLoop, /) -> None:
78
78
  run_tasks(self.startup_tasks, loop)
79
79
 
80
- def shutdown(self, loop: asyncio.AbstractEventLoop) -> None:
80
+ def shutdown(self, loop: asyncio.AbstractEventLoop, /) -> None:
81
81
  run_tasks(self.shutdown_tasks, loop)
82
82
 
83
83
 
@@ -15,16 +15,15 @@ if typing.TYPE_CHECKING:
15
15
  )
16
16
 
17
17
  Impl: typing.TypeAlias = type[classmethod]
18
- NodeImpl: typing.TypeAlias = Impl
19
18
  FuncType: typing.TypeAlias = types.FunctionType | typing.Callable[..., typing.Any]
20
19
 
21
20
  TRANSLATIONS_KEY: typing.Final[str] = "_translations"
22
21
  IMPL_MARK: typing.Final[str] = "_is_impl"
23
- NODE_IMPL_MARK: typing.Final[str] = "_is_node_impl"
24
22
 
25
23
 
26
24
  def cache_magic_value(mark_key: str, /):
27
25
  def inner(func: "F") -> "F":
26
+
28
27
  @wraps(func)
29
28
  def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
30
29
  if mark_key not in args[0].__dict__:
@@ -32,7 +31,6 @@ def cache_magic_value(mark_key: str, /):
32
31
  return args[0].__dict__[mark_key]
33
32
 
34
33
  return wrapper # type: ignore
35
-
36
34
  return inner
37
35
 
38
36
 
@@ -49,9 +47,10 @@ def get_default_args(func: FuncType) -> dict[str, typing.Any]:
49
47
 
50
48
 
51
49
  def get_annotations(func: FuncType, *, return_type: bool = False) -> dict[str, typing.Any]:
50
+ annotations = func.__annotations__
52
51
  if not return_type and "return" in func.__annotations__:
53
- del func.__annotations__["return"]
54
- return func.__annotations__
52
+ annotations.pop("return")
53
+ return annotations
55
54
 
56
55
 
57
56
  def to_str(s: str | enum.Enum) -> str:
@@ -60,13 +59,64 @@ def to_str(s: str | enum.Enum) -> str:
60
59
  return s
61
60
 
62
61
 
62
+ @typing.overload
63
+ def magic_bundle(handler: FuncType, kw: dict[str, typing.Any]) -> dict[str, typing.Any]:
64
+ ...
65
+
66
+
67
+ @typing.overload
68
+ def magic_bundle(handler: FuncType, kw: dict[enum.Enum, typing.Any]) -> dict[str, typing.Any]:
69
+ ...
70
+
71
+
72
+ @typing.overload
73
+ def magic_bundle(
74
+ handler: FuncType,
75
+ kw: dict[str, typing.Any],
76
+ *,
77
+ start_idx: int = 1,
78
+ bundle_ctx: bool = True,
79
+ ) -> dict[str, typing.Any]:
80
+ ...
81
+
82
+
83
+ @typing.overload
84
+ def magic_bundle(
85
+ handler: FuncType,
86
+ kw: dict[enum.Enum, typing.Any],
87
+ *,
88
+ start_idx: int = 1,
89
+ bundle_ctx: bool = True,
90
+ ) -> dict[str, typing.Any]:
91
+ ...
92
+
93
+
94
+ @typing.overload
63
95
  def magic_bundle(
64
96
  handler: FuncType,
65
- kw: dict[str | enum.Enum, typing.Any],
97
+ kw: dict[type, typing.Any],
98
+ *,
99
+ typebundle: typing.Literal[True] = True,
100
+ ) -> dict[str, typing.Any]:
101
+ ...
102
+
103
+
104
+ def magic_bundle(
105
+ handler: FuncType,
106
+ kw: dict[typing.Any, typing.Any],
66
107
  *,
67
108
  start_idx: int = 1,
68
109
  bundle_ctx: bool = True,
110
+ typebundle: bool = False,
69
111
  ) -> dict[str, typing.Any]:
112
+
113
+ if typebundle:
114
+ types = get_annotations(handler, return_type=False)
115
+ bundle: dict[str, typing.Any] = {}
116
+ for name, type in types.items():
117
+ bundle[name] = kw[type]
118
+ return bundle
119
+
70
120
  names = resolve_arg_names(handler, start_idx=start_idx)
71
121
  args = get_default_args(handler)
72
122
  args.update({to_str(k): v for k, v in kw.items() if to_str(k) in names})
@@ -85,24 +135,19 @@ def cache_translation(base_rule: "T", locale: str, translated_rule: "T") -> None
85
135
  setattr(base_rule, TRANSLATIONS_KEY, translations)
86
136
 
87
137
 
88
- def get_impls_by_key(cls: type["Node"], mark_key: str) -> dict[str, typing.Callable[..., typing.Any]]:
89
- return {
90
- name: func.__func__
91
- for name, func in vars(cls).items()
92
- if isinstance(func, classmethod) and getattr(func.__func__, mark_key, False)
93
- }
94
-
95
-
96
138
  @typing.cast(typing.Callable[..., Impl], lambda f: f)
97
139
  def impl(method: typing.Callable[..., typing.Any]):
98
140
  setattr(method, IMPL_MARK, True)
99
141
  return classmethod(method)
100
142
 
101
143
 
102
- @typing.cast(typing.Callable[..., NodeImpl], lambda f: f)
103
- def node_impl(method: typing.Callable[..., typing.Any]):
104
- setattr(method, NODE_IMPL_MARK, True)
105
- return classmethod(method)
144
+ def get_impls(cls: type["Node"]) -> list[typing.Callable[..., typing.Any]]:
145
+ return [
146
+ func.__func__
147
+ for func in vars(cls).values()
148
+ if isinstance(func, classmethod) and getattr(func.__func__, IMPL_MARK, False)
149
+ ]
150
+
106
151
 
107
152
 
108
153
  __all__ = (
@@ -114,8 +159,8 @@ __all__ = (
114
159
  "get_default_args",
115
160
  "get_default_args",
116
161
  "impl",
162
+ "get_impls",
117
163
  "magic_bundle",
118
- "node_impl",
119
164
  "resolve_arg_names",
120
165
  "to_str",
121
166
  )
@@ -205,6 +205,7 @@ __all__ = (
205
205
  "ReactionType",
206
206
  "ReactionTypeCustomEmoji",
207
207
  "ReactionTypeEmoji",
208
+ "ReactionTypePaid",
208
209
  "ReactionTypeType",
209
210
  "RefundedPayment",
210
211
  "ReplyKeyboardMarkup",
@@ -443,6 +443,7 @@ class ChatType(str, enum.Enum):
443
443
  GROUP = "group"
444
444
  SUPERGROUP = "supergroup"
445
445
  CHANNEL = "channel"
446
+ SENDER = "sender"
446
447
 
447
448
 
448
449
  class ChatMemberStatus(str, enum.Enum):