telegrinder 0.4.2__py3-none-any.whl → 0.5.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 (233) hide show
  1. telegrinder/__init__.py +37 -55
  2. telegrinder/__meta__.py +1 -0
  3. telegrinder/api/__init__.py +6 -4
  4. telegrinder/api/api.py +100 -26
  5. telegrinder/api/error.py +42 -8
  6. telegrinder/api/response.py +4 -1
  7. telegrinder/api/token.py +2 -2
  8. telegrinder/bot/__init__.py +9 -25
  9. telegrinder/bot/bot.py +31 -25
  10. telegrinder/bot/cute_types/__init__.py +0 -0
  11. telegrinder/bot/cute_types/base.py +103 -61
  12. telegrinder/bot/cute_types/callback_query.py +447 -400
  13. telegrinder/bot/cute_types/chat_join_request.py +59 -62
  14. telegrinder/bot/cute_types/chat_member_updated.py +154 -157
  15. telegrinder/bot/cute_types/inline_query.py +41 -44
  16. telegrinder/bot/cute_types/message.py +2621 -2590
  17. telegrinder/bot/cute_types/pre_checkout_query.py +38 -42
  18. telegrinder/bot/cute_types/update.py +1 -8
  19. telegrinder/bot/cute_types/utils.py +1 -1
  20. telegrinder/bot/dispatch/__init__.py +10 -15
  21. telegrinder/bot/dispatch/abc.py +12 -11
  22. telegrinder/bot/dispatch/action.py +104 -0
  23. telegrinder/bot/dispatch/context.py +32 -26
  24. telegrinder/bot/dispatch/dispatch.py +61 -134
  25. telegrinder/bot/dispatch/handler/__init__.py +2 -0
  26. telegrinder/bot/dispatch/handler/abc.py +10 -8
  27. telegrinder/bot/dispatch/handler/audio_reply.py +2 -3
  28. telegrinder/bot/dispatch/handler/base.py +10 -33
  29. telegrinder/bot/dispatch/handler/document_reply.py +2 -3
  30. telegrinder/bot/dispatch/handler/func.py +55 -87
  31. telegrinder/bot/dispatch/handler/media_group_reply.py +2 -3
  32. telegrinder/bot/dispatch/handler/message_reply.py +2 -3
  33. telegrinder/bot/dispatch/handler/photo_reply.py +2 -3
  34. telegrinder/bot/dispatch/handler/sticker_reply.py +2 -3
  35. telegrinder/bot/dispatch/handler/video_reply.py +2 -3
  36. telegrinder/bot/dispatch/middleware/__init__.py +0 -0
  37. telegrinder/bot/dispatch/middleware/abc.py +79 -55
  38. telegrinder/bot/dispatch/middleware/global_middleware.py +18 -33
  39. telegrinder/bot/dispatch/process.py +84 -105
  40. telegrinder/bot/dispatch/return_manager/__init__.py +0 -0
  41. telegrinder/bot/dispatch/return_manager/abc.py +102 -65
  42. telegrinder/bot/dispatch/return_manager/callback_query.py +4 -5
  43. telegrinder/bot/dispatch/return_manager/inline_query.py +3 -4
  44. telegrinder/bot/dispatch/return_manager/message.py +8 -10
  45. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +4 -5
  46. telegrinder/bot/dispatch/view/__init__.py +4 -4
  47. telegrinder/bot/dispatch/view/abc.py +6 -16
  48. telegrinder/bot/dispatch/view/base.py +54 -178
  49. telegrinder/bot/dispatch/view/box.py +19 -18
  50. telegrinder/bot/dispatch/view/callback_query.py +4 -8
  51. telegrinder/bot/dispatch/view/chat_join_request.py +5 -6
  52. telegrinder/bot/dispatch/view/chat_member.py +5 -25
  53. telegrinder/bot/dispatch/view/error.py +9 -0
  54. telegrinder/bot/dispatch/view/inline_query.py +4 -8
  55. telegrinder/bot/dispatch/view/message.py +5 -25
  56. telegrinder/bot/dispatch/view/pre_checkout_query.py +4 -8
  57. telegrinder/bot/dispatch/view/raw.py +3 -109
  58. telegrinder/bot/dispatch/waiter_machine/__init__.py +2 -5
  59. telegrinder/bot/dispatch/waiter_machine/actions.py +6 -4
  60. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +1 -3
  61. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +1 -1
  62. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +11 -7
  63. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
  64. telegrinder/bot/dispatch/waiter_machine/machine.py +43 -60
  65. telegrinder/bot/dispatch/waiter_machine/middleware.py +19 -23
  66. telegrinder/bot/dispatch/waiter_machine/short_state.py +6 -5
  67. telegrinder/bot/polling/__init__.py +0 -0
  68. telegrinder/bot/polling/abc.py +0 -0
  69. telegrinder/bot/polling/polling.py +209 -88
  70. telegrinder/bot/rules/__init__.py +3 -16
  71. telegrinder/bot/rules/abc.py +42 -122
  72. telegrinder/bot/rules/callback_data.py +29 -49
  73. telegrinder/bot/rules/chat_join.py +5 -23
  74. telegrinder/bot/rules/command.py +8 -4
  75. telegrinder/bot/rules/enum_text.py +3 -4
  76. telegrinder/bot/rules/func.py +7 -14
  77. telegrinder/bot/rules/fuzzy.py +3 -4
  78. telegrinder/bot/rules/inline.py +8 -20
  79. telegrinder/bot/rules/integer.py +2 -3
  80. telegrinder/bot/rules/is_from.py +12 -11
  81. telegrinder/bot/rules/logic.py +11 -5
  82. telegrinder/bot/rules/markup.py +22 -14
  83. telegrinder/bot/rules/mention.py +8 -7
  84. telegrinder/bot/rules/message_entities.py +8 -4
  85. telegrinder/bot/rules/node.py +23 -12
  86. telegrinder/bot/rules/payload.py +5 -4
  87. telegrinder/bot/rules/payment_invoice.py +6 -21
  88. telegrinder/bot/rules/regex.py +2 -4
  89. telegrinder/bot/rules/rule_enum.py +8 -7
  90. telegrinder/bot/rules/start.py +5 -6
  91. telegrinder/bot/rules/state.py +1 -1
  92. telegrinder/bot/rules/text.py +4 -15
  93. telegrinder/bot/rules/update.py +3 -4
  94. telegrinder/bot/scenario/__init__.py +0 -0
  95. telegrinder/bot/scenario/abc.py +6 -5
  96. telegrinder/bot/scenario/checkbox.py +1 -1
  97. telegrinder/bot/scenario/choice.py +30 -39
  98. telegrinder/client/__init__.py +3 -5
  99. telegrinder/client/abc.py +11 -6
  100. telegrinder/client/aiohttp.py +141 -27
  101. telegrinder/client/form_data.py +1 -1
  102. telegrinder/model.py +61 -89
  103. telegrinder/modules.py +325 -102
  104. telegrinder/msgspec_utils/__init__.py +40 -0
  105. telegrinder/msgspec_utils/abc.py +18 -0
  106. telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
  107. telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
  108. telegrinder/msgspec_utils/custom_types/enum_meta.py +43 -0
  109. telegrinder/msgspec_utils/custom_types/literal.py +25 -0
  110. telegrinder/msgspec_utils/custom_types/option.py +17 -0
  111. telegrinder/msgspec_utils/decoder.py +389 -0
  112. telegrinder/msgspec_utils/encoder.py +206 -0
  113. telegrinder/{msgspec_json.py → msgspec_utils/json.py} +6 -5
  114. telegrinder/msgspec_utils/tools.py +75 -0
  115. telegrinder/node/__init__.py +24 -7
  116. telegrinder/node/attachment.py +1 -0
  117. telegrinder/node/base.py +154 -72
  118. telegrinder/node/callback_query.py +5 -5
  119. telegrinder/node/collection.py +39 -0
  120. telegrinder/node/command.py +1 -2
  121. telegrinder/node/composer.py +121 -72
  122. telegrinder/node/container.py +11 -8
  123. telegrinder/node/context.py +48 -0
  124. telegrinder/node/either.py +27 -40
  125. telegrinder/node/error.py +41 -0
  126. telegrinder/node/event.py +37 -11
  127. telegrinder/node/exceptions.py +7 -0
  128. telegrinder/node/file.py +0 -0
  129. telegrinder/node/i18n.py +108 -0
  130. telegrinder/node/me.py +3 -2
  131. telegrinder/node/payload.py +1 -1
  132. telegrinder/node/polymorphic.py +63 -28
  133. telegrinder/node/reply_message.py +12 -0
  134. telegrinder/node/rule.py +6 -13
  135. telegrinder/node/scope.py +14 -5
  136. telegrinder/node/session.py +53 -0
  137. telegrinder/node/source.py +41 -9
  138. telegrinder/node/text.py +1 -2
  139. telegrinder/node/tools/__init__.py +0 -0
  140. telegrinder/node/tools/generator.py +3 -5
  141. telegrinder/node/utility.py +16 -0
  142. telegrinder/py.typed +0 -0
  143. telegrinder/rules.py +0 -0
  144. telegrinder/tools/__init__.py +48 -88
  145. telegrinder/tools/aio.py +103 -0
  146. telegrinder/tools/callback_data_serialization/__init__.py +5 -0
  147. telegrinder/tools/{callback_data_serilization → callback_data_serialization}/abc.py +0 -0
  148. telegrinder/tools/{callback_data_serilization → callback_data_serialization}/json_ser.py +2 -3
  149. telegrinder/tools/{callback_data_serilization → callback_data_serialization}/msgpack_ser.py +45 -27
  150. telegrinder/tools/final.py +21 -0
  151. telegrinder/tools/formatting/__init__.py +2 -18
  152. telegrinder/tools/formatting/deep_links/__init__.py +39 -0
  153. telegrinder/tools/formatting/{deep_links.py → deep_links/links.py} +12 -85
  154. telegrinder/tools/formatting/deep_links/parsing.py +90 -0
  155. telegrinder/tools/formatting/deep_links/validators.py +8 -0
  156. telegrinder/tools/formatting/html_formatter.py +18 -45
  157. telegrinder/tools/fullname.py +83 -0
  158. telegrinder/tools/global_context/__init__.py +4 -3
  159. telegrinder/tools/global_context/abc.py +17 -14
  160. telegrinder/tools/global_context/builtin_context.py +39 -0
  161. telegrinder/tools/global_context/global_context.py +138 -39
  162. telegrinder/tools/input_file_directory.py +0 -0
  163. telegrinder/tools/keyboard/__init__.py +39 -0
  164. telegrinder/tools/keyboard/abc.py +159 -0
  165. telegrinder/tools/keyboard/base.py +77 -0
  166. telegrinder/tools/keyboard/buttons/__init__.py +14 -0
  167. telegrinder/tools/keyboard/buttons/base.py +18 -0
  168. telegrinder/tools/{buttons.py → keyboard/buttons/buttons.py} +71 -23
  169. telegrinder/tools/keyboard/buttons/static_buttons.py +56 -0
  170. telegrinder/tools/keyboard/buttons/tools.py +18 -0
  171. telegrinder/tools/keyboard/data.py +20 -0
  172. telegrinder/tools/keyboard/keyboard.py +131 -0
  173. telegrinder/tools/keyboard/static_keyboard.py +83 -0
  174. telegrinder/tools/lifespan.py +87 -51
  175. telegrinder/tools/limited_dict.py +4 -1
  176. telegrinder/tools/loop_wrapper.py +332 -0
  177. telegrinder/tools/magic/__init__.py +32 -0
  178. telegrinder/tools/magic/annotations.py +165 -0
  179. telegrinder/tools/magic/dictionary.py +20 -0
  180. telegrinder/tools/magic/function.py +246 -0
  181. telegrinder/tools/magic/shortcut.py +111 -0
  182. telegrinder/tools/parse_mode.py +9 -3
  183. telegrinder/tools/singleton/__init__.py +4 -0
  184. telegrinder/tools/singleton/abc.py +14 -0
  185. telegrinder/tools/singleton/singleton.py +18 -0
  186. telegrinder/tools/state_storage/__init__.py +0 -0
  187. telegrinder/tools/state_storage/abc.py +6 -1
  188. telegrinder/tools/state_storage/memory.py +1 -1
  189. telegrinder/tools/strings.py +0 -0
  190. telegrinder/types/__init__.py +307 -268
  191. telegrinder/types/enums.py +64 -37
  192. telegrinder/types/input_file.py +3 -3
  193. telegrinder/types/methods.py +5699 -5055
  194. telegrinder/types/methods_utils.py +62 -0
  195. telegrinder/types/objects.py +7846 -7058
  196. telegrinder/verification_utils.py +3 -1
  197. telegrinder-0.5.0.dist-info/METADATA +162 -0
  198. telegrinder-0.5.0.dist-info/RECORD +200 -0
  199. {telegrinder-0.4.2.dist-info → telegrinder-0.5.0.dist-info}/licenses/LICENSE +2 -2
  200. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +0 -20
  201. telegrinder/bot/rules/id.py +0 -24
  202. telegrinder/bot/rules/message.py +0 -15
  203. telegrinder/client/sonic.py +0 -212
  204. telegrinder/msgspec_utils.py +0 -478
  205. telegrinder/tools/adapter/__init__.py +0 -19
  206. telegrinder/tools/adapter/abc.py +0 -49
  207. telegrinder/tools/adapter/dataclass.py +0 -56
  208. telegrinder/tools/adapter/errors.py +0 -5
  209. telegrinder/tools/adapter/event.py +0 -61
  210. telegrinder/tools/adapter/node.py +0 -46
  211. telegrinder/tools/adapter/raw_event.py +0 -27
  212. telegrinder/tools/adapter/raw_update.py +0 -30
  213. telegrinder/tools/callback_data_serilization/__init__.py +0 -5
  214. telegrinder/tools/error_handler/__init__.py +0 -10
  215. telegrinder/tools/error_handler/abc.py +0 -30
  216. telegrinder/tools/error_handler/error.py +0 -9
  217. telegrinder/tools/error_handler/error_handler.py +0 -179
  218. telegrinder/tools/formatting/spec_html_formats.py +0 -75
  219. telegrinder/tools/functional.py +0 -8
  220. telegrinder/tools/global_context/telegrinder_ctx.py +0 -27
  221. telegrinder/tools/i18n/__init__.py +0 -12
  222. telegrinder/tools/i18n/abc.py +0 -32
  223. telegrinder/tools/i18n/middleware/__init__.py +0 -3
  224. telegrinder/tools/i18n/middleware/abc.py +0 -22
  225. telegrinder/tools/i18n/simple.py +0 -43
  226. telegrinder/tools/keyboard.py +0 -132
  227. telegrinder/tools/loop_wrapper/__init__.py +0 -4
  228. telegrinder/tools/loop_wrapper/abc.py +0 -20
  229. telegrinder/tools/loop_wrapper/loop_wrapper.py +0 -169
  230. telegrinder/tools/magic.py +0 -344
  231. telegrinder-0.4.2.dist-info/METADATA +0 -151
  232. telegrinder-0.4.2.dist-info/RECORD +0 -182
  233. {telegrinder-0.4.2.dist-info → telegrinder-0.5.0.dist-info}/WHEEL +0 -0
