telegrinder 0.1.dev170__py3-none-any.whl → 0.1.dev171__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 (79) hide show
  1. telegrinder/api/abc.py +7 -1
  2. telegrinder/api/api.py +12 -3
  3. telegrinder/api/error.py +2 -1
  4. telegrinder/bot/bot.py +6 -1
  5. telegrinder/bot/cute_types/base.py +144 -17
  6. telegrinder/bot/cute_types/callback_query.py +6 -1
  7. telegrinder/bot/cute_types/chat_member_updated.py +1 -2
  8. telegrinder/bot/cute_types/message.py +23 -11
  9. telegrinder/bot/cute_types/update.py +48 -0
  10. telegrinder/bot/cute_types/utils.py +2 -465
  11. telegrinder/bot/dispatch/__init__.py +2 -3
  12. telegrinder/bot/dispatch/abc.py +6 -3
  13. telegrinder/bot/dispatch/context.py +6 -6
  14. telegrinder/bot/dispatch/dispatch.py +61 -23
  15. telegrinder/bot/dispatch/handler/abc.py +2 -2
  16. telegrinder/bot/dispatch/handler/func.py +36 -17
  17. telegrinder/bot/dispatch/handler/message_reply.py +2 -2
  18. telegrinder/bot/dispatch/middleware/abc.py +2 -2
  19. telegrinder/bot/dispatch/process.py +10 -10
  20. telegrinder/bot/dispatch/return_manager/abc.py +3 -3
  21. telegrinder/bot/dispatch/view/abc.py +12 -15
  22. telegrinder/bot/dispatch/view/box.py +73 -62
  23. telegrinder/bot/dispatch/view/message.py +11 -3
  24. telegrinder/bot/dispatch/view/raw.py +3 -0
  25. telegrinder/bot/dispatch/waiter_machine/machine.py +2 -2
  26. telegrinder/bot/dispatch/waiter_machine/middleware.py +1 -1
  27. telegrinder/bot/dispatch/waiter_machine/short_state.py +2 -1
  28. telegrinder/bot/polling/polling.py +3 -3
  29. telegrinder/bot/rules/abc.py +11 -7
  30. telegrinder/bot/rules/adapter/event.py +7 -4
  31. telegrinder/bot/rules/adapter/node.py +1 -1
  32. telegrinder/bot/rules/command.py +5 -7
  33. telegrinder/bot/rules/func.py +1 -1
  34. telegrinder/bot/rules/fuzzy.py +1 -1
  35. telegrinder/bot/rules/integer.py +1 -2
  36. telegrinder/bot/rules/markup.py +3 -3
  37. telegrinder/bot/rules/message_entities.py +1 -1
  38. telegrinder/bot/rules/node.py +2 -2
  39. telegrinder/bot/rules/regex.py +1 -1
  40. telegrinder/bot/rules/rule_enum.py +1 -1
  41. telegrinder/bot/scenario/checkbox.py +2 -2
  42. telegrinder/model.py +85 -46
  43. telegrinder/modules.py +3 -3
  44. telegrinder/msgspec_utils.py +33 -5
  45. telegrinder/node/__init__.py +20 -11
  46. telegrinder/node/attachment.py +19 -16
  47. telegrinder/node/base.py +120 -24
  48. telegrinder/node/callback_query.py +5 -9
  49. telegrinder/node/command.py +6 -2
  50. telegrinder/node/composer.py +82 -54
  51. telegrinder/node/container.py +4 -4
  52. telegrinder/node/event.py +59 -0
  53. telegrinder/node/me.py +3 -0
  54. telegrinder/node/message.py +6 -10
  55. telegrinder/node/polymorphic.py +11 -12
  56. telegrinder/node/rule.py +27 -5
  57. telegrinder/node/source.py +10 -11
  58. telegrinder/node/text.py +4 -4
  59. telegrinder/node/update.py +1 -2
  60. telegrinder/py.typed +0 -0
  61. telegrinder/tools/__init__.py +2 -2
  62. telegrinder/tools/buttons.py +5 -10
  63. telegrinder/tools/error_handler/error.py +2 -0
  64. telegrinder/tools/error_handler/error_handler.py +1 -1
  65. telegrinder/tools/formatting/spec_html_formats.py +10 -10
  66. telegrinder/tools/global_context/__init__.py +2 -2
  67. telegrinder/tools/global_context/global_context.py +2 -2
  68. telegrinder/tools/global_context/telegrinder_ctx.py +4 -4
  69. telegrinder/tools/keyboard.py +2 -2
  70. telegrinder/tools/loop_wrapper/loop_wrapper.py +39 -5
  71. telegrinder/tools/magic.py +48 -15
  72. telegrinder/types/enums.py +1 -0
  73. telegrinder/types/methods.py +14 -5
  74. telegrinder/types/objects.py +3 -0
  75. {telegrinder-0.1.dev170.dist-info → telegrinder-0.1.dev171.dist-info}/METADATA +2 -2
  76. telegrinder-0.1.dev171.dist-info/RECORD +145 -0
  77. telegrinder-0.1.dev170.dist-info/RECORD +0 -143
  78. {telegrinder-0.1.dev170.dist-info → telegrinder-0.1.dev171.dist-info}/LICENSE +0 -0
  79. {telegrinder-0.1.dev170.dist-info → telegrinder-0.1.dev171.dist-info}/WHEEL +0 -0
