telegrinder 0.1.dev20__py3-none-any.whl → 0.1.dev158__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 (132) hide show
  1. telegrinder/__init__.py +129 -22
  2. telegrinder/api/__init__.py +11 -2
  3. telegrinder/api/abc.py +25 -9
  4. telegrinder/api/api.py +29 -24
  5. telegrinder/api/error.py +14 -4
  6. telegrinder/api/response.py +11 -7
  7. telegrinder/bot/__init__.py +68 -7
  8. telegrinder/bot/bot.py +30 -24
  9. telegrinder/bot/cute_types/__init__.py +11 -1
  10. telegrinder/bot/cute_types/base.py +47 -0
  11. telegrinder/bot/cute_types/callback_query.py +64 -14
  12. telegrinder/bot/cute_types/inline_query.py +22 -16
  13. telegrinder/bot/cute_types/message.py +145 -53
  14. telegrinder/bot/cute_types/update.py +23 -0
  15. telegrinder/bot/dispatch/__init__.py +56 -3
  16. telegrinder/bot/dispatch/abc.py +9 -7
  17. telegrinder/bot/dispatch/composition.py +74 -0
  18. telegrinder/bot/dispatch/context.py +71 -0
  19. telegrinder/bot/dispatch/dispatch.py +86 -49
  20. telegrinder/bot/dispatch/handler/__init__.py +3 -0
  21. telegrinder/bot/dispatch/handler/abc.py +11 -5
  22. telegrinder/bot/dispatch/handler/func.py +41 -32
  23. telegrinder/bot/dispatch/handler/message_reply.py +46 -0
  24. telegrinder/bot/dispatch/middleware/__init__.py +2 -0
  25. telegrinder/bot/dispatch/middleware/abc.py +10 -4
  26. telegrinder/bot/dispatch/process.py +53 -49
  27. telegrinder/bot/dispatch/return_manager/__init__.py +19 -0
  28. telegrinder/bot/dispatch/return_manager/abc.py +95 -0
  29. telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
  30. telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
  31. telegrinder/bot/dispatch/return_manager/message.py +25 -0
  32. telegrinder/bot/dispatch/view/__init__.py +14 -2
  33. telegrinder/bot/dispatch/view/abc.py +121 -2
  34. telegrinder/bot/dispatch/view/box.py +38 -0
  35. telegrinder/bot/dispatch/view/callback_query.py +13 -39
  36. telegrinder/bot/dispatch/view/inline_query.py +11 -39
  37. telegrinder/bot/dispatch/view/message.py +11 -47
  38. telegrinder/bot/dispatch/waiter_machine/__init__.py +9 -0
  39. telegrinder/bot/dispatch/waiter_machine/machine.py +116 -0
  40. telegrinder/bot/dispatch/waiter_machine/middleware.py +76 -0
  41. telegrinder/bot/dispatch/waiter_machine/short_state.py +37 -0
  42. telegrinder/bot/polling/__init__.py +2 -0
  43. telegrinder/bot/polling/abc.py +11 -4
  44. telegrinder/bot/polling/polling.py +89 -40
  45. telegrinder/bot/rules/__init__.py +91 -5
  46. telegrinder/bot/rules/abc.py +81 -63
  47. telegrinder/bot/rules/adapter/__init__.py +11 -0
  48. telegrinder/bot/rules/adapter/abc.py +21 -0
  49. telegrinder/bot/rules/adapter/errors.py +5 -0
  50. telegrinder/bot/rules/adapter/event.py +43 -0
  51. telegrinder/bot/rules/adapter/raw_update.py +24 -0
  52. telegrinder/bot/rules/callback_data.py +159 -38
  53. telegrinder/bot/rules/command.py +116 -0
  54. telegrinder/bot/rules/enum_text.py +28 -0
  55. telegrinder/bot/rules/func.py +17 -17
  56. telegrinder/bot/rules/fuzzy.py +13 -10
  57. telegrinder/bot/rules/inline.py +61 -0
  58. telegrinder/bot/rules/integer.py +12 -7
  59. telegrinder/bot/rules/is_from.py +148 -7
  60. telegrinder/bot/rules/markup.py +21 -18
  61. telegrinder/bot/rules/mention.py +17 -0
  62. telegrinder/bot/rules/message_entities.py +33 -0
  63. telegrinder/bot/rules/regex.py +27 -19
  64. telegrinder/bot/rules/rule_enum.py +74 -0
  65. telegrinder/bot/rules/start.py +25 -13
  66. telegrinder/bot/rules/text.py +23 -14
  67. telegrinder/bot/scenario/__init__.py +2 -0
  68. telegrinder/bot/scenario/abc.py +12 -5
  69. telegrinder/bot/scenario/checkbox.py +48 -30
  70. telegrinder/bot/scenario/choice.py +16 -10
  71. telegrinder/client/__init__.py +2 -0
  72. telegrinder/client/abc.py +8 -21
  73. telegrinder/client/aiohttp.py +30 -21
  74. telegrinder/model.py +68 -37
  75. telegrinder/modules.py +189 -21
  76. telegrinder/msgspec_json.py +14 -0
  77. telegrinder/msgspec_utils.py +207 -0
  78. telegrinder/node/__init__.py +31 -0
  79. telegrinder/node/attachment.py +71 -0
  80. telegrinder/node/base.py +93 -0
  81. telegrinder/node/composer.py +71 -0
  82. telegrinder/node/container.py +22 -0
  83. telegrinder/node/message.py +18 -0
  84. telegrinder/node/rule.py +56 -0
  85. telegrinder/node/source.py +31 -0
  86. telegrinder/node/text.py +13 -0
  87. telegrinder/node/tools/__init__.py +3 -0
  88. telegrinder/node/tools/generator.py +40 -0
  89. telegrinder/node/update.py +12 -0
  90. telegrinder/rules.py +1 -1
  91. telegrinder/tools/__init__.py +165 -4
  92. telegrinder/tools/buttons.py +75 -51
  93. telegrinder/tools/error_handler/__init__.py +8 -0
  94. telegrinder/tools/error_handler/abc.py +30 -0
  95. telegrinder/tools/error_handler/error_handler.py +156 -0
  96. telegrinder/tools/formatting/__init__.py +81 -3
  97. telegrinder/tools/formatting/html.py +283 -37
  98. telegrinder/tools/formatting/links.py +32 -0
  99. telegrinder/tools/formatting/spec_html_formats.py +121 -0
  100. telegrinder/tools/global_context/__init__.py +12 -0
  101. telegrinder/tools/global_context/abc.py +66 -0
  102. telegrinder/tools/global_context/global_context.py +451 -0
  103. telegrinder/tools/global_context/telegrinder_ctx.py +25 -0
  104. telegrinder/tools/i18n/__init__.py +12 -0
  105. telegrinder/tools/i18n/base.py +31 -0
  106. telegrinder/tools/i18n/middleware/__init__.py +3 -0
  107. telegrinder/tools/i18n/middleware/base.py +26 -0
  108. telegrinder/tools/i18n/simple.py +48 -0
  109. telegrinder/tools/inline_query.py +684 -0
  110. telegrinder/tools/kb_set/__init__.py +2 -0
  111. telegrinder/tools/kb_set/base.py +3 -0
  112. telegrinder/tools/kb_set/yaml.py +28 -17
  113. telegrinder/tools/keyboard.py +84 -62
  114. telegrinder/tools/loop_wrapper/__init__.py +4 -0
  115. telegrinder/tools/loop_wrapper/abc.py +18 -0
  116. telegrinder/tools/loop_wrapper/loop_wrapper.py +132 -0
  117. telegrinder/tools/magic.py +48 -23
  118. telegrinder/tools/parse_mode.py +1 -2
  119. telegrinder/types/__init__.py +1 -0
  120. telegrinder/types/enums.py +651 -0
  121. telegrinder/types/methods.py +3920 -1251
  122. telegrinder/types/objects.py +4702 -1718
  123. {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev158.dist-info}/LICENSE +2 -1
  124. telegrinder-0.1.dev158.dist-info/METADATA +108 -0
  125. telegrinder-0.1.dev158.dist-info/RECORD +126 -0
  126. {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev158.dist-info}/WHEEL +1 -1
  127. telegrinder/bot/dispatch/waiter.py +0 -38
  128. telegrinder/result.py +0 -38
  129. telegrinder/tools/formatting/abc.py +0 -52
  130. telegrinder/tools/formatting/markdown.py +0 -57
  131. telegrinder-0.1.dev20.dist-info/METADATA +0 -22
  132. telegrinder-0.1.dev20.dist-info/RECORD +0 -71