@@ -1,9 +1,14 @@
1
+ from __future__ import annotations
2
+
1
3
  import dataclasses
4
+ import types
2
5
  import typing
3
6
 
4
7
  import msgspec
5
8
 
6
9
  from telegrinder.msgspec_utils import encoder
10
+ from telegrinder.tools.callback_data_serialization import ABCDataSerializer, JSONSerializer
11
+ from telegrinder.tools.keyboard.buttons.base import BaseButton
7
12
  from telegrinder.types.objects import (
8
13
  CallbackGame,
9
14
  CopyTextButton,
@@ -15,34 +20,70 @@ from telegrinder.types.objects import (
15
20
  WebAppInfo,
16
21
  )
17
22
 
18
- from .callback_data_serilization import ABCDataSerializer, JSONSerializer
19
-
20
23
  if typing.TYPE_CHECKING:
21
24
  from _typeshed import DataclassInstance
22
25
 
26
+ from telegrinder.tools.keyboard.abc import ABCKeyboard
27
+ from telegrinder.tools.keyboard.keyboard import InlineKeyboard, Keyboard
28
+
23
29
  type CallbackData = str | bytes | dict[str, typing.Any] | DataclassInstance | msgspec.Struct
24
30
 
25
31
 
26
- @dataclasses.dataclass
27
- class BaseButton:
28
- def get_data(self) -> dict[str, typing.Any]:
29
- return {k: v for k, v in dataclasses.asdict(self).items() if v is not None}
32
+ def _get_keyboard_class(name: str, /) -> type[ABCKeyboard]:
33
+ from telegrinder.tools.keyboard.keyboard import InlineKeyboard, Keyboard # noqa
34
+
35
+ return locals()[name]
36
+
37
+
38
+ def _magic_keyboard(
39
+ name_keyboard: str,
40
+ button: BaseButton,
41
+ other: typing.Any,
42
+ /,
43
+ *,
44
+ row: bool = False,
45
+ ) -> ABCKeyboard | types.NotImplementedType:
46
+ keyboard_type: type[typing.Any] = _get_keyboard_class(name_keyboard)
47
+ if not isinstance(other, keyboard_type | type(button)):
48
+ return NotImplemented
49
+
50
+ keyboard: ABCKeyboard = keyboard_type().add(button)
51
+ if row:
52
+ keyboard = keyboard.row()
30
53
 
54
+ return keyboard.merge_to_last_row(other) if isinstance(other, keyboard_type) else keyboard.add(other)
31
55
 
32
- class RowButtons[KeyboardButton: BaseButton]:
33
- buttons: list[KeyboardButton]
34
- auto_row: bool
35
56
 
36
- def __init__(self, *buttons: KeyboardButton, auto_row: bool = True) -> None:
37
- self.buttons = list(buttons)
38
- self.auto_row = auto_row
57
+ class ConvertButtonMixin[T: ABCKeyboard]:
58
+ if typing.TYPE_CHECKING:
59
+ as_keyboard: type[T]
60
+ else:
61
+ KEYBOARD_CLASS_KEY = "__keyboard_class__"
39
62
 
40
- def get_data(self) -> list[dict[str, typing.Any]]:
41
- return [b.get_data() for b in self.buttons]
63
+ @classmethod
64
+ def get_keyboard_class(cls):
65
+ if (kb_cls := getattr(cls, cls.KEYBOARD_CLASS_KEY, None)) is not None:
66
+ return kb_cls
67
+
68
+ arg = None
69
+ for base in cls.__orig_bases__:
70
+ origin = typing.get_origin(base) or base
71
+ if issubclass(origin, ConvertButtonMixin):
72
+ arg = typing.get_origin(arg := typing.get_args(base)[0]) or arg
73
+
74
+ assert arg is not None
75
+ kb_cls_name = arg.__forward_arg__ if isinstance(arg, typing.ForwardRef) else arg.__name__
76
+ kb_cls = _get_keyboard_class(kb_cls_name)
77
+ setattr(cls, cls.KEYBOARD_CLASS_KEY, kb_cls)
78
+ return kb_cls
79
+
80
+ def as_keyboard(self, *args, **kwargs):
81
+ keyboard = self.get_keyboard_class()(*args, **kwargs)
82
+ return keyboard.add(self)
42
83
 
43
84
 
44
85
  @dataclasses.dataclass
45
- class Button(BaseButton):
86
+ class Button(BaseButton, ConvertButtonMixin["Keyboard"]):
46
87
  text: str
47
88
  request_contact: bool = dataclasses.field(default=False, kw_only=True)
48
89
  request_location: bool = dataclasses.field(default=False, kw_only=True)
@@ -57,9 +98,15 @@ class Button(BaseButton):
57
98
  )
