telegrinder 0.3.4.post1__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of telegrinder might be problematic. Click here for more details.
- telegrinder/__init__.py +30 -31
- telegrinder/api/__init__.py +2 -1
- telegrinder/api/api.py +28 -20
- telegrinder/api/error.py +8 -4
- telegrinder/api/response.py +2 -2
- telegrinder/api/token.py +2 -2
- telegrinder/bot/__init__.py +6 -0
- telegrinder/bot/bot.py +38 -31
- telegrinder/bot/cute_types/__init__.py +2 -0
- telegrinder/bot/cute_types/base.py +54 -128
- telegrinder/bot/cute_types/callback_query.py +76 -61
- telegrinder/bot/cute_types/chat_join_request.py +4 -3
- telegrinder/bot/cute_types/chat_member_updated.py +28 -31
- telegrinder/bot/cute_types/inline_query.py +5 -4
- telegrinder/bot/cute_types/message.py +555 -602
- telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
- telegrinder/bot/cute_types/update.py +20 -12
- telegrinder/bot/cute_types/utils.py +3 -36
- telegrinder/bot/dispatch/__init__.py +4 -0
- telegrinder/bot/dispatch/abc.py +8 -9
- telegrinder/bot/dispatch/context.py +5 -7
- telegrinder/bot/dispatch/dispatch.py +85 -33
- telegrinder/bot/dispatch/handler/abc.py +5 -6
- telegrinder/bot/dispatch/handler/audio_reply.py +2 -2
- telegrinder/bot/dispatch/handler/base.py +3 -3
- telegrinder/bot/dispatch/handler/document_reply.py +2 -2
- telegrinder/bot/dispatch/handler/func.py +36 -42
- telegrinder/bot/dispatch/handler/media_group_reply.py +5 -4
- telegrinder/bot/dispatch/handler/message_reply.py +2 -2
- telegrinder/bot/dispatch/handler/photo_reply.py +2 -2
- telegrinder/bot/dispatch/handler/sticker_reply.py +2 -2
- telegrinder/bot/dispatch/handler/video_reply.py +2 -2
- telegrinder/bot/dispatch/middleware/abc.py +83 -8
- telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
- telegrinder/bot/dispatch/process.py +44 -50
- telegrinder/bot/dispatch/return_manager/__init__.py +2 -0
- telegrinder/bot/dispatch/return_manager/abc.py +6 -10
- telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +20 -0
- telegrinder/bot/dispatch/view/__init__.py +2 -0
- telegrinder/bot/dispatch/view/abc.py +10 -6
- telegrinder/bot/dispatch/view/base.py +81 -50
- telegrinder/bot/dispatch/view/box.py +20 -9
- telegrinder/bot/dispatch/view/callback_query.py +3 -4
- telegrinder/bot/dispatch/view/chat_join_request.py +2 -7
- telegrinder/bot/dispatch/view/chat_member.py +3 -5
- telegrinder/bot/dispatch/view/inline_query.py +3 -4
- telegrinder/bot/dispatch/view/message.py +3 -4
- telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
- telegrinder/bot/dispatch/view/raw.py +42 -40
- telegrinder/bot/dispatch/waiter_machine/actions.py +5 -4
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +9 -7
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/state.py +3 -2
- telegrinder/bot/dispatch/waiter_machine/machine.py +113 -34
- telegrinder/bot/dispatch/waiter_machine/middleware.py +15 -10
- telegrinder/bot/dispatch/waiter_machine/short_state.py +7 -18
- telegrinder/bot/polling/polling.py +62 -54
- telegrinder/bot/rules/__init__.py +24 -1
- telegrinder/bot/rules/abc.py +17 -10
- telegrinder/bot/rules/callback_data.py +20 -61
- telegrinder/bot/rules/chat_join.py +6 -4
- telegrinder/bot/rules/command.py +4 -4
- telegrinder/bot/rules/enum_text.py +1 -4
- telegrinder/bot/rules/func.py +5 -3
- telegrinder/bot/rules/fuzzy.py +1 -1
- telegrinder/bot/rules/id.py +24 -0
- telegrinder/bot/rules/inline.py +6 -4
- telegrinder/bot/rules/integer.py +2 -1
- telegrinder/bot/rules/logic.py +18 -0
- telegrinder/bot/rules/markup.py +5 -6
- telegrinder/bot/rules/message.py +2 -4
- telegrinder/bot/rules/message_entities.py +1 -3
- telegrinder/bot/rules/node.py +15 -9
- telegrinder/bot/rules/payload.py +81 -0
- telegrinder/bot/rules/payment_invoice.py +29 -0
- telegrinder/bot/rules/regex.py +5 -6
- telegrinder/bot/rules/state.py +1 -3
- telegrinder/bot/rules/text.py +10 -5
- telegrinder/bot/rules/update.py +0 -0
- telegrinder/bot/scenario/abc.py +2 -4
- telegrinder/bot/scenario/checkbox.py +12 -14
- telegrinder/bot/scenario/choice.py +6 -9
- telegrinder/client/__init__.py +9 -1
- telegrinder/client/abc.py +35 -10
- telegrinder/client/aiohttp.py +28 -24
- telegrinder/client/form_data.py +31 -0
- telegrinder/client/sonic.py +212 -0
- telegrinder/model.py +38 -145
- telegrinder/modules.py +3 -1
- telegrinder/msgspec_utils.py +136 -68
- telegrinder/node/__init__.py +74 -13
- telegrinder/node/attachment.py +92 -16
- telegrinder/node/base.py +196 -68
- telegrinder/node/callback_query.py +17 -16
- telegrinder/node/command.py +3 -2
- telegrinder/node/composer.py +40 -75
- telegrinder/node/container.py +13 -7
- telegrinder/node/either.py +82 -0
- telegrinder/node/event.py +20 -31
- telegrinder/node/file.py +51 -0
- telegrinder/node/me.py +4 -5
- telegrinder/node/payload.py +78 -0
- telegrinder/node/polymorphic.py +27 -8
- telegrinder/node/rule.py +2 -6
- telegrinder/node/scope.py +4 -6
- telegrinder/node/source.py +37 -21
- telegrinder/node/text.py +20 -8
- telegrinder/node/tools/generator.py +7 -11
- telegrinder/py.typed +0 -0
- telegrinder/rules.py +0 -61
- telegrinder/tools/__init__.py +97 -38
- telegrinder/tools/adapter/__init__.py +19 -0
- telegrinder/tools/adapter/abc.py +49 -0
- telegrinder/tools/adapter/dataclass.py +56 -0
- telegrinder/{bot/rules → tools}/adapter/event.py +8 -10
- telegrinder/{bot/rules → tools}/adapter/node.py +8 -10
- telegrinder/{bot/rules → tools}/adapter/raw_event.py +2 -2
- telegrinder/{bot/rules → tools}/adapter/raw_update.py +2 -2
- telegrinder/tools/buttons.py +52 -26
- telegrinder/tools/callback_data_serilization/__init__.py +5 -0
- telegrinder/tools/callback_data_serilization/abc.py +51 -0
- telegrinder/tools/callback_data_serilization/json_ser.py +60 -0
- telegrinder/tools/callback_data_serilization/msgpack_ser.py +172 -0
- telegrinder/tools/error_handler/abc.py +4 -7
- telegrinder/tools/error_handler/error.py +0 -0
- telegrinder/tools/error_handler/error_handler.py +34 -48
- telegrinder/tools/formatting/__init__.py +57 -37
- telegrinder/tools/formatting/deep_links.py +541 -0
- telegrinder/tools/formatting/{html.py → html_formatter.py} +51 -79
- telegrinder/tools/formatting/spec_html_formats.py +14 -60
- telegrinder/tools/functional.py +1 -5
- telegrinder/tools/global_context/global_context.py +26 -51
- telegrinder/tools/global_context/telegrinder_ctx.py +3 -3
- telegrinder/tools/i18n/abc.py +0 -0
- telegrinder/tools/i18n/middleware/abc.py +3 -6
- telegrinder/tools/input_file_directory.py +30 -0
- telegrinder/tools/keyboard.py +9 -9
- telegrinder/tools/lifespan.py +105 -0
- telegrinder/tools/limited_dict.py +5 -10
- telegrinder/tools/loop_wrapper/abc.py +7 -2
- telegrinder/tools/loop_wrapper/loop_wrapper.py +40 -95
- telegrinder/tools/magic.py +184 -34
- telegrinder/tools/state_storage/__init__.py +0 -0
- telegrinder/tools/state_storage/abc.py +5 -9
- telegrinder/tools/state_storage/memory.py +1 -1
- telegrinder/tools/strings.py +13 -0
- telegrinder/types/__init__.py +8 -0
- telegrinder/types/enums.py +31 -21
- telegrinder/types/input_file.py +51 -0
- telegrinder/types/methods.py +531 -109
- telegrinder/types/objects.py +934 -826
- telegrinder/verification_utils.py +0 -2
- {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/LICENSE +2 -2
- telegrinder-0.4.0.dist-info/METADATA +144 -0
- telegrinder-0.4.0.dist-info/RECORD +182 -0
- {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/WHEEL +1 -1
- telegrinder/bot/rules/adapter/__init__.py +0 -17
- telegrinder/bot/rules/adapter/abc.py +0 -31
- telegrinder/node/message.py +0 -14
- telegrinder/node/update.py +0 -15
- telegrinder/tools/formatting/links.py +0 -38
- telegrinder/tools/kb_set/__init__.py +0 -4
- telegrinder/tools/kb_set/base.py +0 -15
- telegrinder/tools/kb_set/yaml.py +0 -63
- telegrinder-0.3.4.post1.dist-info/METADATA +0 -110
- telegrinder-0.3.4.post1.dist-info/RECORD +0 -165
- /telegrinder/{bot/rules → tools}/adapter/errors.py +0 -0
|
@@ -3,20 +3,18 @@ from fntypes.result import Error, Ok, Result
|
|
|
3
3
|
|
|
4
4
|
from telegrinder.api.api import API
|
|
5
5
|
from telegrinder.bot.dispatch.context import Context
|
|
6
|
-
from telegrinder.bot.rules.adapter.abc import ABCAdapter, Event
|
|
7
|
-
from telegrinder.bot.rules.adapter.errors import AdapterError
|
|
8
6
|
from telegrinder.msgspec_utils import repr_type
|
|
9
7
|
from telegrinder.node.composer import NodeSession, compose_nodes
|
|
8
|
+
from telegrinder.tools.adapter.abc import ABCAdapter, Event
|
|
9
|
+
from telegrinder.tools.adapter.errors import AdapterError
|
|
10
10
|
from telegrinder.types.objects import Update
|
|
11
11
|
|
|
12
12
|
if typing.TYPE_CHECKING:
|
|
13
|
-
from telegrinder.node.base import
|
|
13
|
+
from telegrinder.node.base import IsNode
|
|
14
14
|
|
|
15
|
-
Ts = typing.TypeVarTuple("Ts", default=typing.Unpack[tuple[type["Node"], ...]])
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def __init__(self, *nodes: *Ts) -> None:
|
|
16
|
+
class NodeAdapter(ABCAdapter[Update, Event[tuple["IsNode", ...]]]):
|
|
17
|
+
def __init__(self, *nodes: "IsNode") -> None:
|
|
20
18
|
self.nodes = nodes
|
|
21
19
|
|
|
22
20
|
def __repr__(self) -> str:
|
|
@@ -30,9 +28,9 @@ class NodeAdapter(typing.Generic[*Ts], ABCAdapter[Update, Event[tuple[*Ts]]]):
|
|
|
30
28
|
api: API,
|
|
31
29
|
update: Update,
|
|
32
30
|
context: Context,
|
|
33
|
-
) -> Result[Event[tuple[
|
|
31
|
+
) -> Result[Event[tuple[NodeSession, ...]], AdapterError]:
|
|
34
32
|
result = await compose_nodes(
|
|
35
|
-
nodes={
|
|
33
|
+
nodes={f"node_{i}": typing.cast("IsNode", node) for i, node in enumerate(self.nodes)},
|
|
36
34
|
ctx=context,
|
|
37
35
|
data={Update: update, API: api},
|
|
38
36
|
)
|
|
@@ -40,7 +38,7 @@ class NodeAdapter(typing.Generic[*Ts], ABCAdapter[Update, Event[tuple[*Ts]]]):
|
|
|
40
38
|
match result:
|
|
41
39
|
case Ok(collection):
|
|
42
40
|
sessions: list[NodeSession] = list(collection.sessions.values())
|
|
43
|
-
return Ok(Event(tuple(sessions)))
|
|
41
|
+
return Ok(Event(tuple(sessions)))
|
|
44
42
|
case Error(err):
|
|
45
43
|
return Error(AdapterError(f"Couldn't compose nodes, error: {err}."))
|
|
46
44
|
|
|
@@ -2,9 +2,9 @@ from fntypes.result import Error, Ok, Result
|
|
|
2
2
|
|
|
3
3
|
from telegrinder.api.api import API
|
|
4
4
|
from telegrinder.bot.dispatch.context import Context
|
|
5
|
-
from telegrinder.bot.rules.adapter.abc import ABCAdapter
|
|
6
|
-
from telegrinder.bot.rules.adapter.errors import AdapterError
|
|
7
5
|
from telegrinder.model import Model
|
|
6
|
+
from telegrinder.tools.adapter.abc import ABCAdapter
|
|
7
|
+
from telegrinder.tools.adapter.errors import AdapterError
|
|
8
8
|
from telegrinder.types.objects import Update
|
|
9
9
|
|
|
10
10
|
|
|
@@ -3,8 +3,8 @@ from fntypes.result import Ok, Result
|
|
|
3
3
|
from telegrinder.api.api import API
|
|
4
4
|
from telegrinder.bot.cute_types.update import UpdateCute
|
|
5
5
|
from telegrinder.bot.dispatch.context import Context
|
|
6
|
-
from telegrinder.
|
|
7
|
-
from telegrinder.
|
|
6
|
+
from telegrinder.tools.adapter.abc import ABCAdapter
|
|
7
|
+
from telegrinder.tools.adapter.errors import AdapterError
|
|
8
8
|
from telegrinder.types.objects import Update
|
|
9
9
|
|
|
10
10
|
|
telegrinder/tools/buttons.py
CHANGED
|
@@ -3,9 +3,10 @@ import typing
|
|
|
3
3
|
|
|
4
4
|
import msgspec
|
|
5
5
|
|
|
6
|
-
from telegrinder.msgspec_utils import
|
|
6
|
+
from telegrinder.msgspec_utils import encoder
|
|
7
7
|
from telegrinder.types.objects import (
|
|
8
8
|
CallbackGame,
|
|
9
|
+
CopyTextButton,
|
|
9
10
|
KeyboardButtonPollType,
|
|
10
11
|
KeyboardButtonRequestChat,
|
|
11
12
|
KeyboardButtonRequestUsers,
|
|
@@ -14,20 +15,21 @@ from telegrinder.types.objects import (
|
|
|
14
15
|
WebAppInfo,
|
|
15
16
|
)
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
from .callback_data_serilization import ABCDataSerializer, JSONSerializer
|
|
19
|
+
|
|
20
|
+
if typing.TYPE_CHECKING:
|
|
21
|
+
from _typeshed import DataclassInstance
|
|
22
|
+
|
|
23
|
+
type CallbackData = str | bytes | dict[str, typing.Any] | DataclassInstance | msgspec.Struct
|
|
18
24
|
|
|
19
25
|
|
|
20
26
|
@dataclasses.dataclass
|
|
21
27
|
class BaseButton:
|
|
22
28
|
def get_data(self) -> dict[str, typing.Any]:
|
|
23
|
-
return {
|
|
24
|
-
k: v if k != "callback_data" or isinstance(v, str) else encoder.encode(v)
|
|
25
|
-
for k, v in dataclasses.asdict(self).items()
|
|
26
|
-
if v is not None
|
|
27
|
-
}
|
|
29
|
+
return {k: v for k, v in dataclasses.asdict(self).items() if v is not None}
|
|
28
30
|
|
|
29
31
|
|
|
30
|
-
class RowButtons
|
|
32
|
+
class RowButtons[KeyboardButton: BaseButton]:
|
|
31
33
|
buttons: list[KeyboardButton]
|
|
32
34
|
auto_row: bool
|
|
33
35
|
|
|
@@ -39,45 +41,69 @@ class RowButtons(typing.Generic[KeyboardButton]):
|
|
|
39
41
|
return [b.get_data() for b in self.buttons]
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
@dataclasses.dataclass
|
|
44
|
+
@dataclasses.dataclass
|
|
43
45
|
class Button(BaseButton):
|
|
44
46
|
text: str
|
|
45
47
|
request_contact: bool = dataclasses.field(default=False, kw_only=True)
|
|
46
48
|
request_location: bool = dataclasses.field(default=False, kw_only=True)
|
|
47
|
-
request_chat:
|
|
48
|
-
default=None,
|
|
49
|
+
request_chat: KeyboardButtonRequestChat | None = dataclasses.field(
|
|
50
|
+
default=None,
|
|
51
|
+
kw_only=True,
|
|
49
52
|
)
|
|
50
|
-
request_user:
|
|
51
|
-
|
|
53
|
+
request_user: KeyboardButtonRequestUsers | None = dataclasses.field(default=None, kw_only=True)
|
|
54
|
+
request_poll: KeyboardButtonPollType | None = dataclasses.field(
|
|
55
|
+
default=None,
|
|
56
|
+
kw_only=True,
|
|
52
57
|
)
|
|
53
|
-
|
|
54
|
-
default=None, kw_only=True
|
|
55
|
-
)
|
|
56
|
-
web_app: dict[str, typing.Any] | WebAppInfo | None = dataclasses.field(default=None, kw_only=True)
|
|
58
|
+
web_app: WebAppInfo | None = dataclasses.field(default=None, kw_only=True)
|
|
57
59
|
|
|
58
60
|
|
|
59
|
-
@dataclasses.dataclass
|
|
61
|
+
@dataclasses.dataclass
|
|
60
62
|
class InlineButton(BaseButton):
|
|
61
63
|
text: str
|
|
62
64
|
url: str | None = dataclasses.field(default=None, kw_only=True)
|
|
63
|
-
login_url:
|
|
65
|
+
login_url: LoginUrl | None = dataclasses.field(default=None, kw_only=True)
|
|
64
66
|
pay: bool | None = dataclasses.field(default=None, kw_only=True)
|
|
65
|
-
callback_data:
|
|
66
|
-
|
|
67
|
+
callback_data: CallbackData | None = dataclasses.field(default=None, kw_only=True)
|
|
68
|
+
callback_data_serializer: dataclasses.InitVar[ABCDataSerializer[typing.Any] | None] = dataclasses.field(
|
|
69
|
+
default=None,
|
|
70
|
+
kw_only=True,
|
|
67
71
|
)
|
|
68
|
-
callback_game:
|
|
72
|
+
callback_game: CallbackGame | None = dataclasses.field(default=None, kw_only=True)
|
|
73
|
+
copy_text: str | CopyTextButton | None = dataclasses.field(default=None, kw_only=True)
|
|
69
74
|
switch_inline_query: str | None = dataclasses.field(default=None, kw_only=True)
|
|
70
75
|
switch_inline_query_current_chat: str | None = dataclasses.field(default=None, kw_only=True)
|
|
71
|
-
switch_inline_query_chosen_chat:
|
|
72
|
-
|
|
76
|
+
switch_inline_query_chosen_chat: SwitchInlineQueryChosenChat | None = dataclasses.field(
|
|
77
|
+
default=None,
|
|
78
|
+
kw_only=True,
|
|
73
79
|
)
|
|
74
|
-
web_app:
|
|
80
|
+
web_app: str | WebAppInfo | None = dataclasses.field(default=None, kw_only=True)
|
|
81
|
+
|
|
82
|
+
def __post_init__(self, callback_data_serializer: ABCDataSerializer[typing.Any] | None) -> None:
|
|
83
|
+
if (
|
|
84
|
+
callback_data_serializer is None
|
|
85
|
+
and isinstance(self.callback_data, msgspec.Struct | dict)
|
|
86
|
+
or dataclasses.is_dataclass(self.callback_data)
|
|
87
|
+
):
|
|
88
|
+
callback_data_serializer = callback_data_serializer or JSONSerializer(
|
|
89
|
+
self.callback_data.__class__,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if callback_data_serializer is not None:
|
|
93
|
+
self.callback_data = callback_data_serializer.serialize(self.callback_data)
|
|
94
|
+
elif self.callback_data is not None and not isinstance(self.callback_data, str | bytes):
|
|
95
|
+
self.callback_data = encoder.encode(self.callback_data)
|
|
96
|
+
|
|
97
|
+
if isinstance(self.copy_text, str):
|
|
98
|
+
self.copy_text = CopyTextButton(text=self.copy_text)
|
|
99
|
+
|
|
100
|
+
if isinstance(self.web_app, str):
|
|
101
|
+
self.web_app = WebAppInfo(url=self.web_app)
|
|
75
102
|
|
|
76
103
|
|
|
77
104
|
__all__ = (
|
|
78
105
|
"BaseButton",
|
|
79
106
|
"Button",
|
|
80
|
-
"DataclassInstance",
|
|
81
107
|
"InlineButton",
|
|
82
108
|
"RowButtons",
|
|
83
109
|
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import typing
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
|
|
7
|
+
import msgspec
|
|
8
|
+
from fntypes.result import Result
|
|
9
|
+
|
|
10
|
+
if typing.TYPE_CHECKING:
|
|
11
|
+
from dataclasses import Field
|
|
12
|
+
|
|
13
|
+
from _typeshed import DataclassInstance
|
|
14
|
+
|
|
15
|
+
type ModelType = DataclassWithIdentKey | ModelWithIdentKey | msgspec.Struct | DataclassInstance
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@typing.runtime_checkable
|
|
19
|
+
class DataclassWithIdentKey(typing.Protocol):
|
|
20
|
+
__key__: str
|
|
21
|
+
__dataclass_fields__: typing.ClassVar[dict[str, Field[typing.Any]]]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@typing.runtime_checkable
|
|
25
|
+
class ModelWithIdentKey(typing.Protocol):
|
|
26
|
+
__key__: str
|
|
27
|
+
__struct_fields__: typing.ClassVar[tuple[str, ...]]
|
|
28
|
+
__struct_config__: typing.ClassVar[msgspec.structs.StructConfig]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ABCDataSerializer[Data](abc.ABC):
|
|
32
|
+
ident_key: str | None = None
|
|
33
|
+
|
|
34
|
+
@abc.abstractmethod
|
|
35
|
+
def __init__(self, data_type: type[Data], /) -> None:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@cached_property
|
|
39
|
+
def key(self) -> str:
|
|
40
|
+
return self.ident_key + "_" if self.ident_key else ""
|
|
41
|
+
|
|
42
|
+
@abc.abstractmethod
|
|
43
|
+
def serialize(self, data: Data) -> str:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
@abc.abstractmethod
|
|
47
|
+
def deserialize(self, serialized_data: str) -> Result[Data, str]:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = ("ABCDataSerializer",)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
import msgspec
|
|
4
|
+
from fntypes.result import Error, Ok, Result
|
|
5
|
+
|
|
6
|
+
from telegrinder.modules import json
|
|
7
|
+
from telegrinder.msgspec_utils import decoder
|
|
8
|
+
|
|
9
|
+
from .abc import ABCDataSerializer, ModelType
|
|
10
|
+
|
|
11
|
+
type Json = dict[str, typing.Any] | ModelType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JSONSerializer[JsonT: Json](ABCDataSerializer[JsonT]):
|
|
15
|
+
@typing.overload
|
|
16
|
+
def __init__(self, model_t: type[JsonT]) -> None: ...
|
|
17
|
+
|
|
18
|
+
@typing.overload
|
|
19
|
+
def __init__(self, model_t: type[JsonT], *, ident_key: str | None = ...) -> None: ...
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
model_t: type[JsonT] = dict[str, typing.Any],
|
|
24
|
+
*,
|
|
25
|
+
ident_key: str | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.model_t = model_t
|
|
28
|
+
self.ident_key: str | None = ident_key or getattr(model_t, "__key__", None)
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def serialize_from_json(cls, data: JsonT, *, ident_key: str | None = None) -> str:
|
|
32
|
+
return cls(data.__class__, ident_key=ident_key).serialize(data)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def deserialize_to_json(cls, serialized_data: str, model_t: type[JsonT]) -> Result[JsonT, str]:
|
|
36
|
+
return cls(model_t).deserialize(serialized_data)
|
|
37
|
+
|
|
38
|
+
def serialize(self, data: JsonT) -> str:
|
|
39
|
+
return self.key + json.dumps(data)
|
|
40
|
+
|
|
41
|
+
def deserialize(self, serialized_data: str) -> Result[JsonT, str]:
|
|
42
|
+
if self.ident_key and not serialized_data.startswith(self.key):
|
|
43
|
+
return Error("Data is not corresponding to key.")
|
|
44
|
+
|
|
45
|
+
data = serialized_data.removeprefix(self.key)
|
|
46
|
+
try:
|
|
47
|
+
data_obj = json.loads(data)
|
|
48
|
+
except (msgspec.ValidationError, msgspec.DecodeError):
|
|
49
|
+
return Error("Cannot decode json.")
|
|
50
|
+
|
|
51
|
+
if not issubclass(self.model_t, dict):
|
|
52
|
+
try:
|
|
53
|
+
return Ok(decoder.convert(data_obj, type=self.model_t))
|
|
54
|
+
except (msgspec.ValidationError, msgspec.DecodeError):
|
|
55
|
+
return Error("Incorrect data.")
|
|
56
|
+
|
|
57
|
+
return Ok(data_obj)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
__all__ = ("JSONSerializer",)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import binascii
|
|
3
|
+
import dataclasses
|
|
4
|
+
import typing
|
|
5
|
+
from collections import deque
|
|
6
|
+
from contextlib import suppress
|
|
7
|
+
from functools import cached_property
|
|
8
|
+
|
|
9
|
+
import msgspec
|
|
10
|
+
from fntypes.result import Error, Ok, Result
|
|
11
|
+
|
|
12
|
+
from telegrinder.msgspec_utils import decoder, encoder, get_class_annotations
|
|
13
|
+
|
|
14
|
+
from .abc import ABCDataSerializer, ModelType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
18
|
+
class ModelParser[Model: ModelType]:
|
|
19
|
+
model_type: type[Model]
|
|
20
|
+
|
|
21
|
+
def _is_model(self, obj: typing.Any, /) -> typing.TypeGuard[ModelType]:
|
|
22
|
+
return dataclasses.is_dataclass(obj) or isinstance(obj, msgspec.Struct)
|
|
23
|
+
|
|
24
|
+
def _model_to_dict(self, model: ModelType) -> dict[str, typing.Any]:
|
|
25
|
+
if dataclasses.is_dataclass(model):
|
|
26
|
+
return dataclasses.asdict(model)
|
|
27
|
+
return msgspec.structs.asdict(model) # type: ignore
|
|
28
|
+
|
|
29
|
+
def _is_union(self, inspected_type: msgspec.inspect.Type, /) -> typing.TypeGuard[msgspec.inspect.UnionType]:
|
|
30
|
+
return isinstance(inspected_type, msgspec.inspect.UnionType)
|
|
31
|
+
|
|
32
|
+
def _is_model_type(
|
|
33
|
+
self,
|
|
34
|
+
inspected_type: msgspec.inspect.Type,
|
|
35
|
+
/,
|
|
36
|
+
) -> typing.TypeGuard[msgspec.inspect.DataclassType | msgspec.inspect.StructType]:
|
|
37
|
+
return isinstance(inspected_type, msgspec.inspect.DataclassType | msgspec.inspect.StructType)
|
|
38
|
+
|
|
39
|
+
def _is_iter_of_model(
|
|
40
|
+
self, inspected_type: msgspec.inspect.Type, /
|
|
41
|
+
) -> typing.TypeGuard[msgspec.inspect.ListType]:
|
|
42
|
+
return isinstance(
|
|
43
|
+
inspected_type,
|
|
44
|
+
msgspec.inspect.ListType | msgspec.inspect.SetType | msgspec.inspect.FrozenSetType,
|
|
45
|
+
) and self._is_model_type(inspected_type.item_type)
|
|
46
|
+
|
|
47
|
+
def _validate_annotation(self, annotation: typing.Any, /) -> tuple[type[ModelType], bool] | None:
|
|
48
|
+
is_iter_of_model = False
|
|
49
|
+
type_args: tuple[msgspec.inspect.Type, ...] | None = None
|
|
50
|
+
inspected_type = msgspec.inspect.type_info(annotation)
|
|
51
|
+
|
|
52
|
+
if self._is_union(inspected_type):
|
|
53
|
+
type_args = inspected_type.types
|
|
54
|
+
elif self._is_iter_of_model(inspected_type):
|
|
55
|
+
type_args = (inspected_type.item_type,)
|
|
56
|
+
is_iter_of_model = True
|
|
57
|
+
elif self._is_model_type(inspected_type):
|
|
58
|
+
type_args = (inspected_type,)
|
|
59
|
+
|
|
60
|
+
if type_args is not None:
|
|
61
|
+
for arg in type_args:
|
|
62
|
+
if self._is_union(arg):
|
|
63
|
+
type_args += arg.types
|
|
64
|
+
if self._is_model_type(arg):
|
|
65
|
+
return (arg.cls, is_iter_of_model)
|
|
66
|
+
if self._is_iter_of_model(arg):
|
|
67
|
+
return (arg.item_type.cls, True) # type: ignore
|
|
68
|
+
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
def parse(self, model: Model) -> list[typing.Any]:
|
|
72
|
+
"""Returns a parsed model as linked list."""
|
|
73
|
+
linked: list[typing.Any] = []
|
|
74
|
+
stack: list[typing.Any] = [(list(self._model_to_dict(model).values()), linked)]
|
|
75
|
+
|
|
76
|
+
while stack:
|
|
77
|
+
current_obj, current = stack.pop()
|
|
78
|
+
|
|
79
|
+
for item in current_obj:
|
|
80
|
+
if self._is_model(item):
|
|
81
|
+
item = self._model_to_dict(item)
|
|
82
|
+
if isinstance(item, dict):
|
|
83
|
+
new_list = []
|
|
84
|
+
current.append(new_list)
|
|
85
|
+
stack.append((list(item.values()), new_list))
|
|
86
|
+
elif isinstance(item, list | tuple):
|
|
87
|
+
new_list = []
|
|
88
|
+
current.append(new_list)
|
|
89
|
+
stack.append((item, new_list))
|
|
90
|
+
else:
|
|
91
|
+
current.append(item)
|
|
92
|
+
|
|
93
|
+
return encoder.to_builtins(linked)
|
|
94
|
+
|
|
95
|
+
def compose(self, linked: list[typing.Any]) -> dict[str, typing.Any]:
|
|
96
|
+
"""Compose linked list to dictionary based on the model class annotations `(without validation)`."""
|
|
97
|
+
root_converted_data: dict[str, typing.Any] = {}
|
|
98
|
+
stack: deque[typing.Any] = deque([(linked, self.model_type, root_converted_data)])
|
|
99
|
+
|
|
100
|
+
while stack:
|
|
101
|
+
current_data, current_model, converted_data = stack.pop()
|
|
102
|
+
|
|
103
|
+
for index, (field, annotation) in enumerate(get_class_annotations(current_model).items()):
|
|
104
|
+
obj, model_type, is_iter_of_model = current_data[index], None, False
|
|
105
|
+
|
|
106
|
+
if isinstance(obj, list) and (validated := self._validate_annotation(annotation)):
|
|
107
|
+
model_type, is_iter_of_model = validated
|
|
108
|
+
|
|
109
|
+
if model_type is not None:
|
|
110
|
+
if is_iter_of_model:
|
|
111
|
+
converted_data[field] = []
|
|
112
|
+
for item in obj:
|
|
113
|
+
new_converted_data = {}
|
|
114
|
+
converted_data[field].append(new_converted_data)
|
|
115
|
+
stack.append((item, model_type, new_converted_data))
|
|
116
|
+
else:
|
|
117
|
+
new_converted_data = {}
|
|
118
|
+
converted_data[field] = new_converted_data
|
|
119
|
+
stack.append((obj, model_type, new_converted_data))
|
|
120
|
+
else:
|
|
121
|
+
converted_data[field] = obj
|
|
122
|
+
|
|
123
|
+
return root_converted_data
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class MsgPackSerializer[Model: ModelType](ABCDataSerializer[Model]):
|
|
127
|
+
@typing.overload
|
|
128
|
+
def __init__(self, model_t: type[Model], /) -> None: ...
|
|
129
|
+
|
|
130
|
+
@typing.overload
|
|
131
|
+
def __init__(self, model_t: type[Model], /, *, ident_key: str | None = ...) -> None: ...
|
|
132
|
+
|
|
133
|
+
def __init__(self, model_t: type[Model], /, *, ident_key: str | None = None) -> None:
|
|
134
|
+
self.model_t = model_t
|
|
135
|
+
self.ident_key: str | None = ident_key or getattr(model_t, "__key__", None)
|
|
136
|
+
self._model_parser = ModelParser(model_t)
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def serialize_from_model(cls, model: Model, *, ident_key: str | None = None) -> str:
|
|
140
|
+
return cls(model.__class__, ident_key=ident_key).serialize(model)
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def deserialize_to_json(cls, serialized_data: str, model_t: type[Model]) -> Result[Model, str]:
|
|
144
|
+
return cls(model_t).deserialize(serialized_data)
|
|
145
|
+
|
|
146
|
+
@cached_property
|
|
147
|
+
def key(self) -> bytes:
|
|
148
|
+
if self.ident_key:
|
|
149
|
+
return msgspec.msgpack.encode(super().key)
|
|
150
|
+
return b""
|
|
151
|
+
|
|
152
|
+
def serialize(self, data: Model) -> str:
|
|
153
|
+
return base64.urlsafe_b64encode(
|
|
154
|
+
self.key + msgspec.msgpack.encode(self._model_parser.parse(data), enc_hook=encoder.enc_hook),
|
|
155
|
+
).decode()
|
|
156
|
+
|
|
157
|
+
def deserialize(self, serialized_data: str) -> Result[Model, str]:
|
|
158
|
+
with suppress(msgspec.DecodeError, msgspec.ValidationError, binascii.Error):
|
|
159
|
+
ser_data = base64.urlsafe_b64decode(serialized_data)
|
|
160
|
+
if self.ident_key and not ser_data.startswith(self.key):
|
|
161
|
+
return Error("Data is not corresponding to key.")
|
|
162
|
+
|
|
163
|
+
data: list[typing.Any] = msgspec.msgpack.decode(
|
|
164
|
+
ser_data.removeprefix(self.key),
|
|
165
|
+
dec_hook=decoder.dec_hook(),
|
|
166
|
+
)
|
|
167
|
+
return Ok(decoder.convert(self._model_parser.compose(data), type=self.model_t))
|
|
168
|
+
|
|
169
|
+
return Error("Incorrect data.")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
__all__ = ("MsgPackSerializer",)
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
3
|
|
|
4
|
-
from fntypes.result import Result
|
|
5
|
-
|
|
6
4
|
from telegrinder.api import API
|
|
7
5
|
from telegrinder.bot.dispatch.context import Context
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
Handler = typing.Callable[..., typing.Awaitable[typing.Any]]
|
|
7
|
+
type Handler = typing.Callable[..., typing.Awaitable[typing.Any]]
|
|
11
8
|
|
|
12
9
|
|
|
13
|
-
class ABCErrorHandler
|
|
10
|
+
class ABCErrorHandler[Event](ABC):
|
|
14
11
|
@abstractmethod
|
|
15
12
|
def __call__(
|
|
16
13
|
self,
|
|
@@ -22,11 +19,11 @@ class ABCErrorHandler(ABC, typing.Generic[Event]):
|
|
|
22
19
|
@abstractmethod
|
|
23
20
|
async def run(
|
|
24
21
|
self,
|
|
25
|
-
|
|
22
|
+
exception: BaseException,
|
|
26
23
|
event: Event,
|
|
27
24
|
api: API,
|
|
28
25
|
ctx: Context,
|
|
29
|
-
) ->
|
|
26
|
+
) -> typing.Any:
|
|
30
27
|
"""Run the error handler."""
|
|
31
28
|
|
|
32
29
|
|
|
File without changes
|