@@ -1,21 +1,33 @@
1
- from .base import KeyboardSetBase, KeyboardSetError
2
- from telegrinder.tools.keyboard import Keyboard, InlineKeyboard
3
- import typing
4
- import re
5
1
  import os
2
+ import re
3
+ import typing
4
+
6
5
  import yaml
7
6
 
7
+ from telegrinder.tools.keyboard import InlineKeyboard, Keyboard
8
+
9
+ from .base import KeyboardSetBase, KeyboardSetError
10
+
11
+ PathLike = str | os.PathLike[str]
12
+
8
13
 
9
14
  class KeyboardSetYAML(KeyboardSetBase):
10
- __config__: str
15
+ __config__: PathLike
11
16
 
12
17
  @classmethod
13
18
  def load(cls) -> None:
14
19
  config_path = getattr(cls, "__config__", "keyboards.yaml")
15
20
  if not os.path.exists(config_path):
16
- raise FileNotFoundError(f"Config file for {cls.__name__} is undefined")
21
+ raise FileNotFoundError(f"Config file for {cls.__name__!r} is undefined.")
17
22
 
18
- config = yaml.load(open(config_path, "r", encoding="utf-8"), yaml.Loader)
23
+ config = yaml.load(
24
+ open( # noqa: SIM115
25
+ str(config_path),
26
+ mode="r",
27
+ encoding="UTF-8",
28
+ ),
29
+ yaml.Loader,
30
+ )
19
31
  for name, hint in typing.get_type_hints(cls).items():
