telegrinder 1.0.0rc1__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.
Files changed (215) hide show
  1. telegrinder/__init__.py +258 -0
  2. telegrinder/__meta__.py +1 -0
  3. telegrinder/api/__init__.py +15 -0
  4. telegrinder/api/api.py +175 -0
  5. telegrinder/api/error.py +50 -0
  6. telegrinder/api/response.py +23 -0
  7. telegrinder/api/token.py +30 -0
  8. telegrinder/api/validators.py +30 -0
  9. telegrinder/bot/__init__.py +144 -0
  10. telegrinder/bot/bot.py +70 -0
  11. telegrinder/bot/cute_types/__init__.py +41 -0
  12. telegrinder/bot/cute_types/base.py +228 -0
  13. telegrinder/bot/cute_types/base.pyi +49 -0
  14. telegrinder/bot/cute_types/business_connection.py +9 -0
  15. telegrinder/bot/cute_types/business_messages_deleted.py +9 -0
  16. telegrinder/bot/cute_types/callback_query.py +248 -0
  17. telegrinder/bot/cute_types/chat_boost_removed.py +9 -0
  18. telegrinder/bot/cute_types/chat_boost_updated.py +9 -0
  19. telegrinder/bot/cute_types/chat_join_request.py +59 -0
  20. telegrinder/bot/cute_types/chat_member_updated.py +158 -0
  21. telegrinder/bot/cute_types/chosen_inline_result.py +11 -0
  22. telegrinder/bot/cute_types/inline_query.py +41 -0
  23. telegrinder/bot/cute_types/message.py +2809 -0
  24. telegrinder/bot/cute_types/message_reaction_count_updated.py +9 -0
  25. telegrinder/bot/cute_types/message_reaction_updated.py +9 -0
  26. telegrinder/bot/cute_types/paid_media_purchased.py +11 -0
  27. telegrinder/bot/cute_types/poll.py +9 -0
  28. telegrinder/bot/cute_types/poll_answer.py +9 -0
  29. telegrinder/bot/cute_types/pre_checkout_query.py +36 -0
  30. telegrinder/bot/cute_types/shipping_query.py +11 -0
  31. telegrinder/bot/cute_types/update.py +209 -0
  32. telegrinder/bot/cute_types/utils.py +141 -0
  33. telegrinder/bot/dispatch/__init__.py +99 -0
  34. telegrinder/bot/dispatch/abc.py +74 -0
  35. telegrinder/bot/dispatch/action.py +99 -0
  36. telegrinder/bot/dispatch/context.py +162 -0
  37. telegrinder/bot/dispatch/dispatch.py +362 -0
  38. telegrinder/bot/dispatch/handler/__init__.py +23 -0
  39. telegrinder/bot/dispatch/handler/abc.py +25 -0
  40. telegrinder/bot/dispatch/handler/audio_reply.py +43 -0
  41. telegrinder/bot/dispatch/handler/base.py +34 -0
  42. telegrinder/bot/dispatch/handler/document_reply.py +43 -0
  43. telegrinder/bot/dispatch/handler/func.py +73 -0
  44. telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
  45. telegrinder/bot/dispatch/handler/message_reply.py +35 -0
  46. telegrinder/bot/dispatch/handler/photo_reply.py +43 -0
  47. telegrinder/bot/dispatch/handler/sticker_reply.py +36 -0
  48. telegrinder/bot/dispatch/handler/video_reply.py +43 -0
  49. telegrinder/bot/dispatch/middleware/__init__.py +13 -0
  50. telegrinder/bot/dispatch/middleware/abc.py +112 -0
  51. telegrinder/bot/dispatch/middleware/box.py +32 -0
  52. telegrinder/bot/dispatch/middleware/filter.py +88 -0
  53. telegrinder/bot/dispatch/middleware/media_group.py +69 -0
  54. telegrinder/bot/dispatch/process.py +93 -0
  55. telegrinder/bot/dispatch/return_manager/__init__.py +21 -0
  56. telegrinder/bot/dispatch/return_manager/abc.py +107 -0
  57. telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
  58. telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
  59. telegrinder/bot/dispatch/return_manager/message.py +34 -0
  60. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +19 -0
  61. telegrinder/bot/dispatch/return_manager/utils.py +20 -0
  62. telegrinder/bot/dispatch/router/__init__.py +4 -0
  63. telegrinder/bot/dispatch/router/abc.py +15 -0
  64. telegrinder/bot/dispatch/router/base.py +154 -0
  65. telegrinder/bot/dispatch/view/__init__.py +15 -0
  66. telegrinder/bot/dispatch/view/abc.py +15 -0
  67. telegrinder/bot/dispatch/view/base.py +226 -0
  68. telegrinder/bot/dispatch/view/box.py +207 -0
  69. telegrinder/bot/dispatch/view/media_group.py +25 -0
  70. telegrinder/bot/dispatch/waiter_machine/__init__.py +25 -0
  71. telegrinder/bot/dispatch/waiter_machine/actions.py +16 -0
  72. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +13 -0
  73. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +53 -0
  74. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +61 -0
  75. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +49 -0
  76. telegrinder/bot/dispatch/waiter_machine/machine.py +264 -0
  77. telegrinder/bot/dispatch/waiter_machine/middleware.py +77 -0
  78. telegrinder/bot/dispatch/waiter_machine/short_state.py +105 -0
  79. telegrinder/bot/polling/__init__.py +4 -0
  80. telegrinder/bot/polling/abc.py +25 -0
  81. telegrinder/bot/polling/error_handler.py +93 -0
  82. telegrinder/bot/polling/polling.py +167 -0
  83. telegrinder/bot/polling/utils.py +12 -0
  84. telegrinder/bot/rules/__init__.py +166 -0
  85. telegrinder/bot/rules/abc.py +150 -0
  86. telegrinder/bot/rules/button.py +20 -0
  87. telegrinder/bot/rules/callback_data.py +109 -0
  88. telegrinder/bot/rules/chat_join.py +28 -0
  89. telegrinder/bot/rules/chat_member_updated.py +145 -0
  90. telegrinder/bot/rules/command.py +137 -0
  91. telegrinder/bot/rules/enum_text.py +29 -0
  92. telegrinder/bot/rules/func.py +21 -0
  93. telegrinder/bot/rules/fuzzy.py +21 -0
  94. telegrinder/bot/rules/inline.py +45 -0
  95. telegrinder/bot/rules/integer.py +19 -0
  96. telegrinder/bot/rules/is_from.py +213 -0
  97. telegrinder/bot/rules/logic.py +22 -0
  98. telegrinder/bot/rules/magic.py +60 -0
  99. telegrinder/bot/rules/markup.py +51 -0
  100. telegrinder/bot/rules/media.py +13 -0
  101. telegrinder/bot/rules/mention.py +15 -0
  102. telegrinder/bot/rules/message_entities.py +37 -0
  103. telegrinder/bot/rules/node.py +43 -0
  104. telegrinder/bot/rules/payload.py +89 -0
  105. telegrinder/bot/rules/payment_invoice.py +14 -0
  106. telegrinder/bot/rules/regex.py +34 -0
  107. telegrinder/bot/rules/rule_enum.py +71 -0
  108. telegrinder/bot/rules/start.py +73 -0
  109. telegrinder/bot/rules/state.py +35 -0
  110. telegrinder/bot/rules/text.py +27 -0
  111. telegrinder/bot/rules/update.py +14 -0
  112. telegrinder/bot/scenario/__init__.py +5 -0
  113. telegrinder/bot/scenario/abc.py +16 -0
  114. telegrinder/bot/scenario/checkbox.py +183 -0
  115. telegrinder/bot/scenario/choice.py +44 -0
  116. telegrinder/client/__init__.py +11 -0
  117. telegrinder/client/abc.py +136 -0
  118. telegrinder/client/form_data.py +34 -0
  119. telegrinder/client/rnet.py +198 -0
  120. telegrinder/model.py +133 -0
  121. telegrinder/model.pyi +57 -0
  122. telegrinder/modules.py +1081 -0
  123. telegrinder/msgspec_utils/__init__.py +42 -0
  124. telegrinder/msgspec_utils/abc.py +16 -0
  125. telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
  126. telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
  127. telegrinder/msgspec_utils/custom_types/enum_meta.py +61 -0
  128. telegrinder/msgspec_utils/custom_types/literal.py +25 -0
  129. telegrinder/msgspec_utils/custom_types/option.py +17 -0
  130. telegrinder/msgspec_utils/decoder.py +388 -0
  131. telegrinder/msgspec_utils/encoder.py +204 -0
  132. telegrinder/msgspec_utils/json.py +15 -0
  133. telegrinder/msgspec_utils/tools.py +80 -0
  134. telegrinder/node/__init__.py +80 -0
  135. telegrinder/node/compose.py +193 -0
  136. telegrinder/node/nodes/__init__.py +96 -0
  137. telegrinder/node/nodes/attachment.py +169 -0
  138. telegrinder/node/nodes/callback_query.py +25 -0
  139. telegrinder/node/nodes/channel.py +97 -0
  140. telegrinder/node/nodes/command.py +33 -0
  141. telegrinder/node/nodes/error.py +43 -0
  142. telegrinder/node/nodes/event.py +70 -0
  143. telegrinder/node/nodes/file.py +39 -0
  144. telegrinder/node/nodes/global_node.py +66 -0
  145. telegrinder/node/nodes/i18n.py +110 -0
  146. telegrinder/node/nodes/me.py +26 -0
  147. telegrinder/node/nodes/message_entities.py +15 -0
  148. telegrinder/node/nodes/payload.py +84 -0
  149. telegrinder/node/nodes/reply_message.py +14 -0
  150. telegrinder/node/nodes/source.py +172 -0
  151. telegrinder/node/nodes/state_mutator.py +71 -0
  152. telegrinder/node/nodes/text.py +62 -0
  153. telegrinder/node/scope.py +88 -0
  154. telegrinder/node/utils.py +38 -0
  155. telegrinder/py.typed +0 -0
  156. telegrinder/rules.py +1 -0
  157. telegrinder/tools/__init__.py +183 -0
  158. telegrinder/tools/aio.py +147 -0
  159. telegrinder/tools/final.py +21 -0
  160. telegrinder/tools/formatting/__init__.py +85 -0
  161. telegrinder/tools/formatting/deep_links/__init__.py +39 -0
  162. telegrinder/tools/formatting/deep_links/links.py +468 -0
  163. telegrinder/tools/formatting/deep_links/parsing.py +88 -0
  164. telegrinder/tools/formatting/deep_links/validators.py +8 -0
  165. telegrinder/tools/formatting/html.py +241 -0
  166. telegrinder/tools/fullname.py +82 -0
  167. telegrinder/tools/global_context/__init__.py +13 -0
  168. telegrinder/tools/global_context/abc.py +63 -0
  169. telegrinder/tools/global_context/builtin_context.py +45 -0
  170. telegrinder/tools/global_context/global_context.py +614 -0
  171. telegrinder/tools/input_file_directory.py +30 -0
  172. telegrinder/tools/keyboard/__init__.py +6 -0
  173. telegrinder/tools/keyboard/abc.py +84 -0
  174. telegrinder/tools/keyboard/base.py +108 -0
  175. telegrinder/tools/keyboard/button.py +181 -0
  176. telegrinder/tools/keyboard/data.py +31 -0
  177. telegrinder/tools/keyboard/keyboard.py +160 -0
  178. telegrinder/tools/keyboard/utils.py +95 -0
  179. telegrinder/tools/lifespan.py +188 -0
  180. telegrinder/tools/limited_dict.py +35 -0
  181. telegrinder/tools/loop_wrapper.py +271 -0
  182. telegrinder/tools/magic/__init__.py +29 -0
  183. telegrinder/tools/magic/annotations.py +172 -0
  184. telegrinder/tools/magic/descriptors.py +57 -0
  185. telegrinder/tools/magic/function.py +254 -0
  186. telegrinder/tools/magic/inspect.py +16 -0
  187. telegrinder/tools/magic/shortcut.py +107 -0
  188. telegrinder/tools/member_descriptor_proxy.py +95 -0
  189. telegrinder/tools/parse_mode.py +12 -0
  190. telegrinder/tools/serialization/__init__.py +5 -0
  191. telegrinder/tools/serialization/abc.py +34 -0
  192. telegrinder/tools/serialization/json_ser.py +60 -0
  193. telegrinder/tools/serialization/msgpack_ser.py +197 -0
  194. telegrinder/tools/serialization/utils.py +18 -0
  195. telegrinder/tools/singleton/__init__.py +4 -0
  196. telegrinder/tools/singleton/abc.py +14 -0
  197. telegrinder/tools/singleton/singleton.py +18 -0
  198. telegrinder/tools/state_mutator/__init__.py +4 -0
  199. telegrinder/tools/state_mutator/mutation.py +85 -0
  200. telegrinder/tools/state_storage/__init__.py +4 -0
  201. telegrinder/tools/state_storage/abc.py +38 -0
  202. telegrinder/tools/state_storage/memory.py +27 -0
  203. telegrinder/tools/strings.py +22 -0
  204. telegrinder/types/__init__.py +323 -0
  205. telegrinder/types/enums.py +754 -0
  206. telegrinder/types/input_file.py +51 -0
  207. telegrinder/types/methods.py +6143 -0
  208. telegrinder/types/methods_utils.py +66 -0
  209. telegrinder/types/objects.py +8184 -0
  210. telegrinder/types/webapp.py +129 -0
  211. telegrinder/verification_utils.py +35 -0
  212. telegrinder-1.0.0rc1.dist-info/METADATA +166 -0
  213. telegrinder-1.0.0rc1.dist-info/RECORD +215 -0
  214. telegrinder-1.0.0rc1.dist-info/WHEEL +4 -0
  215. telegrinder-1.0.0rc1.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,84 @@