58
99
  web_app: WebAppInfo | None = dataclasses.field(default=None, kw_only=True)
59
100
 
101
+ def __and__(self, other: object, /) -> Keyboard:
102
+ return _magic_keyboard("Keyboard", self, other)
103
+
104
+ def __or__(self, other: object, /) -> Keyboard:
105
+ return _magic_keyboard("Keyboard", self, other, row=True)
106
+
60
107
 
61
108
  @dataclasses.dataclass
62
- class InlineButton(BaseButton):
109
+ class InlineButton(BaseButton, ConvertButtonMixin["InlineKeyboard"]):
63
110
  text: str
64
111
  url: str | None = dataclasses.field(default=None, kw_only=True)
65
112
  login_url: LoginUrl | None = dataclasses.field(default=None, kw_only=True)
@@ -86,7 +133,7 @@ class InlineButton(BaseButton):
86
133
  or dataclasses.is_dataclass(self.callback_data)
87
134
  ):
88
135
  callback_data_serializer = callback_data_serializer or JSONSerializer(
89
- self.callback_data.__class__,
136
+ type(self.callback_data),
90
137
  )
91
138
 
92
139
  if callback_data_serializer is not None:
@@ -100,10 +147,11 @@ class InlineButton(BaseButton):
100
147
  if isinstance(self.web_app, str):