@@ -0,0 +1,59 @@
1
+ import typing
2
+
3
+ import msgspec
4
+
5
+ from telegrinder.bot.dispatch.context import Context
6
+ from telegrinder.msgspec_utils import DataclassInstance
7
+ from telegrinder.node.base import BaseNode, ComposeError, DataNode
8
+ from telegrinder.node.update import UpdateNode
9
+
10
+ if typing.TYPE_CHECKING:
11
+ Dataclass = typing.TypeVar("Dataclass", bound="DataclassType")
12
+
13
+ DataclassType: typing.TypeAlias = DataclassInstance | DataNode | msgspec.Struct | dict[str, typing.Any]
14
+
15
+ EVENT_NODE_KEY = "_event_node"
16
+
17
+
18
+ if typing.TYPE_CHECKING:
19
+ EventNode: typing.TypeAlias = typing.Annotated["Dataclass", ...]
20
+
21
+ else:
22
+ from telegrinder.msgspec_utils import decoder
23
+
24
+ class EventNode(BaseNode):
25
+ dataclass: type["DataclassType"]
26
+
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
+
33
+ def __class_getitem__(cls, dataclass: type["DataclassType"], /) -> type[typing.Self]:
34
+ return cls(dataclass)
35
+
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
+
40
+ try:
41
+ if issubclass(dataclass_type, dict):
42
+ dataclass = cls.dataclass(**raw_update.incoming_update.to_full_dict())
43
+
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
+ )
49
+
50
+ else:
51
+ dataclass = cls.dataclass(**raw_update.incoming_update.to_dict())
52
+
53
+ ctx[EVENT_NODE_KEY] = cls
54
+ return dataclass
55
+ except Exception:
56
+ raise ComposeError(f"Cannot parse update to {cls.dataclass.__name__!r}.")
57
+
58
+
59
+ __all__ = ("EventNode",)
telegrinder/node/me.py CHANGED
@@ -12,3 +12,6 @@ class Me(ScalarNode, User):
12
12
  async def compose(cls, api: API) -> User:
13
13
  me = await api.get_me()
14
14
  return me.expect(ComposeError("Can't complete get_me request"))
15
+
16
+
17
+ __all__ = ("Me",)
@@ -1,18 +1,14 @@
1
- from telegrinder.bot.cute_types import MessageCute
2
-
3
- from .base import ComposeError, ScalarNode
4
- from .update import UpdateNode
1
+ from telegrinder.bot.cute_types.message import MessageCute
2
+ from telegrinder.node.base import ComposeError, ScalarNode
3
+ from telegrinder.node.update import UpdateNode
5
4
 
6
5
 
7
6
  class MessageNode(ScalarNode, MessageCute):
8
7
  @classmethod
9
- async def compose(cls, update: UpdateNode) -> "MessageNode":
8
+ async def compose(cls, update: UpdateNode) -> MessageCute:
10
9
  if not update.message:
11
- raise ComposeError
12
- return MessageNode(
13
- **update.message.unwrap().to_dict(),
14
- api=update.api,
15
- )
10
+ raise ComposeError("Update is not a message.")
11
+ return update.message.unwrap()
16
12
 
