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.
- telegrinder/__init__.py +258 -0
- telegrinder/__meta__.py +1 -0
- telegrinder/api/__init__.py +15 -0
- telegrinder/api/api.py +175 -0
- telegrinder/api/error.py +50 -0
- telegrinder/api/response.py +23 -0
- telegrinder/api/token.py +30 -0
- telegrinder/api/validators.py +30 -0
- telegrinder/bot/__init__.py +144 -0
- telegrinder/bot/bot.py +70 -0
- telegrinder/bot/cute_types/__init__.py +41 -0
- telegrinder/bot/cute_types/base.py +228 -0
- telegrinder/bot/cute_types/base.pyi +49 -0
- telegrinder/bot/cute_types/business_connection.py +9 -0
- telegrinder/bot/cute_types/business_messages_deleted.py +9 -0
- telegrinder/bot/cute_types/callback_query.py +248 -0
- telegrinder/bot/cute_types/chat_boost_removed.py +9 -0
- telegrinder/bot/cute_types/chat_boost_updated.py +9 -0
- telegrinder/bot/cute_types/chat_join_request.py +59 -0
- telegrinder/bot/cute_types/chat_member_updated.py +158 -0
- telegrinder/bot/cute_types/chosen_inline_result.py +11 -0
- telegrinder/bot/cute_types/inline_query.py +41 -0
- telegrinder/bot/cute_types/message.py +2809 -0
- telegrinder/bot/cute_types/message_reaction_count_updated.py +9 -0
- telegrinder/bot/cute_types/message_reaction_updated.py +9 -0
- telegrinder/bot/cute_types/paid_media_purchased.py +11 -0
- telegrinder/bot/cute_types/poll.py +9 -0
- telegrinder/bot/cute_types/poll_answer.py +9 -0
- telegrinder/bot/cute_types/pre_checkout_query.py +36 -0
- telegrinder/bot/cute_types/shipping_query.py +11 -0
- telegrinder/bot/cute_types/update.py +209 -0
- telegrinder/bot/cute_types/utils.py +141 -0
- telegrinder/bot/dispatch/__init__.py +99 -0
- telegrinder/bot/dispatch/abc.py +74 -0
- telegrinder/bot/dispatch/action.py +99 -0
- telegrinder/bot/dispatch/context.py +162 -0
- telegrinder/bot/dispatch/dispatch.py +362 -0
- telegrinder/bot/dispatch/handler/__init__.py +23 -0
- telegrinder/bot/dispatch/handler/abc.py +25 -0
- telegrinder/bot/dispatch/handler/audio_reply.py +43 -0
- telegrinder/bot/dispatch/handler/base.py +34 -0
- telegrinder/bot/dispatch/handler/document_reply.py +43 -0
- telegrinder/bot/dispatch/handler/func.py +73 -0
- telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
- telegrinder/bot/dispatch/handler/message_reply.py +35 -0
- telegrinder/bot/dispatch/handler/photo_reply.py +43 -0
- telegrinder/bot/dispatch/handler/sticker_reply.py +36 -0
- telegrinder/bot/dispatch/handler/video_reply.py +43 -0
- telegrinder/bot/dispatch/middleware/__init__.py +13 -0
- telegrinder/bot/dispatch/middleware/abc.py +112 -0
- telegrinder/bot/dispatch/middleware/box.py +32 -0
- telegrinder/bot/dispatch/middleware/filter.py +88 -0
- telegrinder/bot/dispatch/middleware/media_group.py +69 -0
- telegrinder/bot/dispatch/process.py +93 -0
- telegrinder/bot/dispatch/return_manager/__init__.py +21 -0
- telegrinder/bot/dispatch/return_manager/abc.py +107 -0
- telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
- telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
- telegrinder/bot/dispatch/return_manager/message.py +34 -0
- telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +19 -0
- telegrinder/bot/dispatch/return_manager/utils.py +20 -0
- telegrinder/bot/dispatch/router/__init__.py +4 -0
- telegrinder/bot/dispatch/router/abc.py +15 -0
- telegrinder/bot/dispatch/router/base.py +154 -0
- telegrinder/bot/dispatch/view/__init__.py +15 -0
- telegrinder/bot/dispatch/view/abc.py +15 -0
- telegrinder/bot/dispatch/view/base.py +226 -0
- telegrinder/bot/dispatch/view/box.py +207 -0
- telegrinder/bot/dispatch/view/media_group.py +25 -0
- telegrinder/bot/dispatch/waiter_machine/__init__.py +25 -0
- telegrinder/bot/dispatch/waiter_machine/actions.py +16 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +13 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +53 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +61 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +49 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +264 -0
- telegrinder/bot/dispatch/waiter_machine/middleware.py +77 -0
- telegrinder/bot/dispatch/waiter_machine/short_state.py +105 -0
- telegrinder/bot/polling/__init__.py +4 -0
- telegrinder/bot/polling/abc.py +25 -0
- telegrinder/bot/polling/error_handler.py +93 -0
- telegrinder/bot/polling/polling.py +167 -0
- telegrinder/bot/polling/utils.py +12 -0
- telegrinder/bot/rules/__init__.py +166 -0
- telegrinder/bot/rules/abc.py +150 -0
- telegrinder/bot/rules/button.py +20 -0
- telegrinder/bot/rules/callback_data.py +109 -0
- telegrinder/bot/rules/chat_join.py +28 -0
- telegrinder/bot/rules/chat_member_updated.py +145 -0
- telegrinder/bot/rules/command.py +137 -0
- telegrinder/bot/rules/enum_text.py +29 -0
- telegrinder/bot/rules/func.py +21 -0
- telegrinder/bot/rules/fuzzy.py +21 -0
- telegrinder/bot/rules/inline.py +45 -0
- telegrinder/bot/rules/integer.py +19 -0
- telegrinder/bot/rules/is_from.py +213 -0
- telegrinder/bot/rules/logic.py +22 -0
- telegrinder/bot/rules/magic.py +60 -0
- telegrinder/bot/rules/markup.py +51 -0
- telegrinder/bot/rules/media.py +13 -0
- telegrinder/bot/rules/mention.py +15 -0
- telegrinder/bot/rules/message_entities.py +37 -0
- telegrinder/bot/rules/node.py +43 -0
- telegrinder/bot/rules/payload.py +89 -0
- telegrinder/bot/rules/payment_invoice.py +14 -0
- telegrinder/bot/rules/regex.py +34 -0
- telegrinder/bot/rules/rule_enum.py +71 -0
- telegrinder/bot/rules/start.py +73 -0
- telegrinder/bot/rules/state.py +35 -0
- telegrinder/bot/rules/text.py +27 -0
- telegrinder/bot/rules/update.py +14 -0
- telegrinder/bot/scenario/__init__.py +5 -0
- telegrinder/bot/scenario/abc.py +16 -0
- telegrinder/bot/scenario/checkbox.py +183 -0
- telegrinder/bot/scenario/choice.py +44 -0
- telegrinder/client/__init__.py +11 -0
- telegrinder/client/abc.py +136 -0
- telegrinder/client/form_data.py +34 -0
- telegrinder/client/rnet.py +198 -0
- telegrinder/model.py +133 -0
- telegrinder/model.pyi +57 -0
- telegrinder/modules.py +1081 -0
- telegrinder/msgspec_utils/__init__.py +42 -0
- telegrinder/msgspec_utils/abc.py +16 -0
- telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
- telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
- telegrinder/msgspec_utils/custom_types/enum_meta.py +61 -0
- telegrinder/msgspec_utils/custom_types/literal.py +25 -0
- telegrinder/msgspec_utils/custom_types/option.py +17 -0
- telegrinder/msgspec_utils/decoder.py +388 -0
- telegrinder/msgspec_utils/encoder.py +204 -0
- telegrinder/msgspec_utils/json.py +15 -0
- telegrinder/msgspec_utils/tools.py +80 -0
- telegrinder/node/__init__.py +80 -0
- telegrinder/node/compose.py +193 -0
- telegrinder/node/nodes/__init__.py +96 -0
- telegrinder/node/nodes/attachment.py +169 -0
- telegrinder/node/nodes/callback_query.py +25 -0
- telegrinder/node/nodes/channel.py +97 -0
- telegrinder/node/nodes/command.py +33 -0
- telegrinder/node/nodes/error.py +43 -0
- telegrinder/node/nodes/event.py +70 -0
- telegrinder/node/nodes/file.py +39 -0
- telegrinder/node/nodes/global_node.py +66 -0
- telegrinder/node/nodes/i18n.py +110 -0
- telegrinder/node/nodes/me.py +26 -0
- telegrinder/node/nodes/message_entities.py +15 -0
- telegrinder/node/nodes/payload.py +84 -0
- telegrinder/node/nodes/reply_message.py +14 -0
- telegrinder/node/nodes/source.py +172 -0
- telegrinder/node/nodes/state_mutator.py +71 -0
- telegrinder/node/nodes/text.py +62 -0
- telegrinder/node/scope.py +88 -0
- telegrinder/node/utils.py +38 -0
- telegrinder/py.typed +0 -0
- telegrinder/rules.py +1 -0
- telegrinder/tools/__init__.py +183 -0
- telegrinder/tools/aio.py +147 -0
- telegrinder/tools/final.py +21 -0
- telegrinder/tools/formatting/__init__.py +85 -0
- telegrinder/tools/formatting/deep_links/__init__.py +39 -0
- telegrinder/tools/formatting/deep_links/links.py +468 -0
- telegrinder/tools/formatting/deep_links/parsing.py +88 -0
- telegrinder/tools/formatting/deep_links/validators.py +8 -0
- telegrinder/tools/formatting/html.py +241 -0
- telegrinder/tools/fullname.py +82 -0
- telegrinder/tools/global_context/__init__.py +13 -0
- telegrinder/tools/global_context/abc.py +63 -0
- telegrinder/tools/global_context/builtin_context.py +45 -0
- telegrinder/tools/global_context/global_context.py +614 -0
- telegrinder/tools/input_file_directory.py +30 -0
- telegrinder/tools/keyboard/__init__.py +6 -0
- telegrinder/tools/keyboard/abc.py +84 -0
- telegrinder/tools/keyboard/base.py +108 -0
- telegrinder/tools/keyboard/button.py +181 -0
- telegrinder/tools/keyboard/data.py +31 -0
- telegrinder/tools/keyboard/keyboard.py +160 -0
- telegrinder/tools/keyboard/utils.py +95 -0
- telegrinder/tools/lifespan.py +188 -0
- telegrinder/tools/limited_dict.py +35 -0
- telegrinder/tools/loop_wrapper.py +271 -0
- telegrinder/tools/magic/__init__.py +29 -0
- telegrinder/tools/magic/annotations.py +172 -0
- telegrinder/tools/magic/descriptors.py +57 -0
- telegrinder/tools/magic/function.py +254 -0
- telegrinder/tools/magic/inspect.py +16 -0
- telegrinder/tools/magic/shortcut.py +107 -0
- telegrinder/tools/member_descriptor_proxy.py +95 -0
- telegrinder/tools/parse_mode.py +12 -0
- telegrinder/tools/serialization/__init__.py +5 -0
- telegrinder/tools/serialization/abc.py +34 -0
- telegrinder/tools/serialization/json_ser.py +60 -0
- telegrinder/tools/serialization/msgpack_ser.py +197 -0
- telegrinder/tools/serialization/utils.py +18 -0
- telegrinder/tools/singleton/__init__.py +4 -0
- telegrinder/tools/singleton/abc.py +14 -0
- telegrinder/tools/singleton/singleton.py +18 -0
- telegrinder/tools/state_mutator/__init__.py +4 -0
- telegrinder/tools/state_mutator/mutation.py +85 -0
- telegrinder/tools/state_storage/__init__.py +4 -0
- telegrinder/tools/state_storage/abc.py +38 -0
- telegrinder/tools/state_storage/memory.py +27 -0
- telegrinder/tools/strings.py +22 -0
- telegrinder/types/__init__.py +323 -0
- telegrinder/types/enums.py +754 -0
- telegrinder/types/input_file.py +51 -0
- telegrinder/types/methods.py +6143 -0
- telegrinder/types/methods_utils.py +66 -0
- telegrinder/types/objects.py +8184 -0
- telegrinder/types/webapp.py +129 -0
- telegrinder/verification_utils.py +35 -0
- telegrinder-1.0.0rc1.dist-info/METADATA +166 -0
- telegrinder-1.0.0rc1.dist-info/RECORD +215 -0
- telegrinder-1.0.0rc1.dist-info/WHEEL +4 -0
- 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")
|