101
148
  self.web_app = WebAppInfo(url=self.web_app)
102
149
 
150
+ def __and__(self, other: object, /) -> InlineKeyboard:
151
+ return _magic_keyboard("InlineKeyboard", self, other)
103
152
 
104
- __all__ = (
105
- "BaseButton",
106
- "Button",
107
- "InlineButton",
108
- "RowButtons",
109
- )
153
+ def __or__(self, other: object, /) -> InlineKeyboard:
154
+ return _magic_keyboard("InlineKeyboard", self, other, row=True)
155
+
156
+
157
+ __all__ = ("Button", "InlineButton")
@@ -0,0 +1,56 @@
1
+ import dataclasses
2
+ import types
3
+ import typing
4
+
5
+ from telegrinder.bot.rules.abc import ABCRule
6
+ from telegrinder.bot.rules.payload import PayloadJsonEqRule, PayloadMarkupRule, PayloadModelRule
7
+ from telegrinder.bot.rules.text import Text
8
+ from telegrinder.tools.callback_data_serialization import ABCDataSerializer
9
+ from telegrinder.tools.keyboard.buttons.base import BaseStaticButton
10
+ from telegrinder.tools.keyboard.buttons.buttons import Button, InlineButton
11
+
12
+
13
+ @dataclasses.dataclass
14
+ class StaticButtonMixin(BaseStaticButton):
15
+ __and__: typing.ClassVar[None] = None
16
+ __or__: typing.ClassVar[None] = None
17
+
18
+
19
+ @dataclasses.dataclass
20
+ class StaticButton(StaticButtonMixin, Button, Text):
21
+ if not typing.TYPE_CHECKING:
22
+
23
+ def __init__(self, text, **kwargs):
24
+ self.row = kwargs.pop("row", False)
25
+ Button.__init__(self, text=text, **kwargs)
26
+ Text.__init__(self, text)
27
+
28
+
29
+ @dataclasses.dataclass
30
+ class StaticInlineButton(StaticButtonMixin, InlineButton, ABCRule):
31
+ if not typing.TYPE_CHECKING:
32
+
33
+ def __init__(self, *args, **kwargs):
34
+ self.row = kwargs.pop("row", False)
35
+ InlineButton.__init__(self, *args, **kwargs)
36
+
37
+ check = lambda self, *args, **kwargs: False
38
+ else:
39
+
40
+ def check(self, *args: typing.Any, **kwargs: typing.Any) -> bool: ...
41
+
42
+ def __post_init__(self, callback_data_serializer: ABCDataSerializer[typing.Any] | None) -> None:
43
+ if isinstance(self.callback_data, str):
44
+ self.check = PayloadMarkupRule(self.callback_data).check
45
+ elif isinstance(self.callback_data, dict):
46
+ self.check = PayloadJsonEqRule(self.callback_data).check
47
+ elif not isinstance(self.callback_data, (bytes, types.NoneType)):
48
+ self.check = PayloadModelRule(
49
+ type(self.callback_data),
50
+ serializer=type(callback_data_serializer) if callback_data_serializer else None,
51
+ ).check
52
+
53
+ super().__post_init__(callback_data_serializer)
54
+
55
+
56
+ __all__ = ("StaticButton", "StaticInlineButton")
@@ -0,0 +1,18 @@
1
+ import typing
2
+
3
+ if typing.TYPE_CHECKING:
4
+ from telegrinder.tools.keyboard.buttons.base import BaseButton
5
+
6
+
7
+ class RowButtons[KeyboardButton: BaseButton]:
8
+ buttons: typing.Iterable[KeyboardButton]
9
+
10
+ def __init__(self, *buttons: KeyboardButton, auto_row: bool = True) -> None:
11
+ self.buttons = buttons
12
+ self.auto_row = auto_row
13
+
14
+ def get_data(self) -> list[dict[str, typing.Any]]:
15
+ return [b.get_data() for b in self.buttons]
16
+
17
+
18
+ __all__ = ("RowButtons",)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import typing
5
+
6
+ if typing.TYPE_CHECKING:
7
+ from telegrinder.tools.keyboard.abc import DictStrAny
8
+
9
+
10
+ @dataclasses.dataclass(kw_only=True, frozen=True)
11
+ class KeyboardModel:
12
+ keyboard: typing.Iterable[typing.Iterable[DictStrAny]] = dataclasses.field(default_factory=lambda: [[]])
13
+ resize_keyboard: bool = dataclasses.field(default=True)
14
+ one_time_keyboard: bool = dataclasses.field(default=False)
15
+ selective: bool = dataclasses.field(default=False)
16
+ is_persistent: bool = dataclasses.field(default=False)
17
+ input_field_placeholder: str | None = dataclasses.field(default=None)
18
+
19
+
20
+ __all__ = ("KeyboardModel",)
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import typing
5
+ from copy import deepcopy
6
+
7
+ from telegrinder.tools.keyboard.base import BaseKeyboard, DictStrAny
8
+ from telegrinder.tools.keyboard.data import KeyboardModel
9
+ from telegrinder.types.objects import (
10
+ InlineKeyboardMarkup,
11
+ ReplyKeyboardMarkup,
12
+ ReplyKeyboardRemove,
13
+ )
14
+
15
+ if typing.TYPE_CHECKING:
16
+ from telegrinder.tools.keyboard.abc import RawKeyboard
17
+
18
+
19
+ @dataclasses.dataclass(kw_only=True, frozen=True)
20
+ class KeyboardModelMixin(KeyboardModel if typing.TYPE_CHECKING else object):
21
+ keyboard_model: KeyboardModel = dataclasses.field(init=False)
22
+
23
+ if not typing.TYPE_CHECKING:
24
+ keyboard_model = dataclasses.field(
25
+ default_factory=lambda: KeyboardModel(keyboard=[[]]),
26
+ repr=False,
27
+ )
28
+
29
+ @property
30
+ def keyboard(self) -> RawKeyboard:
31
+ return self.keyboard_model.keyboard # type: ignore
32
+
33
+ @property
34
+ def is_persistent(self) -> bool:
35
+ return self.keyboard_model.is_persistent
36
+
37
+ @property
38
+ def one_time_keyboard(self) -> bool:
39
+ return self.keyboard_model.one_time_keyboard
40
+
41
+ @property
42
+ def resize_keyboard(self) -> bool:
43
+ return self.keyboard_model.resize_keyboard
44
+
45
+ @property
46
+ def is_selective(self) -> bool:
47
+ return self.keyboard_model.selective
48
+
49
+ @property
50
+ def input_field_placeholder(self) -> str | None:
51
+ return self.keyboard_model.input_field_placeholder
52
+
53
+
54
+ @dataclasses.dataclass(frozen=True, kw_only=True)
55
+ class Keyboard(KeyboardModelMixin, BaseKeyboard):
56
+ if not typing.TYPE_CHECKING:
57
+
58
+ def __init__(self, **kwargs):
59
+ super().__init__(keyboard_model=kwargs.pop("keyboard_model", None) or KeyboardModel(**kwargs))
60
+
61
+ def dict(self) -> DictStrAny:
62
+ return dataclasses.asdict(self.keyboard_model) | {"keyboard": [row for row in self.keyboard if row]}
63
+
64
+ def get_markup(self) -> ReplyKeyboardMarkup:
65
+ return ReplyKeyboardMarkup.from_dict(self.dict())
66
+
67
+ def copy(self, **with_changes: typing.Any) -> typing.Self:
68
+ return dataclasses.replace(
69
+ self,
70
+ keyboard_model=dataclasses.replace(
71
+ self.keyboard_model,
72
+ keyboard=deepcopy(self.keyboard_model.keyboard),
73
+ **with_changes,
74
+ ),
75
+ )
76
+
77
+ def get_keyboard_remove(self) -> ReplyKeyboardRemove:
78
+ return ReplyKeyboardRemove.from_data(
79
+ remove_keyboard=True,
80
+ selective=self.keyboard_model.selective,
81
+ )
82
+
83
+ def resize(self) -> typing.Self:
84
+ return self.copy(resize_keyboard=True)
85
+
86
+ def one_time(self) -> typing.Self:
87
+ return self.copy(one_time_keyboard=True)
88
+
89
+ def selective(self) -> typing.Self:
90
+ return self.copy(selective=True)
91
+
92
+ def persistent(self) -> typing.Self:
93
+ return self.copy(is_persistent=True)
94
+
95
+ def no_resize(self) -> typing.Self:
96
+ return self.copy(resize_keyboard=False)
97
+
98
+ def no_one_time(self) -> typing.Self:
99
+ return self.copy(one_time_keyboard=False)
100
+
101
+ def no_selective(self) -> typing.Self:
102
+ return self.copy(selective=False)
103
+
104
+ def no_persistent(self) -> typing.Self:
105
+ return self.copy(is_persistent=False)
106
+
107
+ def placeholder(self, value: str | None, /) -> typing.Self:
108
+ return self.copy(input_field_placeholder=value)
109
+
110
+
111
+ @dataclasses.dataclass(frozen=True)
112
+ class InlineKeyboard(BaseKeyboard):
113
+ keyboard: RawKeyboard = dataclasses.field(init=False)
114
+
115
+ if not typing.TYPE_CHECKING:
116
+ keyboard = dataclasses.field(
117
+ default_factory=lambda: [[]],
118
+ repr=False,
119
+ )
120
+
121
+ def dict(self) -> DictStrAny:
122
+ return dict(inline_keyboard=[row for row in self.keyboard if row])
123
+
124
+ def get_markup(self) -> InlineKeyboardMarkup:
125
+ return InlineKeyboardMarkup.from_dict(self.dict())
126
+
127
+ def copy(self, **_: typing.Any) -> typing.Self:
128
+ return dataclasses.replace(self, keyboard=deepcopy(self.keyboard))
129
+
130
+
131
+ __all__ = ("InlineKeyboard", "Keyboard")
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import typing
5
+
6
+ from telegrinder.tools.keyboard.abc import DictStrAny
7
+ from telegrinder.tools.keyboard.base import BaseStaticKeyboard
8
+ from telegrinder.tools.keyboard.data import KeyboardModel
9
+ from telegrinder.types.objects import InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove
10
+
11
+ type RawKeyboard = typing.Iterable[typing.Iterable[DictStrAny]]
12
+
13
+
14
+ def create_keyboard(cls_: type[BaseStaticKeyboard], /) -> RawKeyboard:
15
+ buttons = cls_.get_buttons()
16
+ max_in_row = cls_.__max_in_row__
17
+ keyboard = [[]]
18
+
19
+ for button in buttons.values():
20
+ keyboard[-1].append(button.get_data())
21
+
22
+ if (button.row is True or len(keyboard[-1]) >= max_in_row) and keyboard[-1]:
23
+ keyboard.append([])
24
+
25
+ return tuple(tuple(x) for x in keyboard if x)
26
+
27
+
28
+ class StaticKeyboard(BaseStaticKeyboard):
29
+ __keyboard__: KeyboardModel
30
+
31
+ def __init_subclass__(
32
+ cls,
33
+ *,
34
+ resize_keyboard: bool = True,
35
+ one_time_keyboard: bool = False,
36
+ selective: bool = False,
37
+ is_persistent: bool = False,
38
+ input_field_placeholder: str | None = None,
39
+ max_in_row: int = 3,
40
+ ) -> None:
41
+ cls.__max_in_row__ = max_in_row
42
+ cls.__keyboard__ = KeyboardModel(
43
+ keyboard=create_keyboard(cls),
44
+ resize_keyboard=resize_keyboard,
45
+ one_time_keyboard=one_time_keyboard,
46
+ selective=selective,
47
+ is_persistent=is_persistent,
48
+ input_field_placeholder=input_field_placeholder,
49
+ )
50
+
51
+ @classmethod
52
+ def dict(cls) -> DictStrAny:
53
+ return dataclasses.asdict(cls.__keyboard__)
54
+
55
+ @classmethod
56
+ def get_markup(cls) -> ReplyKeyboardMarkup:
57
+ return ReplyKeyboardMarkup.from_dict(cls.dict())
58
+
59
+ @classmethod
60
+ def get_keyboard_remove(cls) -> ReplyKeyboardRemove:
61
+ return ReplyKeyboardRemove.from_data(
62
+ remove_keyboard=True,
63
+ selective=cls.__keyboard__.selective,
64
+ )
65
+
66
+
67
+ class StaticInlineKeyboard(BaseStaticKeyboard):
68
+ __keyboard__: RawKeyboard
69
+
70
+ def __init_subclass__(cls, *, max_in_row: int = 3) -> None:
71
+ cls.__max_in_row__ = max_in_row
72
+ cls.__keyboard__ = create_keyboard(cls)
73
+
74
+ @classmethod
75
+ def dict(cls) -> DictStrAny:
76
+ return dict(inline_keyboard=cls.__keyboard__)
77
+
78
+ @classmethod
79
+ def get_markup(cls) -> InlineKeyboardMarkup:
80
+ return InlineKeyboardMarkup.from_dict(cls.dict())
81
+
82
+
83
+ __all__ = ("StaticInlineKeyboard", "StaticKeyboard")
@@ -3,20 +3,15 @@ import dataclasses
3
3
  import datetime
