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,183 @@
1
+ import dataclasses
2
+ import enum
3
+ import secrets
4
+ import typing
5
+
6
+ from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
7
+ from telegrinder.bot.dispatch.waiter_machine.hasher.hasher import Hasher
8
+ from telegrinder.bot.dispatch.waiter_machine.machine import WaiterMachine
9
+ from telegrinder.bot.scenario.abc import ABCScenario
10
+ from telegrinder.tools.keyboard import InlineButton, InlineKeyboard
11
+ from telegrinder.tools.parse_mode import ParseMode
12
+ from telegrinder.types.objects import InlineKeyboardMarkup
13
+
14
+ if typing.TYPE_CHECKING:
15
+ from telegrinder.api.api import API
16
+ from telegrinder.bot.dispatch.view.base import View
17
+
18
+ type MessageId = int
19
+ type CallbackQueryView = View
20
+
21
+
22
+ class ChoiceAction(enum.StrEnum):
23
+ READY = "ready"
24
+ CANCEL = "cancel"
25
+
26
+
27
+ @dataclasses.dataclass(slots=True)
28
+ class Choice[Key: typing.Hashable]:
29
+ key: Key
30
+ is_picked: bool
31
+ default_text: str
32
+ picked_text: str
33
+ code: str
34
+
35
+
36
+ class _Checkbox[T](ABCScenario):
37
+ choices: list[Choice[typing.Hashable]]
38
+
39
+ INVALID_CODE = "Invalid code"
40
+ CALLBACK_ANSWER = "Done"
41
+ PARSE_MODE = ParseMode.HTML
42
+
43
+ def __init__(
44
+ self,
45
+ waiter_machine: WaiterMachine,
46
+ chat_id: int,
47
+ message: str,
48
+ *,
49
+ ready_text: str = "Ready",
50
+ cancel_text: str | None = None,
51
+ max_in_row: int = 3,
52
+ ) -> None:
53
+ self.chat_id = chat_id
54
+ self.message = message
55
+ self.choices = []
56
+ self.ready = ready_text
57
+ self.max_in_row = max_in_row
58
+ self.random_code = secrets.token_hex(8)
59
+ self.waiter_machine = waiter_machine
60
+ self.cancel_text = cancel_text
61
+
62
+ def __repr__(self) -> str:
63
+ return (
64
+ "<{}@{!r}: (choices={!r}, max_in_row={}) with waiter_machine={!r}, ready_text={!r} "
65
+ "for chat_id={} with message={!r}>"
66
+ ).format(
67
+ type(self).__name__,
68
+ self.random_code,
69
+ self.choices,
70
+ self.max_in_row,
71
+ self.waiter_machine,
72
+ self.ready,
73
+ self.chat_id,
74
+ self.message,
75
+ )
76
+
77
+ def get_markup(self) -> InlineKeyboardMarkup:
78
+ kb = InlineKeyboard()
79
+ choices = self.choices.copy()
80
+ while choices:
81
+ while len(kb.keyboard[-1]) < self.max_in_row and choices:
82
+ choice = choices.pop(0)
83
+ kb.add(
84
+ InlineButton(
85
+ text=(choice.default_text if not choice.is_picked else choice.picked_text),
86
+ callback_data=self.random_code + "/" + choice.code,
87
+ )
88
+ )
89
+ kb.row()
90
+
91
+ kb.add(InlineButton(self.ready, callback_data=self.random_code + "/" + ChoiceAction.READY))
92
+ if self.cancel_text is not None:
93
+ kb.row()
94
+ kb.add(InlineButton(self.cancel_text, callback_data=self.random_code + "/" + ChoiceAction.CANCEL))
95
+
96
+ return kb.get_markup()
97
+
98
+ def add_option[Key: typing.Hashable](
99
+ self,
100
+ key: Key,
101
+ default_text: str,
102
+ picked_text: str,
103
+ *,
104
+ is_picked: bool = False,
105
+ ) -> Checkbox[Key]:
106
+ self.choices.append(
107
+ Choice(key, is_picked, default_text, picked_text, secrets.token_hex(8)),
108
+ )
109
+ return self # type: ignore
110
+
111
+ async def handle(self, cb: CallbackQueryCute) -> bool:
112
+ code = cb.data.unwrap().replace(self.random_code + "/", "", 1)
113
+
114
+ match code:
115
+ case ChoiceAction.READY:
116
+ return False
117
+ case ChoiceAction.CANCEL:
118
+ self.choices = []
119
+ return False
120
+
121
+ for i, choice in enumerate(self.choices):
122
+ if choice.code == code:
123
+ # Toggle choice
124
+ self.choices[i].is_picked = not self.choices[i].is_picked
125
+ await cb.edit_text(
126
+ text=self.message,
127
+ parse_mode=self.PARSE_MODE,
128
+ reply_markup=self.get_markup(),
129
+ )
130
+ break
131
+
132
+ return True
133
+
134
+ async def wait(
135
+ self,
136
+ hasher: Hasher[CallbackQueryCute, MessageId],
137
+ view: CallbackQueryView,
138
+ api: API,
139
+ ) -> tuple[dict[typing.Hashable, bool], MessageId]:
140
+ assert len(self.choices) > 0
141
+ message = (
142
+ await api.send_message(
143
+ chat_id=self.chat_id,
144
+ text=self.message,
145
+ parse_mode=self.PARSE_MODE,
146
+ reply_markup=self.get_markup(),
147
+ )
148
+ ).unwrap()
149
+
150
+ while True:
151
+ q, _ = await self.waiter_machine.wait(
152
+ hasher,
153
+ view=view,
154
+ data=message.message_id,
155
+ )
156
+ should_continue = await self.handle(q)
157
+ await q.answer(self.CALLBACK_ANSWER)
158
+ if not should_continue:
159
+ break
160
+
161
+ return (
162
+ {choice.key: choice.is_picked for choice in self.choices},
163
+ message.message_id,
164
+ )
165
+
166
+
167
+ if typing.TYPE_CHECKING:
168
+
169
+ class Checkbox[Key: typing.Hashable](_Checkbox):
170
+ choices: list[Choice[Key]]
171
+
172
+ async def wait(
173
+ self,
174
+ hasher: Hasher[CallbackQueryCute, MessageId],
175
+ view: CallbackQueryView,
176
+ api: API,
177
+ ) -> tuple[dict[Key, bool], MessageId]: ...
178
+
179
+ else:
180
+ Checkbox = _Checkbox
181
+
182
+
183
+ __all__ = ("Checkbox", "Choice")
@@ -0,0 +1,44 @@
1
+ import typing
2
+
3
+ from telegrinder.api.api import API
4
+ from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
5
+ from telegrinder.bot.dispatch.waiter_machine.hasher.hasher import Hasher
6
+ from telegrinder.bot.scenario.checkbox import CallbackQueryView, Checkbox, ChoiceAction, MessageId
7
+
8
+
9
+ class Choice[Key: typing.Hashable](Checkbox[Key]):
10
+ async def handle(self, cb: CallbackQueryCute) -> bool:
11
+ code = cb.data.unwrap().replace(f"{self.random_code}/", "", 1)
12
+ if code == ChoiceAction.READY:
13
+ return False
14
+
15
+ for choice in self.choices:
16
+ choice.is_picked = False
17
+
18
+ for i, choice in enumerate(self.choices):
19
+ if choice.code == code:
20
+ self.choices[i].is_picked = True
21
+ await cb.ctx_api.edit_message_text(
22
+ text=self.message,
23
+ chat_id=cb.message.unwrap().v.chat.id,
24
+ message_id=cb.message.unwrap().v.message_id,
25
+ parse_mode=self.PARSE_MODE,
26
+ reply_markup=self.get_markup(),
27
+ )
28
+
29
+ return True
30
+
31
+ async def wait(
32
+ self,
33
+ hasher: Hasher[CallbackQueryCute, MessageId],
34
+ view: CallbackQueryView,
35
+ api: API,
36
+ ) -> tuple[Key, MessageId]:
37
+ if len(tuple(choice for choice in self.choices if choice.is_picked)) != 1:
38
+ raise ValueError("Exactly one choice must be picked.")
39
+
40
+ choices, m_id = await super().wait(hasher, view, api)
41
+ return tuple(choices.keys())[tuple(choices.values()).index(True)], m_id
42
+
43
+
44
+ __all__ = ("Choice",)
@@ -0,0 +1,11 @@
1
+ from telegrinder.client.abc import ABCClient, Response
2
+ from telegrinder.client.form_data import MultipartBuilderProto, encode_form_data
3
+ from telegrinder.client.rnet import RnetClient
4
+
5
+ __all__ = (
6
+ "ABCClient",
7
+ "MultipartBuilderProto",
8
+ "Response",
9
+ "RnetClient",
10
+ "encode_form_data",
11
+ )
@@ -0,0 +1,136 @@
1
+ import dataclasses
2
+ import typing
3
+ from abc import ABC, abstractmethod
4
+ from datetime import timedelta
5
+ from http import HTTPStatus
6
+
7
+ from telegrinder.client.form_data import MultipartBuilderProto, encode_form_data
8
+
9
+ if typing.TYPE_CHECKING:
10
+ import datetime
11
+
12
+ type Data = typing.Any
13
+ type Files = dict[str, tuple[str, typing.Any]]
14
+ type Timeout = int | float | datetime.timedelta
15
+
16
+
17
+ @dataclasses.dataclass(frozen=True, slots=True)
18
+ class Response[T = typing.Any]:
19
+ response: T
20
+ content: bytes
21
+ status: HTTPStatus
22
+
23
+
24
+ class ABCClient(ABC):
25
+ CONNECTION_TIMEOUT_ERRORS: tuple[type[BaseException], ...] = ()
26
+ CLIENT_CONNECTION_ERRORS: tuple[type[BaseException], ...] = ()
27
+
28
+ @abstractmethod
29
+ def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
30
+ pass
31
+
32
+ @property
33
+ @abstractmethod
34
+ def timeout(self) -> datetime.timedelta:
35
+ pass
36
+
37
+ @abstractmethod
38
+ async def request(
39
+ self,
40
+ url: str,
41
+ method: str = "GET",
42
+ data: Data | None = None,
43
+ timeout: int | float | timedelta | None = None,
44
+ **kwargs: typing.Any,
45
+ ) -> Response:
46
+ pass
47
+
48
+ @abstractmethod
49
+ async def request_text(
50
+ self,
51
+ url: str,
52
+ method: str = "GET",
53
+ data: Data | None = None,
54
+ timeout: Timeout | None = None,
55
+ **kwargs: typing.Any,
56
+ ) -> str:
57
+ pass
58
+
59
+ @abstractmethod
60
+ async def request_json(
61
+ self,
62
+ url: str,
63
+ method: str = "GET",
64
+ data: Data | None = None,
65
+ timeout: Timeout | None = None,
66
+ **kwargs: typing.Any,
67
+ ) -> dict[str, typing.Any]:
68
+ pass
69
+
70
+ @abstractmethod
71
+ async def request_content(
72
+ self,
73
+ url: str,
74
+ method: str = "GET",
75
+ data: Data | None = None,
76
+ timeout: Timeout | None = None,
77
+ **kwargs: typing.Any,
78
+ ) -> bytes:
79
+ pass
80
+
81
+ @abstractmethod
82
+ async def request_bytes(
83
+ self,
84
+ url: str,
85
+ method: str = "GET",
86
+ data: Data | None = None,
87
+ timeout: Timeout | None = None,
88
+ **kwargs: typing.Any,
89
+ ) -> bytes:
90
+ pass
91
+
92
+ @abstractmethod
93
+ async def close(self, **kwargs: typing.Any) -> None:
94
+ pass
95
+
96
+ @classmethod
97
+ @abstractmethod
98
+ def multipart_form_builder(cls) -> MultipartBuilderProto:
99
+ pass
100
+
101
+ @classmethod
102
+ def get_form(
103
+ cls,
104
+ *,
105
+ data: dict[str, typing.Any] | None = None,
106
+ files: Files | None = None,
107
+ ) -> typing.Any:
108
+ builder = cls.multipart_form_builder()
109
+
110
+ if not data and not files:
111
+ return builder.build()
112
+
113
+ data = data or {}
114
+ files = files or {}
115
+
116
+ for k, v in encode_form_data(data, files).items():
117
+ builder.add_field(k, v)
118
+
119
+ for n, (filename, content) in files.items():
120
+ builder.add_field(n, content, filename=filename)
121
+
122
+ return builder.build()
123
+
124
+ async def __aenter__(self) -> typing.Self:
125
+ return self
126
+
127
+ async def __aexit__(
128
+ self,
129
+ exc_type: typing.Any,
130
+ exc_val: typing.Any,
131
+ exc_tb: typing.Any,
132
+ ) -> None:
133
+ await self.close()
134
+
135
+
136
+ __all__ = ("ABCClient", "Response")
@@ -0,0 +1,34 @@
1
+ import typing
2
+
3
+ from telegrinder.msgspec_utils import encoder
4
+
5
+
6
+ def encode_form_data(
7
+ data: dict[str, typing.Any],
8
+ files: dict[str, tuple[str, typing.Any]],
9
+ /,
10
+ ) -> dict[str, str]:
11
+ context = dict(files=files)
12
+ return {
13
+ k: encoder.encode(v, context=context).strip('"') # Remove quoted string
14
+ if not isinstance(v, str)
15
+ else v
16
+ for k, v in data.items()
17
+ }
18
+
19
+
20
+ @typing.runtime_checkable
21
+ class MultipartBuilderProto(typing.Protocol):
22
+ def add_field(
23
+ self,
24
+ name: str,
25
+ value: typing.Any,
26
+ /,
27
+ filename: str | None = None,
28
+ **kwargs: typing.Any,
29
+ ) -> None: ...
30
+
31
+ def build(self) -> typing.Any: ...
32
+
33
+
34
+ __all__ = ("MultipartBuilderProto", "encode_form_data")
@@ -0,0 +1,198 @@
1
+ import dataclasses
2
+ import datetime
3
+ import pathlib
4
+ import sys
5
+ import typing
6
+ from http import HTTPStatus
7
+
8
+ import certifi
9
+ import rnet
10
+ import rnet.exceptions
11
+ from rnet import Method as HTTPMethod
12
+
13
+ from telegrinder.__meta__ import __version__
14
+ from telegrinder.client.abc import ABCClient, Response
15
+ from telegrinder.modules import json
16
+
17
+ if typing.TYPE_CHECKING:
18
+ from rnet import ClientConfig, Request
19
+
20
+ type Data = dict[str, typing.Any] | rnet.Multipart
21
+ type Method = typing.Literal["GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "TRACE", "PATCH"]
22
+
23
+ _METHODS_MAP: typing.Final[dict[Method, HTTPMethod]] = {
24
+ "GET": HTTPMethod.GET,
25
+ "HEAD": HTTPMethod.HEAD,
26
+ "POST": HTTPMethod.POST,
27
+ "PUT": HTTPMethod.PUT,
28
+ "DELETE": HTTPMethod.DELETE,
29
+ "OPTIONS": HTTPMethod.OPTIONS,
30
+ "TRACE": HTTPMethod.TRACE,
31
+ "PATCH": HTTPMethod.PATCH,
32
+ }
33
+ USER_AGENT: typing.Final = "CPython/{}.{} RNET/3 Telegrinder/{}".format(
34
+ sys.version_info.major,
35
+ sys.version_info.minor,
36
+ __version__,
37
+ )
38
+ DEFAULT_CONNECTION_TIMEOUT: typing.Final = datetime.timedelta(seconds=30)
39
+ DEFAULT_READ_TIMEOUT: typing.Final = datetime.timedelta(seconds=30)
40
+ DEFAULT_TIMEOUT: typing.Final = datetime.timedelta(seconds=30)
41
+ DEFAULT_HTTP2_MAX_RETRIES: typing.Final = 10
42
+ DEFAULT_ZSTD: typing.Final = True
43
+ DEFAULT_VERIFY: typing.Final = pathlib.Path(certifi.where())
44
+ DEFAULT_ALLOW_REDIRECTS: typing.Final = True
45
+ DEFAULT_HTTP2_ONLY: typing.Final = True
46
+ DEFAULT_TCP_KEEPALIVE_TIME: typing.Final = datetime.timedelta(seconds=60)
47
+ DEFAULT_TCP_KEEPALIVE_INTERVAL: typing.Final = datetime.timedelta(seconds=15)
48
+ DEFAULT_TCP_KEEPALIVE_RETRIES: typing.Final = 4
49
+ DEFAULT_TCP_USER_TIMEOUT: typing.Final = (
50
+ DEFAULT_TCP_KEEPALIVE_TIME + DEFAULT_TCP_KEEPALIVE_INTERVAL
51
+ ) * DEFAULT_TCP_KEEPALIVE_RETRIES
52
+ DEFAULT_TCP_REUSEADDR: typing.Final = True
53
+ DEFAULT_CONNECTION_POOL_IDLE_TIMEOUT: typing.Final = datetime.timedelta(seconds=60)
54
+ DEFAULT_CONNECTION_POOL_CONNECTIONS: typing.Final = 32
55
+ CONNECTION_POOL_MAX_SIZE: typing.Final = DEFAULT_CONNECTION_POOL_CONNECTIONS * 2
56
+
57
+
58
+ @dataclasses.dataclass(frozen=True, slots=True)
59
+ class RnetMultipartBuilder:
60
+ parts: list[rnet.Part] = dataclasses.field(default_factory=list[rnet.Part])
61
+
62
+ def add_field(
63
+ self,
64
+ name: str,
65
+ value: typing.Any,
66
+ /,
67
+ *,
68
+ filename: str | None = None,
69
+ ) -> None:
70
+ self.parts.append(rnet.Part(name, value, filename=filename))
71
+
72
+ def build(self) -> rnet.Multipart:
73
+ return rnet.Multipart(*self.parts)
74
+
75
+
76
+ class RnetClient(ABCClient):
77
+ __slots__ = ("_timeout", "_client")
78
+
79
+ CONNECTION_TIMEOUT_ERRORS: typing.ClassVar = (
80
+ TimeoutError,
81
+ rnet.exceptions.TimeoutError,
82
+ rnet.exceptions.RustPanic,
83
+ )
84
+ CLIENT_CONNECTION_ERRORS: typing.ClassVar = (
85
+ rnet.exceptions.ConnectionError,
86
+ rnet.exceptions.ConnectionResetError,
87
+ rnet.exceptions.TlsError,
88
+ rnet.exceptions.RustPanic,
89
+ )
90
+
91
+ def __init__(self, **params: typing.Unpack[ClientConfig]) -> None:
92
+ params.setdefault("user_agent", USER_AGENT)
93
+ params.setdefault("connect_timeout", DEFAULT_CONNECTION_TIMEOUT)
94
+ params.setdefault("read_timeout", DEFAULT_READ_TIMEOUT)
95
+ params.setdefault("verify", DEFAULT_VERIFY)
96
+ params.setdefault("http2_only", DEFAULT_HTTP2_ONLY)
97
+ params.setdefault("zstd", DEFAULT_ZSTD)
98
+ params.setdefault("tcp_keepalive", DEFAULT_TCP_KEEPALIVE_TIME)
99
+ params.setdefault("tcp_keepalive_interval", DEFAULT_TCP_KEEPALIVE_INTERVAL)
100
+ params.setdefault("tcp_keepalive_retries", DEFAULT_TCP_KEEPALIVE_RETRIES)
101
+ params.setdefault("tcp_user_timeout", DEFAULT_TCP_USER_TIMEOUT)
102
+ params.setdefault("tcp_reuse_address", DEFAULT_TCP_REUSEADDR)
103
+ params.setdefault("pool_idle_timeout", DEFAULT_CONNECTION_POOL_IDLE_TIMEOUT)
104
+ params.setdefault("pool_max_idle_per_host", DEFAULT_CONNECTION_POOL_CONNECTIONS)
105
+ params.setdefault("pool_max_size", CONNECTION_POOL_MAX_SIZE)
106
+
107
+ self._timeout = params.setdefault("timeout", DEFAULT_TIMEOUT)
108
+ self._client = rnet.Client(**params)
109
+
110
+ def __repr__(self) -> str:
111
+ return "<{} {!r}, timeout={!r}>".format(
112
+ type(self).__name__,
113
+ self._client,
114
+ self._timeout,
115
+ )
116
+
117
+ @property
118
+ def timeout(self) -> datetime.timedelta:
119
+ return self._timeout
120
+
121
+ @classmethod
122
+ def multipart_form_builder(cls) -> RnetMultipartBuilder:
123
+ return RnetMultipartBuilder()
124
+
125
+ async def request(
126
+ self,
127
+ url: str,
128
+ method: Method = "GET",
129
+ data: Data | None = None,
130
+ **kwargs: typing.Unpack[Request],
131
+ ) -> Response[rnet.Response]:
132
+ kwargs.setdefault("version", rnet.Version.HTTP_2)
133
+ kwargs.setdefault("zstd", DEFAULT_ZSTD)
134
+
135
+ if data is not None:
136
+ if isinstance(data, rnet.Multipart):
137
+ kwargs["multipart"] = data
138
+ elif isinstance(data, dict):
139
+ kwargs["json"] = data
140
+
141
+ if (json_body := kwargs.pop("json", None)) is not None:
142
+ kwargs["body"] = json.dumps(json_body)
143
+
144
+ if (timeout := kwargs.get("timeout")) is not None and isinstance(timeout, int | float):
145
+ kwargs["timeout"] = datetime.timedelta(seconds=timeout)
146
+
147
+ response = await self._client.request(_METHODS_MAP[method], url, **kwargs)
148
+ return Response(
149
+ response=response,
150
+ content=await response.bytes(),
151
+ status=HTTPStatus(response.status.as_int()),
152
+ )
153
+
154
+ async def request_text(
155
+ self,
156
+ *,
157
+ url: str,
158
+ method: Method = "GET",
159
+ data: Data | None = None,
160
+ **kwargs: typing.Unpack[Request],
161
+ ) -> str:
162
+ return await (await self.request(url, method, data=data, **kwargs)).response.text_with_charset(encoding="utf-8")
163
+
164
+ async def request_bytes(
165
+ self,
166
+ *,
167
+ url: str,
168
+ method: Method = "GET",
169
+ data: Data | None = None,
170
+ **kwargs: typing.Unpack[Request],
171
+ ) -> bytes:
172
+ return (await self.request(url, method, data=data, **kwargs)).content
173
+
174
+ async def request_content(
175
+ self,
176
+ *,
177
+ url: str,
178
+ method: Method = "GET",
179
+ data: Data | None = None,
180
+ **kwargs: typing.Unpack[Request],
181
+ ) -> bytes:
182
+ return await self.request_bytes(url=url, method=method, data=data, **kwargs)
183
+
184
+ async def request_json(
185
+ self,
186
+ *,
187
+ url: str,
188
+ method: Method = "GET",
189
+ data: Data | None = None,
190
+ **kwargs: typing.Unpack[Request],
191
+ ) -> dict[str, typing.Any]:
192
+ return json.loads(await self.request_bytes(url=url, method=method, data=data, **kwargs))
193
+
194
+ async def close(self) -> None:
195
+ return None
196
+
197
+
198
+ __all__ = ("RnetClient", "RnetMultipartBuilder")