20
32
  g = re.match(r"(?:kb_|keyboard_)(.+)", name.lower())
21
33
  if not g:
@@ -23,12 +35,9 @@ class KeyboardSetYAML(KeyboardSetBase):
23
35
 
24
36
  short_name = g.group(1)
25
37
  if short_name not in config:
26
- raise KeyboardSetError(
27
- f"Keyboard {short_name!r} is undefined in config"
28
- )
38
+ raise KeyboardSetError(f"Keyboard {short_name!r} is undefined in config.")
29
39
 
30
40
  kb_config = config[short_name]
31
-
32
41
  if (
33
42
  not isinstance(kb_config, dict)
34
43
  or "buttons" not in kb_config
@@ -36,18 +45,20 @@ class KeyboardSetYAML(KeyboardSetBase):
36
45
  ):
37
46
  raise KeyboardSetError(
38
47
  "Keyboard should be dict with field buttons which must be a list, "
39
- "check documentation"
48
+ "check documentation."
40
49
  )
41
-
50
+
42
51
  buttons = kb_config.pop("buttons")
43
- new_keyboard: typing.Union[Keyboard, InlineKeyboard] = hint(**kb_config)
44
-
52
+ new_keyboard: Keyboard | InlineKeyboard = hint(**kb_config)
45
53
  for button in buttons:
46
54
  if not button:
47
55
  new_keyboard.row()
48
56
  continue
49
57
  if "text" not in button:
50
- raise KeyboardSetError("text is required")
51
- new_keyboard.add(new_keyboard.BUTTON(**button))
58
+ raise KeyboardSetError("Text is required in button.")
59
+ new_keyboard.add(new_keyboard.BUTTON(**button)) # type: ignore
52
60
 
53
61
  setattr(cls, name, new_keyboard)
62
+
63
+
64
+ __all__ = ("KeyboardSetYAML",)
@@ -1,47 +1,45 @@
1
+ import dataclasses
1
2
  import typing
2
- from typing import List, Optional
3
- from dataclasses import dataclass
4
3
  from abc import ABC, abstractmethod
5
- from telegrinder.types.objects import InlineKeyboardMarkup, ReplyKeyboardMarkup
4
+ from types import NoneType
6
5
 
7
- from .buttons import Button, InlineButton, ABCButton
6
+ from fntypes.option import Nothing, Some
8
7
 
8
+ from telegrinder.msgspec_utils import Option
9
+ from telegrinder.types.objects import (
10
+ InlineKeyboardMarkup,
11
+ ReplyKeyboardMarkup,
12
+ ReplyKeyboardRemove,
13
+ )
9
14
 