4
4
  import typing
5
5
 
6
+ from telegrinder.modules import logger
7
+ from telegrinder.tools.aio import run_task
8
+ from telegrinder.tools.fullname import fullname
9
+
6
10
  type CoroutineTask[T] = typing.Coroutine[typing.Any, typing.Any, T]
7
11
  type CoroutineFunc[**P, T] = typing.Callable[P, CoroutineTask[T]]
8
12
  type Task[**P, T] = CoroutineFunc[P, T] | CoroutineTask[T] | DelayedTask[typing.Callable[P, CoroutineTask[T]]]
9
13
 
10
14
 
11
- def run_tasks(
12
- tasks: list[CoroutineTask[typing.Any]],
13
- /,
14
- ) -> None:
15
- loop = asyncio.get_event_loop()
16
- while tasks:
17
- loop.run_until_complete(tasks.pop(0))
18
-
19
-
20
15
  def to_coroutine_task[**P, T](task: Task[P, T], /) -> CoroutineTask[T]:
21
16
  if asyncio.iscoroutinefunction(task) or isinstance(task, DelayedTask):
22
17
  task = task()
@@ -25,12 +20,17 @@ def to_coroutine_task[**P, T](task: Task[P, T], /) -> CoroutineTask[T]:
25
20
  return task