17
13
 
18
14
  __all__ = ("MessageNode",)
@@ -2,38 +2,37 @@ import inspect
2
2
  import typing
3
3
 
4
4
  from telegrinder.bot.dispatch.context import Context
5
- from telegrinder.tools.magic import get_impls, impl
5
+ from telegrinder.node.base import BaseNode, ComposeError
6
+ from telegrinder.node.composer import CONTEXT_STORE_NODES_KEY, Composition, NodeSession
7
+ from telegrinder.node.scope import NodeScope
8
+ from telegrinder.node.update import UpdateNode
9
+ from telegrinder.tools.magic import IMPL_MARK, get_impls_by_key, impl
6
10
 
7
- from .base import ComposeError
8
- from .composer import CONTEXT_STORE_NODES_KEY, Composition, NodeSession
9
- from .scope import NodeScope
10
- from .update import UpdateNode
11
11
 
12
-
13
- class Polymorphic:
12
+ class Polymorphic(BaseNode):
14
13
  @classmethod
15
14
  async def compose(cls, update: UpdateNode, context: Context) -> typing.Any:
16
15
  scope = getattr(cls, "scope", None)
17
16
  node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
18
17
 
19
- for i, impl in enumerate(get_impls(cls)):
20
- composition = Composition(impl, True)
18
+ for i, impl in enumerate(get_impls_by_key(cls, IMPL_MARK).values()):
19
+ composition = Composition(impl, True, node_class=cls)
21
20
  node_collection = await composition.compose_nodes(update, context)
22
21
  if node_collection is None:
23
22
  continue
24
23
 
25
24
  # To determine whether this is a right morph, all subnodes must be resolved
26
25
  if scope is NodeScope.PER_EVENT and (cls, i) in node_ctx:
27
- result: NodeSession = node_ctx[(cls, i)]
26
+ res: NodeSession = node_ctx[(cls, i)]
28
27
  await node_collection.close_all()
29
- return result.value
28
+ return res.value
30
29
 
31
30
  result = composition.func(cls, **node_collection.values())
32
31
  if inspect.isawaitable(result):
33
32
  result = await result
34
33
 
35
34
  if scope is NodeScope.PER_EVENT:
36
- node_ctx[(cls, i)] = NodeSession(cls, result, {}) # type: ignore
35
+ node_ctx[(cls, i)] = NodeSession(cls, result, {})
37
36
 
38
37
  await node_collection.close_all(with_value=result)
39
38
  return result
telegrinder/node/rule.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import dataclasses
2
+ import importlib
2
3
  import typing
3
4
 
4
5
  from telegrinder.bot.dispatch.context import Context
@@ -6,6 +7,7 @@ from telegrinder.node.base import ComposeError, Node
6
7
  from telegrinder.node.update import UpdateNode
7
8
 
8
9
  if typing.TYPE_CHECKING:
10
+ from telegrinder.bot.dispatch.process import check_rule
9
11
  from telegrinder.bot.rules.abc import ABCRule
10
12
 
11
13
 
@@ -13,7 +15,9 @@ class RuleChain(dict[str, typing.Any]):
13
15
  dataclass = dict
14
16
  rules: tuple["ABCRule", ...] = ()
15
17
 
16
- def __init_subclass__(cls) -> None:
18
+ def __init_subclass__(cls, *args: typing.Any, **kwargs: typing.Any) -> None:
19
+ super().__init_subclass__(*args, **kwargs)
20
+
17
21
  if cls.__name__ == "_RuleNode":
18
22
  return
19
23
  cls.dataclass = cls.generate_node_dataclass(cls)
@@ -21,7 +25,7 @@ class RuleChain(dict[str, typing.Any]):
21
25
  def __new__(cls, *rules: "ABCRule") -> type[Node]:
22
26
  return type("_RuleNode", (cls,), {"dataclass": dict, "rules": rules}) # type: ignore
23
27
 
24
- def __class_getitem__(cls, items: typing.Union[tuple["ABCRule", ...], "ABCRule"]) -> typing.Self:
28
+ def __class_getitem__(cls, items: "ABCRule | tuple[ABCRule, ...]", /) -> typing.Self:
25
29
  if not isinstance(items, tuple):