10
- AnyMarkup = typing.Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]
15
+ from .buttons import Button, ButtonT, InlineButton, RowButtons
11
16
 
17
+ DictStrAny = dict[str, typing.Any]
18
+ AnyMarkup = InlineKeyboardMarkup | ReplyKeyboardMarkup
12
19
 
13
- @dataclass
14
- class KeyboardModel:
15
- resize_keyboard: bool
16
- one_time_keyboard: bool
17
- selective: bool
18
- keyboard: List[List[dict]]
19
20
 
21
+ def keyboard_remove(*, selective: bool | None = None) -> ReplyKeyboardRemove:
22
+ return ReplyKeyboardRemove(
23
+ remove_keyboard=True,
24
+ selective=Nothing() if selective is None else Some(selective),
25
+ )
20
26
 
21
- class ABCMarkup(ABC, KeyboardModel):
22
- BUTTON: typing.Type[ABCButton]
23
27
 
24
- def __init__(
25
- self,
26
- resize_keyboard: bool = True,
27
- one_time_keyboard: bool = False,
28
- selective: Optional[bool] = None,
29
- ):
30
- self.keyboard = [[]]
31
- self.resize_keyboard = resize_keyboard
32
- self.one_time_keyboard = one_time_keyboard
33
- self.selective = selective
28
+ @dataclasses.dataclass
29
+ class KeyboardModel:
30
+ resize_keyboard: bool | Option[bool]
31
+ one_time_keyboard: bool | Option[bool]
32
+ selective: bool | Option[bool]
33
+ is_persistent: bool | Option[bool]
34
+ keyboard: list[list[dict[str, typing.Any]]]
34
35
 
35
- @abstractmethod
36
- def add(self, button) -> "ABCMarkup":
37
- pass
38
36
 
39
- @abstractmethod
40
- def row(self) -> "ABCMarkup":
41
- pass
37
+ class ABCMarkup(ABC, typing.Generic[ButtonT]):
38
+ BUTTON: type[ButtonT]
39
+ keyboard: list[list[dict[str, typing.Any]]]
42
40
 
43
41
  @abstractmethod
44
- def dict(self) -> dict:
42
+ def dict(self) -> DictStrAny:
45
43
  pass
46
44
 
47
45
  @abstractmethod
@@ -52,6 +50,26 @@ class ABCMarkup(ABC, KeyboardModel):
52
50
  def empty(cls) -> AnyMarkup:
53
51
  return cls().get_markup()
54
52
 
53
+ def add(self, row_or_button: RowButtons[ButtonT] | ButtonT) -> typing.Self:
54
+ if not len(self.keyboard):
55
+ self.row()
56
+
57
+ if isinstance(row_or_button, RowButtons):
58
+ self.keyboard[-1].extend(row_or_button.get_data())
59
+ if row_or_button.auto_row:
60
+ self.row()
61
+ return self
62
+
63
+ self.keyboard[-1].append(row_or_button.get_data())
64
+ return self
65
+
66
+ def row(self) -> typing.Self:
67
+ if len(self.keyboard) and not len(self.keyboard[-1]):
68
+ raise RuntimeError("Last row is empty!")
69
+
70
+ self.keyboard.append([])
71
+ return self
72
+
55
73
  def format(self, **format_data: typing.Dict[str, str]) -> "ABCMarkup":
56
74
  copy_keyboard = self.__class__()
57
75
  for row in self.keyboard:
@@ -62,54 +80,58 @@ class ABCMarkup(ABC, KeyboardModel):
62
80
  copy_keyboard.row()
63
81
  return copy_keyboard
64
82
 
65
- def merge(self, other: "ABCMarkup") -> "ABCMarkup":
83
+ def merge(self, other: typing.Self) -> typing.Self:
66
84
  self.keyboard.extend(other.keyboard)
67
85
  return self
68
86
 
69
87
 
70
- class Keyboard(ABCMarkup):
88
+ class Keyboard(ABCMarkup[Button], KeyboardModel):
71
89
  BUTTON = Button
72
90
 