26
21
 
27
22
 
28
- @dataclasses.dataclass(slots=True)
29
- class DelayedTask[Handler: CoroutineFunc[..., typing.Any]]:
30
- handler: Handler
23
+ @dataclasses.dataclass
24
+ class DelayedTask[Function: CoroutineFunc[..., typing.Any]]:
25
+ _cancelled: bool = dataclasses.field(default=False, init=False, repr=False)
26
+ _task: asyncio.Task[typing.Any] | None = dataclasses.field(default=None, init=False, repr=False)
27
+
28
+ function: Function
31
29
  seconds: float | datetime.timedelta
32
30
  repeat: bool = dataclasses.field(default=False, kw_only=True)
33
- _cancelled: bool = dataclasses.field(default=False, init=False, repr=False)
31
+
32
+ def __post_init__(self) -> None:
33
+ self.function.cancel = self.cancel
34
34
 
35
35
  @property
36
36
  def is_cancelled(self) -> bool:
@@ -38,68 +38,104 @@ class DelayedTask[Handler: CoroutineFunc[..., typing.Any]]:
38
38
 
39
39
  @property
40
40
  def delay(self) -> float:
41
- return self.seconds if isinstance(self.seconds, int | float) else self.seconds.total_seconds()
41
+ return float(self.seconds) if isinstance(self.seconds, int | float) else self.seconds.total_seconds()
42
42
 