26
30
  items = (items,)
27
31
  assert all(isinstance(rule, "ABCRule") for rule in items), "All items must be instances of 'ABCRule'."
@@ -32,13 +36,23 @@ class RuleChain(dict[str, typing.Any]):
32
36
  return dataclasses.dataclass(type(cls_.__name__, (object,), dict(cls_.__dict__)))
33
37
 
34
38
  @classmethod
35
- async def compose(cls, update: UpdateNode):
36
- from telegrinder.bot.dispatch.process import check_rule
39
+ async def compose(cls, update: UpdateNode) -> typing.Any:
40
+ globalns = globals()
41
+ if "check_rule" not in globalns:
42
+ globalns.update(
43
+ {
44
+ "check_rule": getattr(
45
+ importlib.import_module("telegrinder.bot.dispatch.process"),
46
+ "check_rule",
47
+ ),
48
+ },
49
+ )
37
50
 
38
51
  ctx = Context()
39
52
  for rule in cls.rules:
40
53
  if not await check_rule(update.api, rule, update, ctx):
41
- raise ComposeError
54
+ raise ComposeError(f"Rule {rule!r} failed!")
55
+
42
56
  try:
43
57
  return cls.dataclass(**ctx) # type: ignore
44
58
  except Exception as exc:
@@ -52,6 +66,14 @@ class RuleChain(dict[str, typing.Any]):
52
66
  def get_sub_nodes(cls) -> dict:
53
67
  return {"update": UpdateNode}
54
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
+
55
77
  @classmethod
56
78
  def is_generator(cls) -> typing.Literal[False]:
57
79
  return False
@@ -1,18 +1,17 @@
1
1
  import dataclasses
2
2
  import typing
3
3
 
4
- from fntypes import Nothing, Option
4
+ from fntypes.option import Nothing, Option
5
5
 
6
- from telegrinder.api import API
7
- from telegrinder.types import Chat, Message, User
6
+ from telegrinder.api.api import API
7
+ from telegrinder.node.base import ComposeError, DataNode, ScalarNode
8
+ from telegrinder.node.callback_query import CallbackQueryNode
9
+ from telegrinder.node.message import MessageNode
10
+ from telegrinder.node.polymorphic import Polymorphic, impl
11
+ from telegrinder.types.objects import Chat, Message, User
8
12
 
9
- from .base import ComposeError, DataNode, ScalarNode
10
- from .callback_query import CallbackQueryNode
11
- from .message import MessageNode
12
- from .polymorphic import Polymorphic, impl
13
13
 
14
-
15
- @dataclasses.dataclass(kw_only=True)
14
+ @dataclasses.dataclass(kw_only=True, slots=True)
16
15
  class Source(Polymorphic, DataNode):
17
16
  api: API
18
17
  chat: Chat
@@ -24,7 +23,7 @@ class Source(Polymorphic, DataNode):
24
23
  return cls(
25
24
  api=message.ctx_api,
26
25
  chat=message.chat,
27
- from_user=message.from_user,
26
+ from_user=message.from_.expect(ComposeError("MessageNode has no from_user")),
28
27
  thread_id=message.message_thread_id,
29
28
  )
30
29
 
@@ -32,7 +31,7 @@ class Source(Polymorphic, DataNode):
32
31
  async def compose_callback_query(cls, callback_query: CallbackQueryNode) -> typing.Self:
33
32
  return cls(
34
33
  api=callback_query.ctx_api,
35
- chat=callback_query.chat.expect(ComposeError),
34
+ chat=callback_query.chat.expect(ComposeError("CallbackQueryNode has no chat")),
36
35
  from_user=callback_query.from_user,
37
36
  thread_id=callback_query.message_thread_id,
38
37
  )
telegrinder/node/text.py CHANGED
@@ -1,12 +1,12 @@
1
- from .base import ComposeError, ScalarNode
2
- from .message import MessageNode
1
+ from telegrinder.node.base import ComposeError, ScalarNode
2
+ from telegrinder.node.message import MessageNode
3
3
 
4
4
 
5
5
  class Text(ScalarNode, str):
6
6
  @classmethod