73
- def row(self) -> "Keyboard":
74
- if len(self.keyboard) and not len(self.keyboard[-1]):
75
- raise RuntimeError("Last row is empty!")
76
-
77
- self.keyboard.append([])
78
- return self
79
-
80
- def add(self, button: Button) -> "Keyboard":
81
- if not len(self.keyboard):
82
- self.row()
83
-
84
- self.keyboard[-1].append(button.get_data())
85
- return self
91
+ def __init__(
92
+ self,
93
+ *,
94
+ resize_keyboard: bool = True,
95
+ one_time_keyboard: bool = False,
96
+ selective: bool = False,
97
+ is_persistent: bool = False,
98
+ ):
99
+ self.keyboard = [[]]
100
+ self.resize_keyboard = resize_keyboard
101
+ self.one_time_keyboard = one_time_keyboard
102
+ self.selective = selective
103
+ self.is_persistent = is_persistent
86
104
 
87
- def dict(self) -> dict:
88
- return {k: v for k, v in self.__dict__.items() if v is not None}
105
+ def dict(self) -> DictStrAny:
106
+ self.keyboard = [row for row in self.keyboard if row]
107
+ return {
108
+ k: v.unwrap() if v and isinstance(v, Some) else v
109
+ for k, v in self.__dict__.items()
110
+ if type(v) not in (NoneType, Nothing)
111
+ }
89
112
 
90
113
  def get_markup(self) -> ReplyKeyboardMarkup:
91
114
  return ReplyKeyboardMarkup(**self.dict())
92
115
 
93
116
 
94
- class InlineKeyboard(ABCMarkup):
117
+ class InlineKeyboard(ABCMarkup[InlineButton]):
95
118
  BUTTON = InlineButton
96
119
 
97
- def row(self) -> "InlineKeyboard":
98
- if len(self.keyboard) and not len(self.keyboard[-1]):
99
- raise RuntimeError("Last row is empty!")
100
-
101
- self.keyboard.append([])
102
- return self
103
-
104
- def add(self, button: InlineButton) -> "InlineKeyboard":
105
- if not len(self.keyboard):
106
- self.row()
107
-
108
- self.keyboard[-1].append(button.get_data())
109
- return self
120
+ def __init__(self) -> None:
121
+ self.keyboard = [[]]
110
122
 
111
- def dict(self) -> dict:
123
+ def dict(self) -> DictStrAny:
124
+ self.keyboard = [row for row in self.keyboard if row]
112
125
  return dict(inline_keyboard=self.keyboard)
113
126
 
114
127
  def get_markup(self) -> InlineKeyboardMarkup:
115
128
  return InlineKeyboardMarkup(**self.dict())