43
43
  async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
44
- while not self.is_cancelled:
44
+ self._task = self._task or asyncio.current_task()
45
+ stopped = False
46
+
47
+ while not self._cancelled and not stopped:
45
48
  await asyncio.sleep(self.delay)
46
- if self.is_cancelled:
47
- break
48
49
  try:
49
- await self.handler(*args, **kwargs)
50
+ await self.function(*args, **kwargs)
51
+ except BaseException:
52
+ logger.exception(
53
+ "Delayed task for function `{}` caught an exception, traceback message below:",
54
+ fullname(self.function),
55
+ )
50
56
  finally:
51
- if not self.repeat:
52
- break
57
+ stopped = not self.repeat
53
58
 
54
- def cancel(self) -> None:
59
+ def cancel(self) -> bool:
55
60
  if not self._cancelled:
56
- self._cancelled = True
61
+ self._cancelled = True if self._task is None else self._task.cancel()
62
+ self._task = None
63
+
64
+ return self._cancelled
57
65
 
58
66
 
59
- @dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
67
+ @dataclasses.dataclass(kw_only=True, slots=True, repr=False)
60
68
  class Lifespan:
69
+ _started: bool = dataclasses.field(default=False, init=False)
61
70
  startup_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
62
71
  shutdown_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
63
72
 