7
7
  async def compose(cls, message: MessageNode) -> str:
8
8
  if not message.text:
9
- raise ComposeError("Message has no text")
9
+ raise ComposeError("Message has no text.")
10
10
  return message.text.unwrap()
11
11
 
12
12
 
@@ -14,7 +14,7 @@ class TextInteger(ScalarNode, int):
14
14
  @classmethod
15
15
  async def compose(cls, text: Text) -> int:
16
16
  if not text.isdigit():
17
- raise ComposeError("Text is not digit")
17
+ raise ComposeError("Text is not digit.")
18
18
  return int(text)
19
19
 
20
20
 
@@ -1,6 +1,5 @@
1
1
  from telegrinder.bot.cute_types import UpdateCute
2
-
3
- from .base import ScalarNode
2
+ from telegrinder.node.base import ScalarNode
4
3
 
5
4
 
6
5
  class UpdateNode(ScalarNode, UpdateCute):
telegrinder/py.typed ADDED
File without changes
@@ -43,7 +43,7 @@ from .global_context import (
43
43
  CtxVar,
44
44
  GlobalContext,
45
45
  GlobalCtxVar,
46
- TelegrinderCtx,
46
+ TelegrinderContext,
47
47
  ctx_var,
48
48
  )
49
49
  from .i18n import (
@@ -110,7 +110,7 @@ __all__ = (
110
110
  "SpecialFormat",
111
111
  "StartBotLink",
112
112
  "StartGroupLink",
113
- "TelegrinderCtx",
113
+ "TelegrinderContext",
114
114
  "TgEmoji",
115
115
  "escape",
116
116
  "get_channel_boost_link",
@@ -3,25 +3,20 @@ import typing
3
3
 
4
4
  import msgspec
5
5
 
6
- from telegrinder.model import encoder
7
- from telegrinder.types import (
6
+ from telegrinder.msgspec_utils import DataclassInstance, encoder
7
+ from telegrinder.types.objects import (
8
8
  CallbackGame,
9
9
  KeyboardButtonPollType,
10
10
  KeyboardButtonRequestChat,
11
11
  KeyboardButtonRequestUsers,
12
+ LoginUrl,
12
13
  SwitchInlineQueryChosenChat,
13
14
  WebAppInfo,
14
15
  )
15
- from telegrinder.types.objects import LoginUrl
16
16
 
17
17
  ButtonT = typing.TypeVar("ButtonT", bound="BaseButton")
18
18
 
19
19
 
20
- @typing.runtime_checkable
21
- class DataclassInstance(typing.Protocol):
22
- __dataclass_fields__: typing.ClassVar[dict[str, dataclasses.Field[typing.Any]]]
23
-
24
-
25
20
  @dataclasses.dataclass
26
21
  class BaseButton:
27
22
  def get_data(self) -> dict[str, typing.Any]:
@@ -44,7 +39,7 @@ class RowButtons(typing.Generic[ButtonT]):
44
39
  return [b.get_data() for b in self.buttons]
45
40
 
46
41
 
47
- @dataclasses.dataclass
42
+ @dataclasses.dataclass(slots=True)
48
43
  class Button(BaseButton):
49
44
  text: str
50
45
  request_contact: bool = dataclasses.field(default=False, kw_only=True)
@@ -61,7 +56,7 @@ class Button(BaseButton):
61
56
  web_app: dict[str, typing.Any] | WebAppInfo | None = dataclasses.field(default=None, kw_only=True)
62
57
 
63
58
 
64
- @dataclasses.dataclass
59
+ @dataclasses.dataclass(slots=True)
65
60
  class InlineButton(BaseButton):
66
61
  text: str
67
62
  url: str | None = dataclasses.field(default=None, kw_only=True)
@@ -1,4 +1,6 @@
1
1
  class CatcherError(BaseException):
2
+ __slots__ = ("exc", "message")
3
+
2
4
  def __init__(self, exc: BaseException, message: str) -> None:
3
5
  self.exc = exc
4
6
  self.message = message
@@ -16,7 +16,7 @@ ExceptionT = typing.TypeVar("ExceptionT", bound=BaseException, contravariant=Tru
16
16
  FuncCatcher = typing.Callable[typing.Concatenate[ExceptionT, ...], typing.Awaitable[typing.Any]]
17
17
 
18
18
 
19
- @dataclasses.dataclass(frozen=True, repr=False)
19
+ @dataclasses.dataclass(frozen=True, repr=False, slots=True)
20
20
  class Catcher(typing.Generic[EventT]):
21
21
  func: FuncCatcher[BaseException]
22
22
  exceptions: list[type[BaseException] | BaseException] = dataclasses.field(
@@ -24,7 +24,7 @@ def is_spec_format(obj: typing.Any) -> typing.TypeGuard[SpecialFormat]:
24
24
  )
25
25
 
26
26
 
27
- @dataclasses.dataclass(repr=False)
27
+ @dataclasses.dataclass(repr=False, slots=True)
28
28
  class BaseSpecFormat:
29
29
  __formatter_name__: typing.ClassVar[str] = dataclasses.field(init=False, repr=False)
30
30
 
@@ -32,7 +32,7 @@ class BaseSpecFormat:
32
32
  return f"<Special formatter {self.__class__.__name__!r} -> {self.__formatter_name__!r}>"
33
33
 
34
34
 
35
- @dataclasses.dataclass(repr=False)
35
+ @dataclasses.dataclass(repr=False, slots=True)
36
36
  class ChannelBoostLink(BaseSpecFormat):
37
37
  __formatter_name__ = "channel_boost_link"
38
38
 
@@ -40,7 +40,7 @@ class ChannelBoostLink(BaseSpecFormat):
40
40
  string: str | None = None
41
41
 
42
42
 
43
- @dataclasses.dataclass(repr=False)
43
+ @dataclasses.dataclass(repr=False, slots=True)
44
44
  class InviteChatLink(BaseSpecFormat):
45
45
  __formatter_name__ = "invite_chat_link"
46
46
 
@@ -48,7 +48,7 @@ class InviteChatLink(BaseSpecFormat):
48
48
  string: str | None = None
49
49
 
50
50
 
51
- @dataclasses.dataclass(repr=False)
51
+ @dataclasses.dataclass(repr=False, slots=True)
52
52
  class Mention(BaseSpecFormat):
53
53
  __formatter_name__ = "mention"
54
54
 
@@ -56,7 +56,7 @@ class Mention(BaseSpecFormat):
56
56
  user_id: int
57
57
 
58
58
 
59
- @dataclasses.dataclass(repr=False)
59
+ @dataclasses.dataclass(repr=False, slots=True)
60
60
  class Link(BaseSpecFormat):
61
61
  __formatter_name__ = "link"
62
62
 
@@ -64,7 +64,7 @@ class Link(BaseSpecFormat):
64
64
  string: str | None = None
65
65
 
66
66
 
67
- @dataclasses.dataclass(repr=False)
67
+ @dataclasses.dataclass(repr=False, slots=True)
68
68
  class PreCode(BaseSpecFormat):
69
69
  __formatter_name__ = "pre_code"
70
70
 
@@ -72,7 +72,7 @@ class PreCode(BaseSpecFormat):
72
72
  lang: str | ProgrammingLanguage | None = None
73
73
 
74
74
 
75
- @dataclasses.dataclass(repr=False)
75
+ @dataclasses.dataclass(repr=False, slots=True)
76
76
  class TgEmoji(BaseSpecFormat):
77
77
  __formatter_name__ = "tg_emoji"
78
78
 
@@ -80,7 +80,7 @@ class TgEmoji(BaseSpecFormat):
80
80
  emoji_id: int
81
81
 
82
82
 
83
- @dataclasses.dataclass(repr=False)
83
+ @dataclasses.dataclass(repr=False, slots=True)
84
84
  class StartBotLink(BaseSpecFormat):
85
85
  __formatter_name__ = "start_bot_link"
86
86
 
@@ -89,7 +89,7 @@ class StartBotLink(BaseSpecFormat):
89
89
  string: str | None
90
90
 
91
91
 
92
- @dataclasses.dataclass(repr=False)
92
+ @dataclasses.dataclass(repr=False, slots=True)
93
93
  class StartGroupLink(BaseSpecFormat):
94
94
  __formatter_name__ = "start_group_link"
95
95
 
@@ -98,7 +98,7 @@ class StartGroupLink(BaseSpecFormat):
98
98
  string: str | None = None
99
99
 
100
100
 
101
- @dataclasses.dataclass(repr=False)
101
+ @dataclasses.dataclass(repr=False, slots=True)
102
102
  class ResolveDomain(BaseSpecFormat):
103
103
  __formatter_name__ = "resolve_domain"
104
104
 
@@ -1,12 +1,12 @@
1
1
  from .abc import ABCGlobalContext, CtxVar, GlobalCtxVar
2
2
  from .global_context import GlobalContext, ctx_var
3
- from .telegrinder_ctx import TelegrinderCtx
3
+ from .telegrinder_ctx import TelegrinderContext
4
4
 
5
5
  __all__ = (
6
6
  "ABCGlobalContext",
7
7
  "CtxVar",
8
8
  "GlobalContext",
9
9
  "GlobalCtxVar",
10
- "TelegrinderCtx",
10
+ "TelegrinderContext",
11
11
  "ctx_var",
12
12
  )
@@ -81,7 +81,7 @@ def ctx_var(value: T, *, const: bool = False) -> T:
81
81
  return typing.cast(T, CtxVar(value, const=const))
82
82
 
83
83
 
84
- @dataclasses.dataclass(frozen=True, eq=False)
84
+ @dataclasses.dataclass(frozen=True, eq=False, slots=True)
85
85
  class RootAttr:
86
86
  name: str
87
87
  can_be_read: bool = dataclasses.field(default=True, kw_only=True)
@@ -91,7 +91,7 @@ class RootAttr:
91
91
  return self.name == __value
92
92
 
93
93
 
94
- @dataclasses.dataclass(repr=False, frozen=True)
94
+ @dataclasses.dataclass(repr=False, frozen=True, slots=True)
95
95
  class Storage:
96
96
  _storage: dict[str, "GlobalContext"] = dataclasses.field(
97
97
  default_factory=lambda: {},
@@ -5,14 +5,14 @@ import vbml
5
5
  from telegrinder.tools.global_context import GlobalContext, ctx_var
6
6
 
7
7
 
8
- class TelegrinderCtx(GlobalContext):
8
+ class TelegrinderContext(GlobalContext):
9
9
  """Basic type-hinted telegrinder context with context name `"telegrinder"`.
10
10
 
11
11
  You can use this class or GlobalContext:
12
12
  ```
13
- from telegrinder.tools.global_context import GlobalContext, TelegrinderCtx
13
+ from telegrinder.tools.global_context import GlobalContext, TelegrinderContext
14
14
 
15
- ctx1 = TelegrinderCtx()
15
+ ctx1 = TelegrinderContext()
16
16
  ctx2 = GlobalContext("telegrinder") # same, but without the type-hints
17
17
  assert ctx1 == ctx2 # ok
18
18
  ```"""
@@ -22,4 +22,4 @@ class TelegrinderCtx(GlobalContext):
22
22
  vbml_patcher: typing.ClassVar[vbml.Patcher] = ctx_var(vbml.Patcher(), const=True)
23
23
 
24
24
 
25
- __all__ = ("TelegrinderCtx",)
25
+ __all__ = ("TelegrinderContext",)
@@ -17,7 +17,7 @@ DictStrAny: typing.TypeAlias = dict[str, typing.Any]
17
17
  AnyMarkup: typing.TypeAlias = InlineKeyboardMarkup | ReplyKeyboardMarkup
18
18
 
19
19
 
20
- @dataclasses.dataclass(kw_only=True)
20
+ @dataclasses.dataclass(kw_only=True, slots=True)
21
21
  class KeyboardModel:
22
22
  resize_keyboard: bool
23
23
  one_time_keyboard: bool
@@ -77,7 +77,7 @@ class ABCMarkup(ABC, typing.Generic[ButtonT]):
77
77
  return self
78
78
 
79
79
 
80
- @dataclasses.dataclass(kw_only=True)
80
+ @dataclasses.dataclass(kw_only=True, slots=True)
81
81
  class Keyboard(ABCMarkup[Button], KeyboardModel):
82
82
  BUTTON = Button
83
83
 
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import contextlib
3
3
  import dataclasses
4
+ import datetime
4
5
  import typing
5
6
 
6
7
  from telegrinder.modules import logger
@@ -32,7 +33,7 @@ def to_coroutine_task(task: Task) -> CoroutineTask[typing.Any]:
32
33
  return task
33
34
 
34
35
 
35
- @dataclasses.dataclass
36
+ @dataclasses.dataclass(slots=True)
36
37
  class DelayedTask(typing.Generic[CoroFunc]):
37
38
  handler: CoroFunc
38
39
  seconds: float
@@ -60,7 +61,7 @@ class DelayedTask(typing.Generic[CoroFunc]):
60
61
  self._cancelled = True
61
62
 
62
63
 
63
- @dataclasses.dataclass(kw_only=True)
64
+ @dataclasses.dataclass(kw_only=True, slots=True)
64
65
  class Lifespan:
65
66
  startup_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
66
67
  shutdown_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
@@ -86,10 +87,11 @@ class LoopWrapper(ABCLoopWrapper):
86
87
  *,
87
88
  tasks: list[CoroutineTask[typing.Any]] | None = None,
88
89
  lifespan: Lifespan | None = None,
90
+ event_loop: asyncio.AbstractEventLoop | None = None,
89
91
  ) -> None:
90
92
  self.tasks: list[CoroutineTask[typing.Any]] = tasks or []
91
93
  self.lifespan = lifespan or Lifespan()
92
- self._loop = asyncio.new_event_loop()
94
+ self._loop = event_loop or asyncio.new_event_loop()
93
95
 
94
96
  def __repr__(self) -> str:
95
97
  return "<{}: loop={!r} with tasks={!r}, lifespan={!r}>".format(
@@ -143,6 +145,10 @@ class LoopWrapper(ABCLoopWrapper):
143
145
  with contextlib.suppress(asyncio.CancelledError):
144
146
  self._loop.run_until_complete(task_to_cancel)
145
147
 
148
+ @typing.overload
149
+ def timer(self, *, seconds: datetime.timedelta) -> typing.Callable[..., typing.Any]: ...
150
+
151
+ @typing.overload
146
152
  def timer(
147
153
  self,
148
154
  *,
@@ -150,7 +156,19 @@ class LoopWrapper(ABCLoopWrapper):
150
156
  hours: int = 0,
151
157
  minutes: int = 0,
152
158
  seconds: float = 0,
153
- ):
159
+ ) -> typing.Callable[..., typing.Any]: ...
160
+
161
+ def timer(
162
+ self,
163
+ *,
164
+ days: int = 0,
165
+ hours: int = 0,
166
+ minutes: int = 0,
167
+ seconds: float | datetime.timedelta = 0,
168
+ ) -> typing.Callable[..., typing.Any]:
169
+ if isinstance(seconds, datetime.timedelta):
170
+ seconds = seconds.total_seconds()
171
+
154
172
  seconds += minutes * 60
155
173
  seconds += hours * 60 * 60
156
174
  seconds += days * 24 * 60 * 60
@@ -161,6 +179,10 @@ class LoopWrapper(ABCLoopWrapper):
161
179
 
162
180
  return decorator
163
181
 
182
+ @typing.overload
183
+ def interval(self, *, seconds: datetime.timedelta) -> typing.Callable[..., typing.Any]: ...
184
+
185
+ @typing.overload
164
186
  def interval(
165
187
  self,
166
188
  *,
@@ -168,7 +190,19 @@ class LoopWrapper(ABCLoopWrapper):
168
190
  hours: int = 0,
169
191
  minutes: int = 0,
170
192
  seconds: float = 0,
171
- ):
193
+ ) -> typing.Callable[..., typing.Any]: ...
194
+
195
+ def interval(
196
+ self,
197
+ *,
198
+ days: int = 0,
199
+ hours: int = 0,
200
+ minutes: int = 0,
201
+ seconds: float | datetime.timedelta = 0,
202
+ ) -> typing.Callable[..., typing.Any]:
203
+ if isinstance(seconds, datetime.timedelta):
204
+ seconds = seconds.total_seconds()
205
+
172
206
  seconds += minutes * 60
173
207
  seconds += hours * 60 * 60
174
208
  seconds += days * 24 * 60 * 60