129
+
130
+
131
+ __all__ = (
132
+ "ABCMarkup",
133
+ "InlineKeyboard",
134
+ "Keyboard",
135
+ "KeyboardModel",
136
+ "keyboard_remove",
137
+ )
@@ -0,0 +1,4 @@
1
+ from .abc import ABCLoopWrapper
2
+ from .loop_wrapper import DelayedTask, LoopWrapper
3
+
4
+ __all__ = ("ABCLoopWrapper", "DelayedTask", "LoopWrapper")
@@ -0,0 +1,18 @@
1
+ import typing
2
+ from abc import ABC, abstractmethod
3
+
4
+ CoroutineTask = typing.Coroutine[typing.Any, typing.Any, typing.Any]
5
+ CoroutineFunc = typing.Callable[..., CoroutineTask]
6
+
7
+
8
+ class ABCLoopWrapper(ABC):
9
+ @abstractmethod
10
+ def add_task(self, task: CoroutineFunc | CoroutineTask) -> None:
11
+ ...
12
+
13
+ @abstractmethod
14
+ def run_event_loop(self) -> None:
15
+ ...
16
+
17
+
18
+ __all__ = ("ABCLoopWrapper",)
@@ -0,0 +1,132 @@
1
+ import asyncio
2
+ import contextlib
3
+ import dataclasses
4
+ import typing
5
+
6
+ from telegrinder.modules import logger
7
+
8
+ from .abc import ABCLoopWrapper, CoroutineFunc, CoroutineTask
9
+
10
+
11
+ @dataclasses.dataclass
12
+ class DelayedTask:
13
+ handler: CoroutineFunc
14
+ seconds: float
15
+ repeat: bool = dataclasses.field(default=False, kw_only=True)
16
+ _cancelled: bool = dataclasses.field(default=False, init=False, repr=False)
17
+
18
+ @property
19
+ def is_cancelled(self) -> bool:
20
+ return self._cancelled
21
+
22
+ async def __call__(self, *args, **kwargs) -> None:
23
+ while not self.is_cancelled:
24
+ await asyncio.sleep(self.seconds)
25
+ if self.is_cancelled:
26
+ break
27
+ await self.handler(*args, **kwargs)
28
+ if not self.repeat:
29
+ break
30
+
31
+ def cancel(self) -> None:
32
+ if not self._cancelled:
33
+ self._cancelled = True
34
+
35
+
36
+ class LoopWrapper(ABCLoopWrapper):
37
+ def __init__(self, tasks: list[CoroutineTask] | None = None):
38
+ self.on_startup: list[CoroutineTask] = []
39
+ self.on_shutdown: list[CoroutineTask] = []
40
+ self.tasks = tasks or []
41
+ self._loop = asyncio.new_event_loop()
42
+
43
+ def run_event_loop(self) -> None:
44
+ if not self.tasks:
45
+ logger.warning("You run loop with 0 tasks!")
46
+
47
+ for startup_task in self.on_startup:
48
+ self._loop.run_until_complete(startup_task)
49
+ for task in self.tasks:
50
+ self._loop.create_task(task)
51
+
52
+ self.tasks.clear()
53
+ tasks = asyncio.all_tasks(self._loop)
54
+ try:
55
+ while tasks:
56
+ tasks_results, _ = self._loop.run_until_complete(
57
+ asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
58
+ )
59
+ for task_result in tasks_results:
60
+ try:
61
+ task_result.result()
62
+ except BaseException as ex:
63
+ logger.exception(ex)
64
+ tasks = asyncio.all_tasks(self._loop)
65
+ except KeyboardInterrupt:
66
+ print() # blank print for ^C
67
+ logger.info("KeyboardInterrupt")
68
+ self.complete_tasks(tasks)
69
+ finally:
70
+ for shutdown_task in self.on_shutdown:
71
+ self._loop.run_until_complete(shutdown_task)
72
+ if self._loop.is_running():
73
+ self._loop.close()
74
+
75
+ def add_task(self, task: CoroutineFunc | CoroutineTask | DelayedTask):
76
+ if asyncio.iscoroutinefunction(task) or isinstance(task, DelayedTask):
77
+ task = task()
78
+ elif not asyncio.iscoroutine(task):
79
+ raise TypeError("Task should be coroutine or coroutine function.")
80
+
81
+ if self._loop and self._loop.is_running():
82
+ self._loop.create_task(task)
83
+ else:
84
+ self.tasks.append(task)
85
+
86
+ def complete_tasks(self, tasks: set[asyncio.Task[typing.Any]]) -> None:
87
+ tasks = tasks | asyncio.all_tasks(self._loop)
88
+ task_to_cancel = asyncio.gather(*tasks, return_exceptions=True)
89
+ task_to_cancel.cancel()
90
+ with contextlib.suppress(asyncio.CancelledError):
91
+ self._loop.run_until_complete(task_to_cancel)
92
+
93
+ def timer(
94
+ self,
95
+ *,
96
+ days: int = 0,
97
+ hours: int = 0,
98
+ minutes: int = 0,
99
+ seconds: float = 0,
100
+ ) -> typing.Callable[[typing.Callable], DelayedTask]:
101
+ seconds += minutes * 60
102
+ seconds += hours * 60 * 60
103
+ seconds += days * 24 * 60 * 60
104
+
105
+ def decorator(func: typing.Callable) -> DelayedTask:
106
+ delayed_task = DelayedTask(func, seconds, repeat=False)
107
+ self.add_task(delayed_task)
108
+ return delayed_task
109
+
110
+ return decorator
111
+
112
+ def interval(
113
+ self,
114
+ *,
115
+ days: int = 0,
116
+ hours: int = 0,
117
+ minutes: int = 0,
118
+ seconds: float = 0,
119
+ ) -> typing.Callable[[typing.Callable], DelayedTask]:
120
+ seconds += minutes * 60
121
+ seconds += hours * 60 * 60
122
+ seconds += days * 24 * 60 * 60
123
+
124
+ def decorator(func: typing.Callable) -> DelayedTask:
125
+ delayed_task = DelayedTask(func, seconds, repeat=True)
126
+ self.add_task(delayed_task)
127
+ return delayed_task
128
+
129
+ return decorator
130
+
131
+
132
+ __all__ = ("DelayedTask", "LoopWrapper")
@@ -1,41 +1,66 @@
1
+ import enum
1
2
  import inspect