64
- def on_startup[**P, T](self, task: Task[P, T], /) -> Task[P, T]:
65
- self.startup_tasks.append(to_coroutine_task(task))
66
- return task
73
+ def __repr__(self) -> str:
74
+ return "<{}: started={}>".format(fullname(self), self._started)
67
75
 
68
- def on_shutdown[**P, T](self, task: Task[P, T], /) -> Task[P, T]:
69
- self.shutdown_tasks.append(to_coroutine_task(task))
70
- return task
76
+ def __add__(self, other: object, /) -> typing.Self:
77
+ if not isinstance(other, self.__class__):
78
+ return NotImplemented
71
79
 
72
- def start(self) -> None:
73
- run_tasks(self.startup_tasks)
80
+ return self.__class__(
81
+ startup_tasks=self.startup_tasks + other.startup_tasks,
82
+ shutdown_tasks=self.shutdown_tasks + other.shutdown_tasks,
83
+ )
74
84
 
75
- def shutdown(self) -> None:
76
- run_tasks(self.shutdown_tasks)
85
+ def __iadd__(self, other: object, /) -> typing.Self:
86
+ if not isinstance(other, self.__class__):
87
+ return NotImplemented
88
+
89
+ self.startup_tasks.extend(other.startup_tasks)
90
+ self.shutdown_tasks.extend(other.shutdown_tasks)
91
+ return self
77
92
 
78
93
  def __enter__(self) -> None:
79
94
  self.start()
80
95
 
81
- def __exit__(self) -> None:
96
+ def __exit__(self, *args: typing.Any) -> None:
82
97
  self.shutdown()
83
98
 
84
99
  async def __aenter__(self) -> None:
85
- for task in self.startup_tasks:
86
- await task
100
+ await self._start()
87
101
 
88
- async def __aexit__(self, *args) -> None:
89
- for task in self.shutdown_tasks:
90
- await task
102
+ async def __aexit__(self, *args: typing.Any) -> None:
103
+ await self._shutdown()
91
104
 
92
- def __add__(self, other: typing.Self, /) -> typing.Self:
93
- return self.__class__(
94
- startup_tasks=self.startup_tasks + other.startup_tasks,
95
- shutdown_tasks=self.shutdown_tasks + other.shutdown_tasks,
96
- )
105
+ @property
106
+ def started(self) -> bool:
107
+ return self._started
108
+
109
+ @staticmethod
110
+ async def _run_tasks(tasks: list[CoroutineTask[typing.Any]], /) -> None:
111
+ while tasks:
112
+ await tasks.pop(0)
113
+
114
+ async def _start(self) -> None:
115
+ if not self._started:
116
+ logger.debug("Running lifespan startup tasks")
117
+ self._started = True
118
+ await self._run_tasks(self.startup_tasks)
119
+
120
+ async def _shutdown(self) -> None:
121
+ if self._started:
122
+ logger.debug("Running lifespan shutdown tasks")
123
+ self._started = False
124
+ await self._run_tasks(self.shutdown_tasks)
125
+
126
+ def start(self) -> None:
127
+ run_task(self._start())
128
+
129
+ def shutdown(self) -> None:
130
+ run_task(self._shutdown())
131
+
132
+ def on_startup[**P, T](self, task: Task[P, T], /) -> Task[P, T]:
133
+ self.startup_tasks.append(to_coroutine_task(task))
134
+ return task
135
+
136
+ def on_shutdown[**P, T](self, task: Task[P, T], /) -> Task[P, T]:
137
+ self.shutdown_tasks.append(to_coroutine_task(task))
138
+ return task
97
139
 
98
140
 
99
- __all__ = (
100
- "CoroutineTask",
101
- "DelayedTask",
102
- "Lifespan",
103
- "run_tasks",
104
- "to_coroutine_task",
105
- )
141
+ __all__ = ("CoroutineTask", "DelayedTask", "Lifespan", "run_task", "to_coroutine_task")
@@ -13,17 +13,20 @@ class LimitedDict[Key, Value](UserDict[Key, Value]):
13
13
  was reached, otherwise None.
14
14
  """
15
15
  deleted_item = None
16
+
16
17
  if len(self.queue) >= self.maxlimit:
17
18
  deleted_item = self.pop(self.queue.popleft(), None)
19
+
18
20
  if key not in self.queue:
19
21
  self.queue.append(key)
22
+
20
23
  super().__setitem__(key, value)
21
24
  return deleted_item
22
25
 
23
26
  def __setitem__(self, key: Key, value: Value, /) -> None:
24
27
  self.set(key, value)
25
28
 
26
- def __delitem__(self, key: Key) -> None:
29
+ def __delitem__(self, key: Key, /) -> None:
27
30
  if key in self.queue:
28
31
  self.queue.remove(key)
29
32
  return super().__delitem__(key)