telegrinder 0.1.dev20__py3-none-any.whl → 0.1.dev159__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of telegrinder might be problematic. Click here for more details.
- telegrinder/__init__.py +129 -22
- telegrinder/api/__init__.py +11 -2
- telegrinder/api/abc.py +25 -9
- telegrinder/api/api.py +47 -28
- telegrinder/api/error.py +14 -4
- telegrinder/api/response.py +11 -7
- telegrinder/bot/__init__.py +68 -7
- telegrinder/bot/bot.py +30 -24
- telegrinder/bot/cute_types/__init__.py +11 -1
- telegrinder/bot/cute_types/base.py +138 -0
- telegrinder/bot/cute_types/callback_query.py +458 -15
- telegrinder/bot/cute_types/inline_query.py +30 -24
- telegrinder/bot/cute_types/message.py +2982 -78
- telegrinder/bot/cute_types/update.py +30 -0
- telegrinder/bot/cute_types/utils.py +794 -0
- telegrinder/bot/dispatch/__init__.py +56 -3
- telegrinder/bot/dispatch/abc.py +9 -7
- telegrinder/bot/dispatch/composition.py +74 -0
- telegrinder/bot/dispatch/context.py +71 -0
- telegrinder/bot/dispatch/dispatch.py +86 -49
- telegrinder/bot/dispatch/handler/__init__.py +3 -0
- telegrinder/bot/dispatch/handler/abc.py +11 -5
- telegrinder/bot/dispatch/handler/func.py +41 -32
- telegrinder/bot/dispatch/handler/message_reply.py +46 -0
- telegrinder/bot/dispatch/middleware/__init__.py +2 -0
- telegrinder/bot/dispatch/middleware/abc.py +10 -4
- telegrinder/bot/dispatch/process.py +53 -49
- telegrinder/bot/dispatch/return_manager/__init__.py +19 -0
- telegrinder/bot/dispatch/return_manager/abc.py +95 -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 +25 -0
- telegrinder/bot/dispatch/view/__init__.py +14 -2
- telegrinder/bot/dispatch/view/abc.py +128 -2
- telegrinder/bot/dispatch/view/box.py +38 -0
- telegrinder/bot/dispatch/view/callback_query.py +13 -39
- telegrinder/bot/dispatch/view/inline_query.py +11 -39
- telegrinder/bot/dispatch/view/message.py +11 -47
- telegrinder/bot/dispatch/waiter_machine/__init__.py +9 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +116 -0
- telegrinder/bot/dispatch/waiter_machine/middleware.py +76 -0
- telegrinder/bot/dispatch/waiter_machine/short_state.py +37 -0
- telegrinder/bot/polling/__init__.py +2 -0
- telegrinder/bot/polling/abc.py +11 -4
- telegrinder/bot/polling/polling.py +89 -40
- telegrinder/bot/rules/__init__.py +91 -5
- telegrinder/bot/rules/abc.py +81 -63
- telegrinder/bot/rules/adapter/__init__.py +11 -0
- telegrinder/bot/rules/adapter/abc.py +21 -0
- telegrinder/bot/rules/adapter/errors.py +5 -0
- telegrinder/bot/rules/adapter/event.py +49 -0
- telegrinder/bot/rules/adapter/raw_update.py +24 -0
- telegrinder/bot/rules/callback_data.py +159 -38
- telegrinder/bot/rules/command.py +116 -0
- telegrinder/bot/rules/enum_text.py +28 -0
- telegrinder/bot/rules/func.py +17 -17
- telegrinder/bot/rules/fuzzy.py +13 -10
- telegrinder/bot/rules/inline.py +61 -0
- telegrinder/bot/rules/integer.py +12 -7
- telegrinder/bot/rules/is_from.py +148 -7
- telegrinder/bot/rules/markup.py +21 -18
- telegrinder/bot/rules/mention.py +17 -0
- telegrinder/bot/rules/message_entities.py +33 -0
- telegrinder/bot/rules/regex.py +27 -19
- telegrinder/bot/rules/rule_enum.py +74 -0
- telegrinder/bot/rules/start.py +25 -13
- telegrinder/bot/rules/text.py +23 -14
- telegrinder/bot/scenario/__init__.py +2 -0
- telegrinder/bot/scenario/abc.py +12 -5
- telegrinder/bot/scenario/checkbox.py +48 -30
- telegrinder/bot/scenario/choice.py +16 -10
- telegrinder/client/__init__.py +3 -1
- telegrinder/client/abc.py +26 -16
- telegrinder/client/aiohttp.py +54 -32
- telegrinder/model.py +119 -40
- telegrinder/modules.py +189 -21
- telegrinder/msgspec_json.py +14 -0
- telegrinder/msgspec_utils.py +227 -0
- telegrinder/node/__init__.py +31 -0
- telegrinder/node/attachment.py +71 -0
- telegrinder/node/base.py +93 -0
- telegrinder/node/composer.py +71 -0
- telegrinder/node/container.py +22 -0
- telegrinder/node/message.py +18 -0
- telegrinder/node/rule.py +56 -0
- telegrinder/node/source.py +31 -0
- telegrinder/node/text.py +13 -0
- telegrinder/node/tools/__init__.py +3 -0
- telegrinder/node/tools/generator.py +40 -0
- telegrinder/node/update.py +12 -0
- telegrinder/rules.py +1 -1
- telegrinder/tools/__init__.py +138 -4
- telegrinder/tools/buttons.py +89 -51
- telegrinder/tools/error_handler/__init__.py +8 -0
- telegrinder/tools/error_handler/abc.py +30 -0
- telegrinder/tools/error_handler/error_handler.py +156 -0
- telegrinder/tools/formatting/__init__.py +81 -3
- telegrinder/tools/formatting/html.py +283 -37
- telegrinder/tools/formatting/links.py +32 -0
- telegrinder/tools/formatting/spec_html_formats.py +121 -0
- telegrinder/tools/global_context/__init__.py +12 -0
- telegrinder/tools/global_context/abc.py +66 -0
- telegrinder/tools/global_context/global_context.py +451 -0
- telegrinder/tools/global_context/telegrinder_ctx.py +25 -0
- telegrinder/tools/i18n/__init__.py +12 -0
- telegrinder/tools/i18n/base.py +31 -0
- telegrinder/tools/i18n/middleware/__init__.py +3 -0
- telegrinder/tools/i18n/middleware/base.py +26 -0
- telegrinder/tools/i18n/simple.py +48 -0
- telegrinder/tools/kb_set/__init__.py +2 -0
- telegrinder/tools/kb_set/base.py +3 -0
- telegrinder/tools/kb_set/yaml.py +28 -17
- telegrinder/tools/keyboard.py +84 -62
- telegrinder/tools/loop_wrapper/__init__.py +4 -0
- telegrinder/tools/loop_wrapper/abc.py +18 -0
- telegrinder/tools/loop_wrapper/loop_wrapper.py +132 -0
- telegrinder/tools/magic.py +48 -23
- telegrinder/tools/parse_mode.py +1 -2
- telegrinder/types/__init__.py +1 -0
- telegrinder/types/enums.py +653 -0
- telegrinder/types/methods.py +4107 -1279
- telegrinder/types/objects.py +4771 -1745
- {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev159.dist-info}/LICENSE +2 -1
- telegrinder-0.1.dev159.dist-info/METADATA +109 -0
- telegrinder-0.1.dev159.dist-info/RECORD +126 -0
- {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev159.dist-info}/WHEEL +1 -1
- telegrinder/bot/dispatch/waiter.py +0 -38
- telegrinder/result.py +0 -38
- telegrinder/tools/formatting/abc.py +0 -52
- telegrinder/tools/formatting/markdown.py +0 -57
- telegrinder-0.1.dev20.dist-info/METADATA +0 -22
- telegrinder-0.1.dev20.dist-info/RECORD +0 -71
telegrinder/bot/scenario/abc.py
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
1
|
import typing
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
from telegrinder.bot.cute_types.base import BaseCute
|
|
3
5
|
|
|
4
6
|
if typing.TYPE_CHECKING:
|
|
5
|
-
from telegrinder.
|
|
6
|
-
from telegrinder.
|
|
7
|
+
from telegrinder.api import ABCAPI
|
|
8
|
+
from telegrinder.bot.dispatch.view.abc import ABCStateView
|
|
7
9
|
|
|
10
|
+
EventT = typing.TypeVar("EventT", bound=BaseCute)
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
|
|
13
|
+
class ABCScenario(ABC, typing.Generic[EventT]):
|
|
10
14
|
@abstractmethod
|
|
11
|
-
def wait(self, api: "
|
|
15
|
+
def wait(self, api: "ABCAPI", view: "ABCStateView[EventT]") -> typing.Any:
|
|
12
16
|
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ("ABCScenario",)
|
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
from dataclasses import dataclass
|
|
3
|
-
from telegrinder.tools import InlineKeyboard, InlineButton
|
|
4
|
-
from telegrinder.types.objects import InlineKeyboardMarkup
|
|
5
|
-
from telegrinder.bot.cute_types import CallbackQueryCute
|
|
6
|
-
import typing
|
|
1
|
+
import dataclasses
|
|
7
2
|
import random
|
|
8
3
|
import string
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
from telegrinder.bot.cute_types import CallbackQueryCute
|
|
7
|
+
from telegrinder.bot.dispatch.waiter_machine import WaiterMachine
|
|
8
|
+
from telegrinder.tools import InlineButton, InlineKeyboard
|
|
9
|
+
from telegrinder.tools.parse_mode import ParseMode
|
|
10
|
+
from telegrinder.types.objects import InlineKeyboardMarkup
|
|
11
|
+
|
|
12
|
+
from .abc import ABCScenario
|
|
9
13
|
|
|
10
14
|
if typing.TYPE_CHECKING:
|
|
11
|
-
from telegrinder.bot.dispatch import Dispatch
|
|
12
15
|
from telegrinder.api import API
|
|
16
|
+
from telegrinder.bot.dispatch.view.abc import BaseStateView
|
|
13
17
|
|
|
14
18
|
|
|
15
|
-
@dataclass
|
|
19
|
+
@dataclasses.dataclass
|
|
16
20
|
class Choice:
|
|
17
21
|
name: str
|
|
18
22
|
is_picked: bool
|
|
19
|
-
|
|
20
23
|
default_text: str
|
|
21
24
|
picked_text: str
|
|
22
25
|
code: str
|
|
@@ -26,13 +29,14 @@ def random_code(length: int) -> str:
|
|
|
26
29
|
return "".join(random.choices(string.ascii_letters + string.digits, k=length))
|
|
27
30
|
|
|
28
31
|
|
|
29
|
-
class Checkbox(ABCScenario):
|
|
30
|
-
INVALID_CODE: str = "Invalid code"
|
|
31
|
-
CALLBACK_ANSWER: str = "Done"
|
|
32
|
-
PARSE_MODE: str =
|
|
32
|
+
class Checkbox(ABCScenario[CallbackQueryCute]):
|
|
33
|
+
INVALID_CODE: typing.ClassVar[str] = "Invalid code"
|
|
34
|
+
CALLBACK_ANSWER: typing.ClassVar[str] = "Done"
|
|
35
|
+
PARSE_MODE: typing.ClassVar[str] = ParseMode.MARKDOWNV2
|
|
33
36
|
|
|
34
37
|
def __init__(
|
|
35
38
|
self,
|
|
39
|
+
waiter_machine: WaiterMachine,
|
|
36
40
|
chat_id: int,
|
|
37
41
|
msg: str,
|
|
38
42
|
ready_text: str = "Ready",
|
|
@@ -40,13 +44,14 @@ class Checkbox(ABCScenario):
|
|
|
40
44
|
):
|
|
41
45
|
self.chat_id = chat_id
|
|
42
46
|
self.msg = msg
|
|
43
|
-
self.choices:
|
|
47
|
+
self.choices: list[Choice] = []
|
|
44
48
|
self.ready = ready_text
|
|
45
49
|
self.max_in_row = max_in_row
|
|
46
50
|
self.random_code = random_code(16)
|
|
51
|
+
self.waiter_machine = waiter_machine
|
|
47
52
|
|
|
48
53
|
def get_markup(self) -> InlineKeyboardMarkup:
|
|
49
|
-
kb = InlineKeyboard(
|
|
54
|
+
kb = InlineKeyboard()
|
|
50
55
|
choices = self.choices.copy()
|
|
51
56
|
while choices:
|
|
52
57
|
while len(kb.keyboard[-1]) < self.max_in_row and choices:
|
|
@@ -60,20 +65,24 @@ class Checkbox(ABCScenario):
|
|
|
60
65
|
)
|
|
61
66
|
)
|
|
62
67
|
kb.row()
|
|
68
|
+
|
|
63
69
|
kb.add(InlineButton(self.ready, callback_data=self.random_code + "/ready"))
|
|
64
70
|
return kb.get_markup()
|
|
65
71
|
|
|
66
72
|
def add_option(
|
|
67
|
-
self,
|
|
68
|
-
|
|
73
|
+
self,
|
|
74
|
+
name: str,
|
|
75
|
+
default_text: str,
|
|
76
|
+
picked_text: str,
|
|
77
|
+
is_picked: bool = False,
|
|
78
|
+
) -> typing.Self:
|
|
69
79
|
self.choices.append(
|
|
70
|
-
Choice(name, is_picked, default_text, picked_text, random_code(16))
|
|
80
|
+
Choice(name, is_picked, default_text, picked_text, random_code(16)),
|
|
71
81
|
)
|
|
72
82
|
return self
|
|
73
83
|
|
|
74
84
|
async def handle(self, cb: CallbackQueryCute) -> bool:
|
|
75
|
-
code = cb.data.replace(self.random_code + "/", "", 1)
|
|
76
|
-
|
|
85
|
+
code = cb.data.unwrap().replace(self.random_code + "/", "", 1)
|
|
77
86
|
if code == "ready":
|
|
78
87
|
return False
|
|
79
88
|
|
|
@@ -81,9 +90,7 @@ class Checkbox(ABCScenario):
|
|
|
81
90
|
if choice.code == code:
|
|
82
91
|
# Toggle choice
|
|
83
92
|
self.choices[i].is_picked = not self.choices[i].is_picked
|
|
84
|
-
await cb.
|
|
85
|
-
cb.message.chat.id,
|
|
86
|
-
cb.message.message_id,
|
|
93
|
+
await cb.edit_text(
|
|
87
94
|
text=self.msg,
|
|
88
95
|
parse_mode=self.PARSE_MODE,
|
|
89
96
|
reply_markup=self.get_markup(),
|
|
@@ -93,21 +100,32 @@ class Checkbox(ABCScenario):
|
|
|
93
100
|
return True
|
|
94
101
|
|
|
95
102
|
async def wait(
|
|
96
|
-
self,
|
|
97
|
-
|
|
103
|
+
self,
|
|
104
|
+
api: "API",
|
|
105
|
+
view: "BaseStateView[CallbackQueryCute]",
|
|
106
|
+
) -> tuple[dict[str, bool], int]:
|
|
98
107
|
assert len(self.choices) > 1
|
|
99
108
|
message = (
|
|
100
109
|
await api.send_message(
|
|
101
|
-
self.chat_id,
|
|
110
|
+
self.chat_id,
|
|
111
|
+
text=self.msg,
|
|
112
|
+
parse_mode=self.PARSE_MODE,
|
|
113
|
+
reply_markup=self.get_markup(),
|
|
102
114
|
)
|
|
103
115
|
).unwrap()
|
|
116
|
+
|
|
104
117
|
while True:
|
|
105
118
|
q: CallbackQueryCute
|
|
106
|
-
q, _ = await
|
|
119
|
+
q, _ = await self.waiter_machine.wait(view, (api, message.message_id))
|
|
107
120
|
should_continue = await self.handle(q)
|
|
108
121
|
await q.answer(self.CALLBACK_ANSWER)
|
|
109
122
|
if not should_continue:
|
|
110
123
|
break
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
{choice.name: choice.is_picked for choice in self.choices},
|
|
127
|
+
message.message_id,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
__all__ = ("Checkbox", "Choice", "random_code")
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
from .checkbox import Checkbox
|
|
2
|
-
from telegrinder.bot.cute_types import CallbackQueryCute
|
|
3
1
|
import typing
|
|
4
2
|
|
|
3
|
+
from telegrinder.bot.cute_types import CallbackQueryCute
|
|
4
|
+
|
|
5
|
+
from .checkbox import Checkbox
|
|
6
|
+
|
|
5
7
|
if typing.TYPE_CHECKING:
|
|
6
8
|
from telegrinder.api import API
|
|
7
|
-
from telegrinder.bot.dispatch import
|
|
9
|
+
from telegrinder.bot.dispatch.view.abc import BaseStateView
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class SingleChoice(Checkbox):
|
|
11
13
|
async def handle(self, cb: CallbackQueryCute) -> bool:
|
|
12
|
-
code = cb.data.replace(self.random_code + "/", "", 1)
|
|
13
|
-
|
|
14
|
+
code = cb.data.unwrap().replace(self.random_code + "/", "", 1)
|
|
14
15
|
if code == "ready":
|
|
15
16
|
return False
|
|
16
17
|
|
|
@@ -21,8 +22,8 @@ class SingleChoice(Checkbox):
|
|
|
21
22
|
if choice.code == code:
|
|
22
23
|
self.choices[i].is_picked = True
|
|
23
24
|
await cb.ctx_api.edit_message_text(
|
|
24
|
-
cb.message.chat.id,
|
|
25
|
-
cb.message.message_id,
|
|
25
|
+
chat_id=cb.message.unwrap().v.chat.id,
|
|
26
|
+
message_id=cb.message.unwrap().v.message_id,
|
|
26
27
|
text=self.msg,
|
|
27
28
|
parse_mode=self.PARSE_MODE,
|
|
28
29
|
reply_markup=self.get_markup(),
|
|
@@ -31,9 +32,14 @@ class SingleChoice(Checkbox):
|
|
|
31
32
|
return True
|
|
32
33
|
|
|
33
34
|
async def wait(
|
|
34
|
-
self,
|
|
35
|
-
|
|
35
|
+
self,
|
|
36
|
+
api: "API",
|
|
37
|
+
cb_view: "BaseStateView[CallbackQueryCute]",
|
|
38
|
+
) -> tuple[str, int]:
|
|
36
39
|
if len([choice for choice in self.choices if choice.is_picked]) != 1:
|
|
37
40
|
raise ValueError("Exactly one choice must be picked")
|
|
38
|
-
choices, m_id = await super().wait(api,
|
|
41
|
+
choices, m_id = await super().wait(api, cb_view)
|
|
39
42
|
return list(choices.keys())[list(choices.values()).index(True)], m_id
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = ("SingleChoice",)
|
telegrinder/client/__init__.py
CHANGED
telegrinder/client/abc.py
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
1
|
import typing
|
|
3
|
-
|
|
4
|
-
ClientData = typing.Any
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
5
3
|
|
|
6
4
|
|
|
7
5
|
class ABCClient(ABC):
|
|
8
6
|
@abstractmethod
|
|
9
|
-
def __init__(self, *args, **kwargs):
|
|
7
|
+
def __init__(self, *args: typing.Any, **kwargs: typing.Any):
|
|
10
8
|
pass
|
|
11
9
|
|
|
12
10
|
@abstractmethod
|
|
@@ -14,8 +12,8 @@ class ABCClient(ABC):
|
|
|
14
12
|
self,
|
|
15
13
|
url: str,
|
|
16
14
|
method: str = "GET",
|
|
17
|
-
data: typing.
|
|
18
|
-
**kwargs
|
|
15
|
+
data: dict[str, typing.Any] | None = None,
|
|
16
|
+
**kwargs: typing.Any,
|
|
19
17
|
) -> str:
|
|
20
18
|
pass
|
|
21
19
|
|
|
@@ -24,9 +22,9 @@ class ABCClient(ABC):
|
|
|
24
22
|
self,
|
|
25
23
|
url: str,
|
|
26
24
|
method: str = "GET",
|
|
27
|
-
data: typing.
|
|
28
|
-
**kwargs
|
|
29
|
-
) -> dict:
|
|
25
|
+
data: dict[str, typing.Any] | None = None,
|
|
26
|
+
**kwargs: typing.Any,
|
|
27
|
+
) -> dict[str, typing.Any]:
|
|
30
28
|
pass
|
|
31
29
|
|
|
32
30
|
@abstractmethod
|
|
@@ -34,8 +32,8 @@ class ABCClient(ABC):
|
|
|
34
32
|
self,
|
|
35
33
|
url: str,
|
|
36
34
|
method: str = "GET",
|
|
37
|
-
data: typing.
|
|
38
|
-
**kwargs
|
|
35
|
+
data: dict[str, typing.Any] | None = None,
|
|
36
|
+
**kwargs: typing.Any,
|
|
39
37
|
) -> bytes:
|
|
40
38
|
pass
|
|
41
39
|
|
|
@@ -44,8 +42,8 @@ class ABCClient(ABC):
|
|
|
44
42
|
self,
|
|
45
43
|
url: str,
|
|
46
44
|
method: str = "GET",
|
|
47
|
-
data: typing.
|
|
48
|
-
**kwargs
|
|
45
|
+
data: dict[str, typing.Any] | None = None,
|
|
46
|
+
**kwargs: typing.Any,
|
|
49
47
|
) -> bytes:
|
|
50
48
|
pass
|
|
51
49
|
|
|
@@ -55,11 +53,23 @@ class ABCClient(ABC):
|
|
|
55
53
|
|
|
56
54
|
@classmethod
|
|
57
55
|
@abstractmethod
|
|
58
|
-
def get_form(
|
|
56
|
+
def get_form(
|
|
57
|
+
cls,
|
|
58
|
+
data: dict[str, typing.Any],
|
|
59
|
+
files: dict[str, tuple[str, bytes]] | None = None,
|
|
60
|
+
) -> typing.Any:
|
|
59
61
|
pass
|
|
60
62
|
|
|
61
|
-
async def __aenter__(self) ->
|
|
63
|
+
async def __aenter__(self) -> typing.Self:
|
|
62
64
|
return self
|
|
63
65
|
|
|
64
|
-
async def __aexit__(
|
|
66
|
+
async def __aexit__(
|
|
67
|
+
self,
|
|
68
|
+
exc_type: type[BaseException],
|
|
69
|
+
exc_val: typing.Any,
|
|
70
|
+
exc_tb: typing.Any,
|
|
71
|
+
) -> None:
|
|
65
72
|
await self.close()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
__all__ = ("ABCClient",)
|
telegrinder/client/aiohttp.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import secrets
|
|
1
2
|
import ssl
|
|
2
3
|
import typing
|
|
3
4
|
|
|
4
5
|
import aiohttp
|
|
6
|
+
import certifi
|
|
7
|
+
from aiohttp import ClientSession, TCPConnector
|
|
5
8
|
|
|
6
9
|
from telegrinder.client.abc import ABCClient
|
|
7
|
-
from
|
|
8
|
-
from telegrinder.modules import json, JSONModule
|
|
9
|
-
import certifi
|
|
10
|
+
from telegrinder.modules import JSONModule, json
|
|
10
11
|
|
|
11
12
|
if typing.TYPE_CHECKING:
|
|
12
13
|
from aiohttp import ClientResponse
|
|
@@ -15,22 +16,30 @@ if typing.TYPE_CHECKING:
|
|
|
15
16
|
class AiohttpClient(ABCClient):
|
|
16
17
|
def __init__(
|
|
17
18
|
self,
|
|
18
|
-
session:
|
|
19
|
-
json_processing_module:
|
|
20
|
-
timeout:
|
|
21
|
-
**session_params
|
|
22
|
-
):
|
|
19
|
+
session: ClientSession | None = None,
|
|
20
|
+
json_processing_module: JSONModule | None = None,
|
|
21
|
+
timeout: aiohttp.ClientTimeout | None = None,
|
|
22
|
+
**session_params: typing.Any,
|
|
23
|
+
) -> None:
|
|
23
24
|
self.session = session
|
|
24
25
|
self.json_processing_module = json_processing_module or json
|
|
25
26
|
self.session_params = session_params
|
|
26
27
|
self.timeout = timeout or aiohttp.ClientTimeout(total=0)
|
|
27
|
-
|
|
28
|
+
|
|
29
|
+
def __repr__(self) -> str:
|
|
30
|
+
return "<{}: session={!r}, timeout={}, closed={}>".format(
|
|
31
|
+
self.__class__.__name__,
|
|
32
|
+
self.session,
|
|
33
|
+
self.timeout,
|
|
34
|
+
False if self.session is None else self.session.closed,
|
|
35
|
+
)
|
|
36
|
+
|
|
28
37
|
async def request_raw(
|
|
29
38
|
self,
|
|
30
39
|
url: str,
|
|
31
40
|
method: str = "GET",
|
|
32
|
-
data: typing.
|
|
33
|
-
**kwargs
|
|
41
|
+
data: dict[str, typing.Any] | None = None,
|
|
42
|
+
**kwargs: typing.Any,
|
|
34
43
|
) -> "ClientResponse":
|
|
35
44
|
if not self.session:
|
|
36
45
|
self.session = ClientSession(
|
|
@@ -41,7 +50,11 @@ class AiohttpClient(ABCClient):
|
|
|
41
50
|
**self.session_params,
|
|
42
51
|
)
|
|
43
52
|
async with self.session.request(
|
|
44
|
-
url=url,
|
|
53
|
+
url=url,
|
|
54
|
+
method=method,
|
|
55
|
+
data=data,
|
|
56
|
+
timeout=self.timeout,
|
|
57
|
+
**kwargs,
|
|
45
58
|
) as response:
|
|
46
59
|
await response.read()
|
|
47
60
|
return response
|
|
@@ -50,32 +63,34 @@ class AiohttpClient(ABCClient):
|
|
|
50
63
|
self,
|
|
51
64
|
url: str,
|
|
52
65
|
method: str = "GET",
|
|
53
|
-
data: typing.
|
|
54
|
-
**kwargs
|
|
55
|
-
) -> dict:
|
|
66
|
+
data: dict[str, typing.Any] | None = None,
|
|
67
|
+
**kwargs: typing.Any,
|
|
68
|
+
) -> dict[str, typing.Any]:
|
|
56
69
|
response = await self.request_raw(url, method, data, **kwargs)
|
|
57
70
|
return await response.json(
|
|
58
|
-
encoding="utf-8",
|
|
71
|
+
encoding="utf-8",
|
|
72
|
+
loads=self.json_processing_module.loads,
|
|
73
|
+
content_type=None,
|
|
59
74
|
)
|
|
60
75
|
|
|
61
76
|
async def request_text(
|
|
62
77
|
self,
|
|
63
78
|
url: str,
|
|
64
79
|
method: str = "GET",
|
|
65
|
-
data:
|
|
66
|
-
**kwargs
|
|
80
|
+
data: dict[str, typing.Any] | aiohttp.FormData | None = None,
|
|
81
|
+
**kwargs: typing.Any,
|
|
67
82
|
) -> str:
|
|
68
|
-
response = await self.request_raw(url, method, data, **kwargs)
|
|
83
|
+
response = await self.request_raw(url, method, data, **kwargs) # type: ignore
|
|
69
84
|
return await response.text(encoding="utf-8")
|
|
70
85
|
|
|
71
86
|
async def request_bytes(
|
|
72
87
|
self,
|
|
73
88
|
url: str,
|
|
74
89
|
method: str = "GET",
|
|
75
|
-
data:
|
|
76
|
-
**kwargs
|
|
90
|
+
data: dict[str, typing.Any] | aiohttp.FormData | None = None,
|
|
91
|
+
**kwargs: typing.Any,
|
|
77
92
|
) -> bytes:
|
|
78
|
-
response = await self.request_raw(url, method, data, **kwargs)
|
|
93
|
+
response = await self.request_raw(url, method, data, **kwargs) # type: ignore
|
|
79
94
|
if response._body is None:
|
|
80
95
|
await response.read()
|
|
81
96
|
return response._body
|
|
@@ -84,8 +99,8 @@ class AiohttpClient(ABCClient):
|
|
|
84
99
|
self,
|
|
85
100
|
url: str,
|
|
86
101
|
method: str = "GET",
|
|
87
|
-
data: typing.
|
|
88
|
-
**kwargs
|
|
102
|
+
data: dict[str, typing.Any] | None = None,
|
|
103
|
+
**kwargs: typing.Any,
|
|
89
104
|
) -> bytes:
|
|
90
105
|
response = await self.request_raw(url, method, data, **kwargs)
|
|
91
106
|
return response._body
|
|
@@ -95,19 +110,26 @@ class AiohttpClient(ABCClient):
|
|
|
95
110
|
await self.session.close()
|
|
96
111
|
|
|
97
112
|
@classmethod
|
|
98
|
-
def get_form(
|
|
113
|
+
def get_form(
|
|
114
|
+
cls,
|
|
115
|
+
data: dict[str, typing.Any],
|
|
116
|
+
files: dict[str, tuple[str, bytes]] | None = None,
|
|
117
|
+
) -> aiohttp.formdata.FormData:
|
|
118
|
+
files = files or {}
|
|
99
119
|
form = aiohttp.formdata.FormData(quote_fields=False)
|
|
100
120
|
for k, v in data.items():
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
form.add_field(k, v, **params)
|
|
121
|
+
form.add_field(k, str(v))
|
|
122
|
+
|
|
123
|
+
for n, f in files.items():
|
|
124
|
+
form.add_field(n, f[1], filename=f[0])
|
|
125
|
+
|
|
107
126
|
return form
|
|
108
127
|
|
|
109
|
-
def __del__(self):
|
|
128
|
+
def __del__(self) -> None:
|
|
110
129
|
if self.session and not self.session.closed:
|
|
111
130
|
if self.session._connector is not None and self.session._connector_owner:
|
|
112
131
|
self.session._connector.close()
|
|
113
132
|
self.session._connector = None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
__all__ = ("AiohttpClient",)
|
telegrinder/model.py
CHANGED
|
@@ -1,65 +1,144 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from msgspec import Raw
|
|
1
|
+
import dataclasses
|
|
2
|
+
import enum
|
|
3
|
+
import secrets
|
|
5
4
|
import typing
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from types import NoneType
|
|
7
|
+
|
|
8
|
+
import msgspec
|
|
9
|
+
from fntypes.co import Nothing, Result, Some
|
|
10
|
+
|
|
11
|
+
from .msgspec_utils import decoder, encoder, get_origin
|
|
12
|
+
|
|
13
|
+
T = typing.TypeVar("T")
|
|
6
14
|
|
|
7
15
|
if typing.TYPE_CHECKING:
|
|
8
16
|
from telegrinder.api.error import APIError
|
|
17
|
+
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
|
|
19
|
+
MODEL_CONFIG: typing.Final[dict[str, typing.Any]] = {
|
|
20
|
+
"omit_defaults": True,
|
|
21
|
+
"dict": True,
|
|
22
|
+
"rename": {"from_": "from"},
|
|
23
|
+
}
|
|
12
24
|
|
|
13
25
|
|
|
26
|
+
@typing.overload
|
|
14
27
|
def full_result(
|
|
15
|
-
result: Result[msgspec.Raw, "APIError"], full_t:
|
|
28
|
+
result: Result[msgspec.Raw, "APIError"], full_t: type[T]
|
|
16
29
|
) -> Result[T, "APIError"]:
|
|
17
|
-
|
|
18
|
-
return result
|
|
19
|
-
return Result(True, value=msgspec.json.decode(result.value, type=full_t))
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def convert(d: typing.Any) -> typing.Any:
|
|
23
|
-
if isinstance(d, Model):
|
|
24
|
-
return msgspec.json.encode(d).decode()
|
|
25
|
-
elif isinstance(d, dict):
|
|
26
|
-
return {k: convert(v) for k, v in d.items() if v is not None}
|
|
27
|
-
elif isinstance(d, list):
|
|
28
|
-
return json.dumps(d)
|
|
29
|
-
return d
|
|
30
|
+
...
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
@typing.overload
|
|
34
|
+
def full_result(
|
|
35
|
+
result: Result[msgspec.Raw, "APIError"],
|
|
36
|
+
full_t: tuple[type[T], ...],
|
|
37
|
+
) -> Result[T, "APIError"]:
|
|
38
|
+
...
|
|
34
39
|
|
|
35
|
-
class Model(msgspec.Struct, **model_config):
|
|
36
|
-
_dict_cached: typing.Optional[dict] = None
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
def full_result(
|
|
42
|
+
result: Result[msgspec.Raw, "APIError"],
|
|
43
|
+
full_t: type[T] | tuple[type[T], ...],
|
|
44
|
+
) -> Result[T, "APIError"]:
|
|
45
|
+
return result.map(lambda v: decoder.decode(v, type=full_t)) # type: ignore
|
|
43
46
|
|
|
44
47
|
|
|
45
|
-
def get_params(params: dict) -> dict:
|
|
48
|
+
def get_params(params: dict[str, typing.Any]) -> dict[str, typing.Any]:
|
|
46
49
|
return {
|
|
47
|
-
k: v
|
|
50
|
+
k: v.unwrap() if v and isinstance(v, Some) else v
|
|
51
|
+
for k, v in (
|
|
52
|
+
*params.pop("other", {}).items(),
|
|
48
53
|
*params.items(),
|
|
49
|
-
*params.pop("other").items()
|
|
50
54
|
)
|
|
51
|
-
if k != "self"
|
|
52
|
-
and v is not None
|
|
55
|
+
if k != "self" and type(v) not in (NoneType, Nothing)
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
|
|
59
|
+
class Model(msgspec.Struct, **MODEL_CONFIG):
|
|
60
|
+
def to_dict(
|
|
61
|
+
self,
|
|
62
|
+
*,
|
|
63
|
+
exclude_fields: set[str] | None = None,
|
|
64
|
+
) -> dict[str, typing.Any]:
|
|
65
|
+
exclude_fields = exclude_fields or set()
|
|
66
|
+
if "model_as_dict" not in self.__dict__:
|
|
67
|
+
self.__dict__["model_as_dict"] = msgspec.structs.asdict(self)
|
|
68
|
+
return {
|
|
69
|
+
key: value
|
|
70
|
+
for key, value in self.__dict__["model_as_dict"].items()
|
|
71
|
+
if key not in exclude_fields
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclasses.dataclass(kw_only=True)
|
|
76
|
+
class DataConverter:
|
|
77
|
+
files: dict[str, tuple[str, bytes]] = dataclasses.field(default_factory=lambda: {})
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def converters(self) -> dict[
|
|
81
|
+
type[typing.Any], typing.Callable[[typing.Self, typing.Any, bool], typing.Any]
|
|
82
|
+
]:
|
|
83
|
+
return {
|
|
84
|
+
get_origin(value.__annotations__["d"]): value
|
|
85
|
+
for key, value in vars(self.__class__).items()
|
|
86
|
+
if key.startswith("convert_")
|
|
87
|
+
and callable(value)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
def get_converter(
|
|
91
|
+
self, t: type[typing.Any]
|
|
92
|
+
) -> typing.Callable[[typing.Self, typing.Any, bool], typing.Any] | None:
|
|
93
|
+
for type, converter in self.converters.items():
|
|
94
|
+
if issubclass(t, type):
|
|
95
|
+
return converter
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def convert_model(self, d: Model, serialize: bool = True) -> str | dict[str, typing.Any]:
|
|
99
|
+
converted_dct = self.convert(d.to_dict(), serialize=False)
|
|
100
|
+
return encoder.encode(converted_dct) if serialize is True else converted_dct
|
|
101
|
+
|
|
102
|
+
def convert_dct(self, d: dict[str, typing.Any], serialize: bool = True) -> dict[str, typing.Any]:
|
|
103
|
+
return {
|
|
104
|
+
k: self.convert(v, serialize=serialize)
|
|
105
|
+
for k, v in d.items()
|
|
106
|
+
if type(v) not in (NoneType, Nothing)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def convert_lst(self, d: list[typing.Any], serialize: bool = True) -> str | list[typing.Any]:
|
|
110
|
+
converted_lst = [self.convert(x, serialize=False) for x in d]
|
|
111
|
+
return encoder.encode(converted_lst) if serialize is True else converted_lst
|
|
112
|
+
|
|
113
|
+
def convert_tpl(self, d: tuple[typing.Any, ...], serialize: bool = True) -> str | tuple[typing.Any, ...]:
|
|
114
|
+
if (
|
|
115
|
+
isinstance(d, tuple)
|
|
116
|
+
and len(d) == 2
|
|
117
|
+
and isinstance(d[0], str)
|
|
118
|
+
and isinstance(d[1], bytes)
|
|
119
|
+
):
|
|
120
|
+
attach_name = secrets.token_urlsafe(16)
|
|
121
|
+
self.files[attach_name] = d
|
|
122
|
+
return "attach://{}".format(attach_name)
|
|
123
|
+
return d
|
|
124
|
+
|
|
125
|
+
def convert_enum(self, d: enum.Enum, serialize: bool = True) -> str:
|
|
126
|
+
return d.value
|
|
127
|
+
|
|
128
|
+
def convert_datetime(self, d: datetime, serialize: bool = True) -> int:
|
|
129
|
+
return int(d.timestamp())
|
|
130
|
+
|
|
131
|
+
def convert(self, data: typing.Any, *, serialize: bool = True) -> typing.Any:
|
|
132
|
+
converter = self.get_converter(get_origin(type(data)))
|
|
133
|
+
if converter is not None:
|
|
134
|
+
return converter(self, data, serialize)
|
|
135
|
+
return data
|
|
136
|
+
|
|
137
|
+
|
|
56
138
|
__all__ = (
|
|
57
|
-
"
|
|
58
|
-
"model_config",
|
|
59
|
-
"encoder",
|
|
60
|
-
"full_result",
|
|
61
|
-
"msgspec",
|
|
139
|
+
"DataConverter",
|
|
62
140
|
"Model",
|
|
63
|
-
"
|
|
141
|
+
"full_result",
|
|
64
142
|
"get_params",
|
|
143
|
+
"MODEL_CONFIG",
|
|
65
144
|
)
|