2
3
  import types
3
4
  import typing
4
5
 
5
- AnyMock = type("Any", (object,), {"__str__": lambda _: "Any"})
6
+ if typing.TYPE_CHECKING:
7
+ from telegrinder.bot.rules.abc import ABCRule
6
8
 
9
+ T = typing.TypeVar("T", bound=ABCRule)
7
10
 
8
- class VarUnset(BaseException):
9
- def __init__(self, name: str, t: typing.Any, handler: types.FunctionType):
10
- self.name = name
11
- self.t = t
12
- self.handler = handler
11
+ FuncType = types.FunctionType | typing.Callable
12
+ TRANSLATIONS_KEY = "_translations"
13
13
 
14
- def __repr__(self):
15
- return (
16
- f"Handler {self.handler.__name__} requires variable {self.name} of type {self.t} "
17
- "which was not set in the context"
18
- )
19
14
 
20
- def __str__(self):
21
- return self.__repr__()
15
+ def resolve_arg_names(func: FuncType, start_idx: int = 1) -> tuple[str, ...]:
16
+ return func.__code__.co_varnames[start_idx : func.__code__.co_argcount]
22
17
 
23
18
 
24
- def resolve_arg_names(func: types.FunctionType) -> typing.Tuple[str, ...]:
25
- return func.__code__.co_varnames[1 : func.__code__.co_argcount]
26
-
27
-
28
- def get_default_args(func: types.FunctionType) -> typing.Dict[str, typing.Any]:
19
+ def get_default_args(func: FuncType) -> dict[str, typing.Any]:
29
20
  fspec = inspect.getfullargspec(func)
30
21
  return dict(zip(fspec.args[::-1], (fspec.defaults or ())[::-1]))
31
22
 
32
23
 
24
+ def to_str(s: str | enum.Enum) -> str:
25
+ if isinstance(s, enum.Enum):
26
+ return str(s.value)
27
+ return s
28
+
29
+
33
30
  def magic_bundle(
34
- handler: types.FunctionType, kw: typing.Dict[str, typing.Any]
35
- ) -> typing.Dict[str, typing.Any]:
36
- names = resolve_arg_names(handler)
31
+ handler: FuncType,
32
+ kw: dict[str | enum.Enum, typing.Any],
33
+ *,
34
+ start_idx: int = 1,
35
+ bundle_ctx: bool = True,
36
+ ) -> dict[str, typing.Any]:
37
+ names = resolve_arg_names(handler, start_idx=start_idx)
37
38
  args = get_default_args(handler)
38
- args.update({k: v for k, v in kw.items() if k in names})
39
- if "ctx" in names:
39
+ args.update({to_str(k): v for k, v in kw.items() if to_str(k) in names})
40
+ if "ctx" in names and bundle_ctx:
40
41
  args["ctx"] = kw
41
42
  return args
43
+
44
+
45
+ def get_cached_translation(rule: "T", locale: str) -> typing.Optional["T"]:
46
+ translations = getattr(rule, TRANSLATIONS_KEY, {})
47
+ if not translations or locale not in translations:
48
+ return None
49
+ return translations[locale]
50
+
51
+
52
+ def cache_translation(base_rule: "T", locale: str, translated_rule: "T") -> None:
53
+ translations = getattr(base_rule, TRANSLATIONS_KEY, {})
54
+ setattr(base_rule, TRANSLATIONS_KEY, {locale: translated_rule, **translations})
55
+
56
+
57
+ __all__ = (
58
+ "TRANSLATIONS_KEY",
59
+ "cache_translation",
60
+ "get_cached_translation",
61
+ "get_default_args",
62
+ "get_default_args",
63
+ "magic_bundle",
64
+ "resolve_arg_names",
65
+ "to_str",
66
+ )
@@ -3,5 +3,4 @@ class ParseMode:
3
3
  HTML = "HTML"
4
4
 
5
5
 
6
- def get_mention_link(user_id: int) -> str:
7
- return f"tg://user?id={user_id}"
6
+ __all__ = ("ParseMode",)
@@ -1 +1,2 @@
1
+ from telegrinder.types.enums import *
1
2
  from telegrinder.types.objects import *