1
+ import abc
2
+ import typing
3
+
4
+ from telegrinder.types.objects import InlineKeyboardMarkup, ReplyKeyboardMarkup
5
+
6
+ if typing.TYPE_CHECKING:
7
+ from telegrinder.tools.keyboard.button import BaseButton
8
+
9
+ from telegrinder.tools.keyboard.utils import RowButtons, copy_keyboard
10
+
11
+ type DictStrAny = dict[str, typing.Any]
12
+ type AnyMarkup = ReplyKeyboardMarkup | InlineKeyboardMarkup
13
+ type RawKeyboard = list[list[DictStrAny]]
14
+ type Button = DictStrAny | list[DictStrAny] | BaseButton | RowButtons[BaseButton]
15
+
16
+
17
+ class ABCKeyboard(typing.Protocol):
18
+ keyboard: RawKeyboard
19
+
20
+ __button_class__: typing.ClassVar[type[BaseButton]]
21
+
22
+ @abc.abstractmethod
23
+ def dict(self) -> DictStrAny:
24
+ pass
25
+
26
+ @abc.abstractmethod
27
+ def get_markup(self) -> AnyMarkup:
28
+ pass
29
+
30
+ @abc.abstractmethod
31
+ def copy(self, **with_changes: typing.Any) -> typing.Self:
32
+ pass
33
+
34
+ def add(self, button: Button, /) -> typing.Self:
35
+ if not self.keyboard:
36
+ self.row()
37
+
38
+ if isinstance(button, RowButtons):
39
+ self.keyboard[-1].extend(button.get_data())
40
+ if button.auto_row:
41
+ self.row()
42
+ return self
43
+
44
+ if isinstance(button, list):
45
+ self.keyboard[-1].extend(button)
46
+ return self
47
+
48
+ self.keyboard[-1].append(button if isinstance(button, dict) else button.get_data())
49
+ return self
50
+
51
+ def row(self) -> typing.Self:
52
+ if len(self.keyboard) and not len(self.keyboard[-1]):
53
+ return self
54
+
55
+ self.keyboard.append([])
56
+ return self
57
+
58
+ def format_text(self, **format_data: typing.Any) -> typing.Self:
59
+ copy_keyboard = self.copy()
60
+
61
+ for row in self.keyboard:
62
+ for button in row:
63
+ button.update(dict(text=button["text"].format(**format_data)))
64
+
65
+ return copy_keyboard
66
+
67
+ def merge(self, other: typing.Self, /) -> typing.Self:
68
+ self.keyboard.extend(copy_keyboard(other.keyboard))
69
+ return self
70
+
71
+ def merge_to_last_row(self, other: typing.Self, /) -> typing.Self:
72
+ total_rows = len(other.keyboard)
73
+
74
+ for index, row in enumerate(copy_keyboard(other.keyboard), start=1):
75
+ for button in row:
76
+ self.keyboard[-1].append(button)
77
+
78
+ if index < total_rows:
79
+ self.keyboard.append([])
80
+
81
+ return self
82
+
83
+
84
+ __all__ = ("ABCKeyboard",)
@@ -0,0 +1,108 @@
1
+ import abc
2
+ import typing
3
+
4
+ if typing.TYPE_CHECKING:
5
+ from telegrinder.tools.keyboard.abc import Button, RawKeyboard
6
+ from telegrinder.tools.keyboard.button import BaseButton
7
+
8
+ from telegrinder.tools.keyboard.abc import ABCKeyboard
9
+ from telegrinder.tools.keyboard.utils import RowButtons, copy_keyboard, get_keyboard_button_rules, is_dunder
10
+
11
+ BUTTON_CLASS_KEY: typing.Final = "__button_class__"
12
+
13
+
14
+ class KeyboardMeta(type):
15
+ if not typing.TYPE_CHECKING:
16
+
17
+ def __getattribute__(cls, __name: str) -> typing.Any:
18
+ if (
19
+ not is_dunder(__name)
20
+ and ABCKeyboard not in type.__getattribute__(cls, "__bases__")
21
+ and hasattr(cls, BUTTON_CLASS_KEY)
22
+ and (button_rule := get_keyboard_button_rules(cls).get(__name)) is not None
23
+ ):
24
+ return button_rule
25
+
26
+ return super().__getattribute__(__name)
27
+
28
+
29
+ class ABCBaseKeyboard(typing._ProtocolMeta, KeyboardMeta): # type: ignore
30
+ pass
31
+
32
+
33
+ class BaseKeyboard[KeyboardButton: BaseButton = typing.Any](typing.Protocol, metaclass=ABCBaseKeyboard):
34
+ keyboard: RawKeyboard
35
+
36
+ __keyboard_instance__: typing.ClassVar[typing.Self]
37
+ __button_class__: typing.ClassVar[type[BaseButton[typing.Self]]]
38
+
39
+ @abc.abstractmethod
40
+ def copy(self, **with_changes: typing.Any) -> typing.Self:
41
+ pass
42
+
43
+ @abc.abstractmethod
44
+ def __init_subclass__(cls) -> None:
45
+ pass
46
+
47
+ def __and__(self, other: object, /) -> typing.Self:
48
+ if not isinstance(other, self.__button_class__ | type(self)):
49
+ return NotImplemented
50
+ return self.add(other) if isinstance(other, self.__button_class__) else self.merge(other) # type: ignore
51
+
52
+ def __or__(self, other: object, /) -> typing.Self:
53
+ if not isinstance(other, self.__button_class__ | type(self)):
54
+ return NotImplemented
55
+ kb = self.row()
56
+ return kb.add(other) if isinstance(other, self.__button_class__) else kb.merge_to_last_row(other) # type: ignore
57
+
58
+ def add(self, button: Button, /) -> typing.Self:
59
+ if not self.keyboard:
60
+ self.row()
61
+
62
+ if isinstance(button, RowButtons):
63
+ self.keyboard[-1].extend(button.get_data())
64
+ if button.auto_row:
65
+ self.row()
66
+ return self
67
+
68
+ if isinstance(button, list):
69
+ self.keyboard[-1].extend(button)
70
+ return self
71
+
72
+ self.keyboard[-1].append(button if isinstance(button, dict) else button.get_data())
73
+ return self
74
+
75
+ def row(self) -> typing.Self:
76
+ if len(self.keyboard) and not len(self.keyboard[-1]):
77
+ return self
78
+
79
+ self.keyboard.append([])
80
+ return self
81
+
82
+ def format_text(self, **format_data: typing.Any) -> typing.Self:
83
+ copy_keyboard = self.copy()
84
+
85
+ for row in self.keyboard:
86
+ for button in row:
87
+ button.update(dict(text=button["text"].format(**format_data)))
88
+
89
+ return copy_keyboard
90
+
91
+ def merge(self, other: BaseKeyboard[KeyboardButton], /) -> typing.Self:
92
+ self.keyboard.extend(copy_keyboard(other.keyboard))
93
+ return self
94
+
95
+ def merge_to_last_row(self, other: BaseKeyboard[KeyboardButton], /) -> typing.Self:
96
+ total_rows = len(other.keyboard)
97
+
98
+ for index, row in enumerate(copy_keyboard(other.keyboard), start=1):
99
+ for button in row:
100
+ self.keyboard[-1].append(button)
101
+
102
+ if index < total_rows:
103
+ self.keyboard.append([])
104
+
105
+ return self
106
+
107
+
108
+ __all__ = ("BaseKeyboard",)
@@ -0,0 +1,181 @@
1
+ import abc
2
+ import dataclasses
3
+ import typing
4
+ from functools import cached_property
5
+
6
+ import msgspec
7
+
8
+ from telegrinder.msgspec_utils.encoder import encoder
9
+ from telegrinder.tools.keyboard.utils import freaky_keyboard_merge
10
+ from telegrinder.tools.serialization.json_ser import JSONSerializer
11
+ from telegrinder.tools.serialization.utils import get_model_serializer
12
+ from telegrinder.types.objects import (
13
+ CallbackGame,
14
+ CopyTextButton,
15
+ KeyboardButtonPollType,
16
+ KeyboardButtonRequestChat,
17
+ KeyboardButtonRequestUsers,
18
+ LoginUrl,
19
+ SwitchInlineQueryChosenChat,
20
+ WebAppInfo,
21
+ )
22
+
23
+ if typing.TYPE_CHECKING:
24
+ from _typeshed import DataclassInstance
25
+
26
+ from telegrinder.bot.rules.abc import ABCRule
27
+ from telegrinder.bot.rules.button import ButtonRule
28
+ from telegrinder.tools.keyboard import keyboard
29
+ from telegrinder.tools.keyboard.base import BaseKeyboard
30
+ from telegrinder.tools.keyboard.button import BaseButton
31
+ from telegrinder.tools.serialization.abc import ABCDataSerializer
32
+
33
+ type CallbackData = str | dict[str, typing.Any] | DataclassInstance | msgspec.Struct
34
+ type Keyboard = keyboard.Keyboard
35
+ type InlineKeyboard = keyboard.InlineKeyboard
36
+
37
+
38
+ @dataclasses.dataclass(kw_only=True)
39
+ class BaseButton[T: BaseKeyboard = typing.Any](abc.ABC):
40
+ new_row: bool = dataclasses.field(default=False)
41
+
42
+ @property
43
+ @abc.abstractmethod
44
+ def keyboard_class(self) -> type[T]:
45
+ pass
46
+
47
+ @property
48
+ @abc.abstractmethod
49
+ def rule(self) -> ABCRule:
50
+ pass
51
+
52
+ if typing.TYPE_CHECKING:
53
+
54
+ def __get__(self, instance: T | None, owner: type[T]) -> ButtonRule[typing.Self]: ...
55
+
56
+ @property
57
+ def as_keyboard(self: BaseButton[T]) -> type[T]: ...
58
+ else:
59
+
60
+ def as_keyboard(self, *args, **kwargs):
61
+ return self.keyboard_class(*args, **kwargs).add(self)
62
+
63
+ def __and__(self, other: object, /) -> T:
64
+ if not isinstance(other, self.keyboard_class | type(self)):
65
+ return NotImplemented
66
+ return freaky_keyboard_merge(self, other)
67
+
68
+ def __or__(self, other: object, /) -> T:
69
+ if not isinstance(other, self.keyboard_class | type(self)):
70
+ return NotImplemented
71
+ return freaky_keyboard_merge(self, other, row=True)
72
+
73
+ def get_data(self) -> dict[str, typing.Any]:
74
+ return {k: v for k, v in dataclasses.asdict(self).items() if v is not None and k != "new_row"}
75
+
76
+
77
+ @dataclasses.dataclass
78
+ class Button(BaseButton[Keyboard]):
79
+ text: str
80
+ request_contact: bool = dataclasses.field(default=False, kw_only=True)
81
+ request_location: bool = dataclasses.field(default=False, kw_only=True)
82
+ request_chat: KeyboardButtonRequestChat | None = dataclasses.field(
83
+ default=None,
84
+ kw_only=True,
85
+ )
86
+ request_user: KeyboardButtonRequestUsers | None = dataclasses.field(default=None, kw_only=True)
87
+ request_poll: KeyboardButtonPollType | None = dataclasses.field(
88
+ default=None,
89
+ kw_only=True,
90
+ )
91
+ web_app: WebAppInfo | None = dataclasses.field(default=None, kw_only=True)
92
+
93
+ @cached_property
94
+ def rule(self) -> ABCRule:
95
+ from telegrinder.bot.rules.text import Text
96
+
97
+ return Text(self.text)
98
+
99
+ @cached_property
100
+ def keyboard_class(self) -> type[Keyboard]:
101
+ from telegrinder.tools.keyboard.keyboard import Keyboard
102
+
103
+ return Keyboard
104
+
105
+
106
+ @dataclasses.dataclass
107
+ class InlineButton(BaseButton[InlineKeyboard]):
108
+ text: str
109
+ url: str | None = dataclasses.field(default=None, kw_only=True)
110
+ login_url: LoginUrl | None = dataclasses.field(default=None, kw_only=True)
111
+ pay: bool | None = dataclasses.field(default=None, kw_only=True)
112
+ callback_data: CallbackData | None = dataclasses.field(default=None, kw_only=True)
113
+ callback_data_serializer: ABCDataSerializer[typing.Any] | None = dataclasses.field(
114
+ default=None,
115
+ kw_only=True,
116
+ )
117
+ callback_game: CallbackGame | None = dataclasses.field(default=None, kw_only=True)
118
+ copy_text: str | CopyTextButton | None = dataclasses.field(default=None, kw_only=True)
119
+ switch_inline_query: str | None = dataclasses.field(default=None, kw_only=True)
120
+ switch_inline_query_current_chat: str | None = dataclasses.field(default=None, kw_only=True)
121
+ switch_inline_query_chosen_chat: SwitchInlineQueryChosenChat | None = dataclasses.field(
122
+ default=None,
123
+ kw_only=True,
124
+ )
125
+ web_app: str | WebAppInfo | None = dataclasses.field(default=None, kw_only=True)
126
+
127
+ def __post_init__(self) -> None:
128
+ model_serializer = get_model_serializer(self.callback_data)
129
+
130
+ self.input_callback_data = self.callback_data
131
+ self.callback_data_serializer = self.callback_data_serializer or (
132
+ None if model_serializer is None else model_serializer(type(self.callback_data))
133
+ )
134
+
135
+ if (
136
+ self.callback_data_serializer is None
137
+ and isinstance(self.callback_data, msgspec.Struct | dict)
138
+ or dataclasses.is_dataclass(self.callback_data)
139
+ ):
140
+ self.callback_data_serializer = self.callback_data_serializer or JSONSerializer(
141
+ type(self.callback_data),
142
+ )
143
+
144
+ if self.callback_data_serializer is not None:
145
+ self.callback_data = self.callback_data_serializer.serialize(self.callback_data)
146
+ elif self.callback_data is not None and not isinstance(self.callback_data, str):
147
+ self.callback_data = encoder.encode(self.callback_data)
148
+
149
+ if isinstance(self.copy_text, str):
150
+ self.copy_text = CopyTextButton(text=self.copy_text)
151
+
152
+ if isinstance(self.web_app, str):
153
+ self.web_app = WebAppInfo(url=self.web_app)
154
+
155
+ @cached_property
156
+ def rule(self) -> ABCRule:
157
+ from telegrinder.bot.rules.payload import PayloadEqRule, PayloadJsonEqRule, PayloadModelRule
158
+
159
+ if isinstance(self.input_callback_data, str):
160
+ return PayloadEqRule(self.input_callback_data)
161
+
162
+ if isinstance(self.input_callback_data, dict):
163
+ return PayloadJsonEqRule(self.input_callback_data)
164
+
165
+ if self.input_callback_data is not None:
166
+ return PayloadModelRule(
167
+ type(self.input_callback_data),
168
+ payload=self.input_callback_data,
169
+ serializer=type(self.callback_data_serializer) if self.callback_data_serializer else None,
170
+ )
171
+
172
+ raise ValueError("Cannot create rule, because callback data is not defined.")
173
+
174
+ @cached_property
175
+ def keyboard_class(self) -> type[InlineKeyboard]:
176
+ from telegrinder.tools.keyboard.keyboard import InlineKeyboard
177
+
178
+ return InlineKeyboard
179
+
180
+
181
+ __all__ = ("BaseButton", "Button", "InlineButton")
@@ -0,0 +1,31 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ if typing.TYPE_CHECKING:
5
+ from telegrinder.tools.keyboard.abc import DictStrAny, RawKeyboard
6
+
7
+
8
+ class KeyboardParams(typing.TypedDict, total=False):
9
+ is_persistent: bool
10
+ one_time_keyboard: bool
11
+ resize_keyboard: bool
12
+ is_selective: bool
13
+ input_field_placeholder: str | None
14
+
15
+
16
+ @dataclasses.dataclass(frozen=True)
17
+ class KeyboardModel:
18
+ keyboard: RawKeyboard
19
+ is_persistent: bool = dataclasses.field(default=False, kw_only=True)
20
+ one_time_keyboard: bool = dataclasses.field(default=False, kw_only=True)
21
+ resize_keyboard: bool = dataclasses.field(default=False, kw_only=True)
22
+ is_selective: bool = dataclasses.field(default=False, kw_only=True)
23
+ input_field_placeholder: str | None = dataclasses.field(default=None, kw_only=True)
24
+
25
+ def dict(self) -> DictStrAny:
26
+ dct = dataclasses.asdict(self)
27
+ dct["keyboard"] = [row for row in self.keyboard if row]
28
+ return dct
29
+
30
+
31
+ __all__ = ("KeyboardModel", "KeyboardParams")
@@ -0,0 +1,160 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from telegrinder.tools.keyboard.abc import ABCKeyboard, DictStrAny, RawKeyboard
5
+ from telegrinder.tools.keyboard.abc import Button as ButtonType
6
+ from telegrinder.tools.keyboard.base import BaseKeyboard
7
+ from telegrinder.tools.keyboard.button import BaseButton, Button, InlineButton
8
+ from telegrinder.tools.keyboard.data import KeyboardModel, KeyboardParams
9
+ from telegrinder.tools.keyboard.utils import (
10
+ RowButtons,
11
+ bound_keyboard_method,
12
+ copy_keyboard,
13
+ init_keyboard,
14
+ )
15
+ from telegrinder.types.objects import InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove
16
+
17
+ if typing.TYPE_CHECKING:
18
+ from telegrinder.tools.serialization.abc import ABCDataSerializer
19
+
20
+
21
+ class Keyboard(BaseKeyboard[Button], ABCKeyboard):
22
+ __button_class__ = Button
23
+ __slots__ = ("keyboard_model",)
24
+
25
+ def __init_subclass__(cls, *, max_in_row: int = 0, **kwargs: typing.Unpack[KeyboardParams]) -> None:
26
+ cls.__keyboard_instance__ = init_keyboard(cls(**kwargs), max_in_row=max_in_row)
27
+
28
+ def __init__(
29
+ self,
30
+ *,
31
+ keyboard_model: KeyboardModel | None = None,
32
+ **kwargs: typing.Unpack[KeyboardParams],
33
+ ) -> None:
34
+ self.keyboard_model = keyboard_model or KeyboardModel(keyboard=[[]], **kwargs)
35
+
36
+ def __repr__(self) -> str:
37
+ return f"<{type(self).__name__}: keyboard_model={self.keyboard_model!r}>"
38
+
39
+ @property
40
+ def keyboard(self) -> RawKeyboard:
41
+ return self.keyboard_model.keyboard
42
+
43
+ @property
44
+ def is_persistent(self) -> bool:
45
+ return self.keyboard_model.is_persistent
46
+
47
+ @property
48
+ def one_time_keyboard(self) -> bool:
49
+ return self.keyboard_model.one_time_keyboard
50
+
51
+ @property
52
+ def resize_keyboard(self) -> bool:
53
+ return self.keyboard_model.resize_keyboard
54
+
55
+ @property
56
+ def is_selective(self) -> bool:
57
+ return self.keyboard_model.is_selective
58
+
59
+ @property
60
+ def input_field_placeholder(self) -> str | None:
61
+ return self.keyboard_model.input_field_placeholder
62
+
63
+ @bound_keyboard_method
64
+ def copy(self, **with_changes: typing.Any) -> typing.Self:
65
+ keyboard_model = dataclasses.replace(
66
+ self.keyboard_model,
67
+ keyboard=copy_keyboard(self.keyboard),
68
+ **with_changes,
69
+ )
70
+ return type(self)(keyboard_model=keyboard_model)
71
+
72
+ @bound_keyboard_method
73
+ def dict(self) -> DictStrAny:
74
+ return self.keyboard_model.dict()
75
+
76
+ @bound_keyboard_method
77
+ def get_markup(self) -> ReplyKeyboardMarkup:
78
+ return ReplyKeyboardMarkup.from_dict(self.dict())
79
+
80
+ @bound_keyboard_method
81
+ def get_keyboard_remove(self) -> ReplyKeyboardRemove:
82
+ return ReplyKeyboardRemove(remove_keyboard=True, selective=self.is_selective)
83
+
84
+ def placeholder(self, value: str | None, /) -> typing.Self:
85
+ return self.copy(input_field_placeholder=value)
86
+
87
+ def resize(self) -> typing.Self:
88
+ return self.copy(resize_keyboard=True)
89
+
90
+ def one_time(self) -> typing.Self:
91
+ return self.copy(one_time_keyboard=True)
92
+
93
+ def selective(self) -> typing.Self:
94
+ return self.copy(is_selective=True)
95
+
96
+ def persistent(self) -> typing.Self:
97
+ return self.copy(is_persistent=True)
98
+
99
+ def no_resize(self) -> typing.Self:
100
+ return self.copy(resize_keyboard=False)
101
+
102
+ def no_one_time(self) -> typing.Self:
103
+ return self.copy(one_time_keyboard=False)
104
+
105
+ def no_selective(self) -> typing.Self:
106
+ return self.copy(is_selective=False)
107
+
108
+ def no_persistent(self) -> typing.Self:
109
+ return self.copy(is_persistent=False)
110
+
111
+
112
+ class InlineKeyboard(BaseKeyboard[InlineButton], ABCKeyboard):
113
+ __button_class__ = InlineButton
114
+ __serializer__: ABCDataSerializer | None = None
115
+ __slots__ = ("keyboard", "serializer")
116
+
117
+ def __init_subclass__(
118
+ cls,
119
+ *,
120
+ max_in_row: int = 0,
121
+ serializer: ABCDataSerializer | None = None,
122
+ ) -> None:
123
+ cls.__keyboard_instance__ = init_keyboard(cls(), max_in_row=max_in_row)
124
+ cls.__serializer__ = serializer
125
+
126
+ def __init__(
127
+ self,
128
+ *,
129
+ serializer: ABCDataSerializer | None = None,
130
+ ) -> None:
131
+ self.keyboard = [[]]
132
+ self.serializer = serializer or self.__serializer__
133
+
134
+ def __repr__(self) -> str:
135
+ return f"<{type(self).__name__}: keyboard={self.keyboard!r}>"
136
+
137
+ def add(self, button: ButtonType, /) -> typing.Self:
138
+ if isinstance(button, BaseButton | RowButtons):
139
+ for b in button.buttons if isinstance(button, RowButtons) else (button,):
140
+ if isinstance(b, InlineButton) and b.callback_data_serializer is None:
141
+ b.callback_data_serializer = self.serializer
142
+
143
+ return super().add(button)
144
+
145
+ @bound_keyboard_method
146
+ def copy(self, **with_changes: typing.Any) -> typing.Self:
147
+ new_keyboard = type(self)()
148
+ new_keyboard.keyboard = copy_keyboard(self.keyboard)
149
+ return new_keyboard
150
+
151
+ @bound_keyboard_method
152
+ def dict(self) -> DictStrAny:
153
+ return dict(inline_keyboard=[row for row in self.keyboard if row])
154
+
155
+ @bound_keyboard_method
156
+ def get_markup(self) -> InlineKeyboardMarkup:
157
+ return InlineKeyboardMarkup.from_dict(self.dict())
158
+
159
+
160
+ __all__ = ("InlineKeyboard", "Keyboard")
@@ -0,0 +1,95 @@
1
+ import typing
2
+ from functools import lru_cache
3
+
4
+ if typing.TYPE_CHECKING:
5
+ from telegrinder.bot.rules.button import ButtonRule
6
+ from telegrinder.tools.keyboard.abc import ABCKeyboard, RawKeyboard
7
+ from telegrinder.tools.keyboard.base import BaseKeyboard
8
+ from telegrinder.tools.keyboard.button import BaseButton
9
+
10
+
11
+ @lru_cache(maxsize=1024)
12
+ def get_keyboard_button_rules(
13
+ keyboard_class: type[ABCKeyboard],
14
+ /,
15
+ ) -> dict[str, ButtonRule[BaseButton[typing.Any]]]:
16
+ from telegrinder.bot.rules.button import ButtonRule
17
+
18
+ return {
19
+ name: ButtonRule(button=button, rule=button.rule)
20
+ for name, button in dict(vars(keyboard_class)).items()
21
+ if isinstance(button, keyboard_class.__button_class__) and not is_dunder(name)
22
+ }
23
+
24
+
25
+ def is_dunder(name: str, /) -> bool:
26
+ return name.startswith("__") and name.endswith("__")
27
+
28
+
29
+ def copy_keyboard(keyboard: RawKeyboard, /) -> RawKeyboard:
30
+ return [row.copy() for row in keyboard if row]
31
+
32
+
33
+ def freaky_keyboard_merge[T: BaseKeyboard = typing.Any](
34
+ button: BaseButton[T],
35
+ keyboard_or_button: T | BaseButton[T],
36
+ /,
37
+ *,
38
+ row: bool = False,
39
+ ) -> T:
40
+ from telegrinder.tools.keyboard.button import BaseButton
41
+
42
+ keyboard = button.keyboard_class().add(button)
43
+
44
+ if row:
45
+ keyboard = keyboard.row()
46
+
47
+ return (
48
+ keyboard.merge_to_last_row(keyboard_or_button)
49
+ if not isinstance(keyboard_or_button, BaseButton)
50
+ else keyboard.add(keyboard_or_button)
51
+ )
52
+
53
+
54
+ def init_keyboard[T: BaseKeyboard = typing.Any](
55
+ keyboard_instance: T,
56
+ /,
57
+ *,
58
+ max_in_row: int = 0,
59
+ ) -> T:
60
+ for button in get_keyboard_button_rules(type(keyboard_instance)).values():
61
+ if button.button.new_row or (max_in_row and len(keyboard_instance.keyboard[-1]) >= max_in_row):
62
+ keyboard_instance.row()
63
+
64
+ keyboard_instance.add(button.button)
65
+
66
+ return keyboard_instance
67
+
68
+
69
+ class bound_keyboard_method[T: BaseKeyboard = typing.Any, **P = ..., R = typing.Any]: # noqa: N801
70
+ def __init__(self, func: typing.Callable[typing.Concatenate[T, P], R], /) -> None:
71
+ self.func = func
72
+
73
+ def __get__(self, instance: T | None, owner: type[T]) -> typing.Callable[P, R]:
74
+ return self.func.__get__(instance or owner.__keyboard_instance__, owner)
75
+
76
+
77
+ class RowButtons[KeyboardButton: BaseButton]:
78
+ buttons: typing.Iterable[KeyboardButton]
79
+
80
+ def __init__(self, *buttons: KeyboardButton, auto_row: bool = True) -> None:
81
+ self.buttons = buttons
82
+ self.auto_row = auto_row
83
+
84
+ def get_data(self) -> list[dict[str, typing.Any]]:
85
+ return [b.get_data() for b in self.buttons]
86
+
87
+
88
+ __all__ = (
89
+ "RowButtons",
90
+ "bound_keyboard_method",
91
+ "copy_keyboard",
92
+ "freaky_keyboard_merge",
93
+ "get_keyboard_button_rules",
94
+ "init_keyboard",
95
+ )