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,197 @@
|
|
|
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 kungfu.library.monad.result import Error, Ok, Result
|
|
11
|
+
|
|
12
|
+
from telegrinder.msgspec_utils import decoder, encoder, get_class_annotations
|
|
13
|
+
from telegrinder.tools.serialization.abc import ABCDataSerializer, ModelType
|
|
14
|
+
from telegrinder.tools.serialization.utils import get_model_ident_key
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import brotli # type: ignore
|
|
18
|
+
|
|
19
|
+
except ImportError:
|
|
20
|
+
brotli = None
|
|
21
|
+
|
|
22
|
+
DESERIALIZE_EXCEPTIONS: typing.Final[frozenset[type[BaseException]]] = frozenset(
|
|
23
|
+
filter(
|
|
24
|
+
None,
|
|
25
|
+
(
|
|
26
|
+
msgspec.DecodeError,
|
|
27
|
+
msgspec.ValidationError,
|
|
28
|
+
binascii.Error,
|
|
29
|
+
ValueError,
|
|
30
|
+
brotli.error if brotli is not None else None, # type: ignore
|
|
31
|
+
),
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
37
|
+
class ModelParser[Model: ModelType]:
|
|
38
|
+
model_type: type[Model]
|
|
39
|
+
|
|
40
|
+
def _is_model(self, obj: typing.Any, /) -> typing.TypeGuard[ModelType]:
|
|
41
|
+
return dataclasses.is_dataclass(obj) or isinstance(obj, msgspec.Struct)
|
|
42
|
+
|
|
43
|
+
def _model_to_dict(self, model: ModelType) -> dict[str, typing.Any]:
|
|
44
|
+
if dataclasses.is_dataclass(model):
|
|
45
|
+
return dataclasses.asdict(model)
|
|
46
|
+
return msgspec.structs.asdict(model) # type: ignore
|
|
47
|
+
|
|
48
|
+
def _is_union(self, inspected_type: msgspec.inspect.Type, /) -> typing.TypeGuard[msgspec.inspect.UnionType]:
|
|
49
|
+
return isinstance(inspected_type, msgspec.inspect.UnionType)
|
|
50
|
+
|
|
51
|
+
def _is_model_type(
|
|
52
|
+
self,
|
|
53
|
+
inspected_type: msgspec.inspect.Type,
|
|
54
|
+
/,
|
|
55
|
+
) -> typing.TypeGuard[msgspec.inspect.DataclassType | msgspec.inspect.StructType]:
|
|
56
|
+
return isinstance(inspected_type, msgspec.inspect.DataclassType | msgspec.inspect.StructType)
|
|
57
|
+
|
|
58
|
+
def _is_iter_of_model(
|
|
59
|
+
self,
|
|
60
|
+
inspected_type: msgspec.inspect.Type,
|
|
61
|
+
/,
|
|
62
|
+
) -> typing.TypeGuard[msgspec.inspect.ListType]:
|
|
63
|
+
return isinstance(
|
|
64
|
+
inspected_type,
|
|
65
|
+
msgspec.inspect.ListType | msgspec.inspect.SetType | msgspec.inspect.FrozenSetType,
|
|
66
|
+
) and self._is_model_type(inspected_type.item_type)
|
|
67
|
+
|
|
68
|
+
def _inspect_annotation(self, annotation: typing.Any, /) -> tuple[type[ModelType], bool] | None:
|
|
69
|
+
is_iter_of_model = False
|
|
70
|
+
type_args: tuple[msgspec.inspect.Type, ...] = tuple()
|
|
71
|
+
inspected_type = msgspec.inspect.type_info(annotation)
|
|
72
|
+
|
|
73
|
+
if self._is_union(inspected_type):
|
|
74
|
+
type_args = inspected_type.types
|
|
75
|
+
elif self._is_iter_of_model(inspected_type):
|
|
76
|
+
type_args = (inspected_type.item_type,)
|
|
77
|
+
is_iter_of_model = True
|
|
78
|
+
elif self._is_model_type(inspected_type):
|
|
79
|
+
type_args = (inspected_type,)
|
|
80
|
+
|
|
81
|
+
for arg in type_args:
|
|
82
|
+
if self._is_union(arg):
|
|
83
|
+
type_args += arg.types
|
|
84
|
+
|
|
85
|
+
if self._is_model_type(arg):
|
|
86
|
+
return (arg.cls, is_iter_of_model)
|
|
87
|
+
|
|
88
|
+
if self._is_iter_of_model(arg):
|
|
89
|
+
return (arg.item_type.cls, True) # type: ignore
|
|
90
|
+
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def parse(self, model: Model) -> list[typing.Any]:
|
|
94
|
+
"""Returns a parsed model as linked list."""
|
|
95
|
+
linked: list[typing.Any] = []
|
|
96
|
+
stack: list[typing.Any] = [(list(self._model_to_dict(model).values()), linked)]
|
|
97
|
+
|
|
98
|
+
while stack:
|
|
99
|
+
current_obj, current = stack.pop()
|
|
100
|
+
|
|
101
|
+
for item in current_obj:
|
|
102
|
+
if self._is_model(item):
|
|
103
|
+
item = self._model_to_dict(item)
|
|
104
|
+
if isinstance(item, dict):
|
|
105
|
+
new_list = []
|
|
106
|
+
current.append(new_list)
|
|
107
|
+
stack.append((list(item.values()), new_list))
|
|
108
|
+
elif isinstance(item, list | tuple):
|
|
109
|
+
new_list = []
|
|
110
|
+
current.append(new_list)
|
|
111
|
+
stack.append((item, new_list))
|
|
112
|
+
else:
|
|
113
|
+
current.append(item)
|
|
114
|
+
|
|
115
|
+
return encoder.to_builtins(linked)
|
|
116
|
+
|
|
117
|
+
def compose(self, linked: list[typing.Any]) -> dict[str, typing.Any]:
|
|
118
|
+
"""Compose linked list to dictionary based on the model class annotations `(without validation)`."""
|
|
119
|
+
root_converted_data: dict[str, typing.Any] = {}
|
|
120
|
+
stack: deque[typing.Any] = deque([(linked, self.model_type, root_converted_data)])
|
|
121
|
+
|
|
122
|
+
while stack:
|
|
123
|
+
current_data, current_model, converted_data = stack.pop()
|
|
124
|
+
|
|
125
|
+
for index, (field, annotation) in enumerate(get_class_annotations(current_model).items()):
|
|
126
|
+
obj, model_type, is_iter_of_model = current_data[index], None, False
|
|
127
|
+
|
|
128
|
+
if isinstance(obj, list) and (inspected := self._inspect_annotation(annotation)):
|
|
129
|
+
model_type, is_iter_of_model = inspected
|
|
130
|
+
|
|
131
|
+
if model_type is not None:
|
|
132
|
+
if is_iter_of_model:
|
|
133
|
+
converted_data[field] = []
|
|
134
|
+
for item in obj:
|
|
135
|
+
new_converted_data = {}
|
|
136
|
+
converted_data[field].append(new_converted_data)
|
|
137
|
+
stack.append((item, model_type, new_converted_data))
|
|
138
|
+
else:
|
|
139
|
+
new_converted_data = {}
|
|
140
|
+
converted_data[field] = new_converted_data
|
|
141
|
+
stack.append((obj, model_type, new_converted_data))
|
|
142
|
+
else:
|
|
143
|
+
converted_data[field] = obj
|
|
144
|
+
|
|
145
|
+
return root_converted_data
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class MsgPackSerializer[Model: ModelType](ABCDataSerializer[Model]):
|
|
149
|
+
@typing.overload
|
|
150
|
+
def __init__(self, model_t: type[Model], /) -> None: ...
|
|
151
|
+
|
|
152
|
+
@typing.overload
|
|
153
|
+
def __init__(self, model_t: type[Model], /, *, ident_key: str | None = ...) -> None: ...
|
|
154
|
+
|
|
155
|
+
def __init__(self, model_t: type[Model], /, *, ident_key: str | None = None) -> None:
|
|
156
|
+
self.model_t = model_t
|
|
157
|
+
self.ident_key = ident_key or get_model_ident_key(model_t)
|
|
158
|
+
self._model_parser = ModelParser(model_t)
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def serialize_from_model(cls, model: Model, *, ident_key: str | None = None) -> str:
|
|
162
|
+
return cls(model.__class__, ident_key=ident_key).serialize(model)
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def deserialize_to_json(cls, serialized_data: str, model_t: type[Model]) -> Result[Model, str]:
|
|
166
|
+
return cls(model_t).deserialize(serialized_data)
|
|
167
|
+
|
|
168
|
+
@cached_property
|
|
169
|
+
def key(self) -> bytes:
|
|
170
|
+
return msgspec.msgpack.encode(super().key) if self.ident_key else b""
|
|
171
|
+
|
|
172
|
+
def serialize(self, data: Model, /) -> str:
|
|
173
|
+
encoded = self.key + msgspec.msgpack.encode(self._model_parser.parse(data), enc_hook=encoder.enc_hook)
|
|
174
|
+
if brotli is not None:
|
|
175
|
+
return base64.b85encode(brotli.compress(encoded, quality=11)).decode() # type: ignore
|
|
176
|
+
return base64.urlsafe_b64encode(encoded).decode()
|
|
177
|
+
|
|
178
|
+
def deserialize(self, serialized_data: str, /) -> Result[Model, str]:
|
|
179
|
+
with suppress(*DESERIALIZE_EXCEPTIONS):
|
|
180
|
+
if brotli is not None:
|
|
181
|
+
ser_data = typing.cast("bytes", brotli.decompress(base64.b85decode(serialized_data)))
|
|
182
|
+
else:
|
|
183
|
+
ser_data = base64.urlsafe_b64decode(serialized_data)
|
|
184
|
+
|
|
185
|
+
if not ser_data.startswith(self.key):
|
|
186
|
+
return Error("Data is not corresponding to key.")
|
|
187
|
+
|
|
188
|
+
data: list[typing.Any] = msgspec.msgpack.decode(
|
|
189
|
+
ser_data.removeprefix(self.key),
|
|
190
|
+
dec_hook=decoder.dec_hook(),
|
|
191
|
+
)
|
|
192
|
+
return Ok(decoder.convert(self._model_parser.compose(data), type=self.model_t))
|
|
193
|
+
|
|
194
|
+
return Error("Incorrect data.")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
__all__ = ("MsgPackSerializer",)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
if typing.TYPE_CHECKING:
|
|
4
|
+
from telegrinder.tools.serialization.abc import ABCDataSerializer
|
|
5
|
+
|
|
6
|
+
IDENT_KEY: typing.Final = "__key__"
|
|
7
|
+
SERIALIZER_KEY: typing.Final = "__serializer__"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_model_ident_key(model: typing.Any, /) -> str | None:
|
|
11
|
+
return getattr(model, IDENT_KEY, None)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_model_serializer(model: typing.Any, /) -> type[ABCDataSerializer[typing.Any]] | None:
|
|
15
|
+
return getattr(model, SERIALIZER_KEY, None)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ("get_model_ident_key", "get_model_serializer")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
|
|
3
|
+
from telegrinder.tools.singleton.singleton import SingletonMeta
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ABCSingletonMeta(abc.ABCMeta, SingletonMeta):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ABCSingleton(metaclass=ABCSingletonMeta):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = ("ABCSingleton", "ABCSingletonMeta")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SingletonMeta(type):
|
|
5
|
+
if not typing.TYPE_CHECKING:
|
|
6
|
+
__instance = None
|
|
7
|
+
|
|
8
|
+
def __call__(cls, *args, **kwargs):
|
|
9
|
+
if cls.__instance is None:
|
|
10
|
+
cls.__instance = super().__call__(*args, **kwargs)
|
|
11
|
+
return cls.__instance
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Singleton(metaclass=SingletonMeta):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ("Singleton", "SingletonMeta")
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from telegrinder.tools.aio import maybe_awaitable
|
|
4
|
+
|
|
5
|
+
if typing.TYPE_CHECKING:
|
|
6
|
+
from telegrinder.node import State, StateMutator
|
|
7
|
+
|
|
8
|
+
class BoundMethod[**P, T, R](typing.Protocol):
|
|
9
|
+
@staticmethod
|
|
10
|
+
def __call__(self: T, *args: P.args, **kwargs: P.kwargs) -> typing.Coroutine[typing.Any, typing.Any, R] | R: ...
|
|
11
|
+
|
|
12
|
+
@typing.runtime_checkable
|
|
13
|
+
class NotBoundFunction[**P, R](typing.Protocol):
|
|
14
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> typing.Coroutine[typing.Any, typing.Any, R] | R: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class mutation[**P, IntoState: State, BoundState: State | None]: # noqa: N801
|
|
18
|
+
from_state: State | None
|
|
19
|
+
|
|
20
|
+
@typing.overload
|
|
21
|
+
def __init__(
|
|
22
|
+
self: mutation[P, IntoState, BoundState], # type: ignore
|
|
23
|
+
construct_mutation: BoundMethod[P, BoundState, IntoState],
|
|
24
|
+
) -> None: ...
|
|
25
|
+
|
|
26
|
+
@typing.overload
|
|
27
|
+
def __init__(
|
|
28
|
+
self: mutation[P, IntoState, None], # type: ignore
|
|
29
|
+
construct_mutation: NotBoundFunction[P, IntoState],
|
|
30
|
+
) -> None: ...
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
construct_mutation: typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, IntoState] | IntoState],
|
|
35
|
+
) -> None:
|
|
36
|
+
self.construct_mutation = construct_mutation
|
|
37
|
+
self.from_state = None
|
|
38
|
+
|
|
39
|
+
@typing.overload
|
|
40
|
+
async def __call__(
|
|
41
|
+
self: mutation[P, IntoState, None],
|
|
42
|
+
mutator: StateMutator,
|
|
43
|
+
/,
|
|
44
|
+
*args: P.args,
|
|
45
|
+
**kwargs: P.kwargs,
|
|
46
|
+
) -> IntoState: ...
|
|
47
|
+
|
|
48
|
+
@typing.overload
|
|
49
|
+
async def __call__(
|
|
50
|
+
self: mutation[P, IntoState, State],
|
|
51
|
+
*args: P.args,
|
|
52
|
+
**kwargs: P.kwargs,
|
|
53
|
+
) -> IntoState: ...
|
|
54
|
+
|
|
55
|
+
async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> IntoState:
|
|
56
|
+
mutator: StateMutator
|
|
57
|
+
|
|
58
|
+
if self.from_state is not None:
|
|
59
|
+
if self.from_state.__mutator__ is None:
|
|
60
|
+
raise RuntimeError("State is not bound. Bind state with .bind(mutator) before mutating")
|
|
61
|
+
|
|
62
|
+
await self.from_state.exit()
|
|
63
|
+
|
|
64
|
+
new_state = await maybe_awaitable(self.construct_mutation(self.from_state, *args, **kwargs))
|
|
65
|
+
mutator = self.from_state.__mutator__
|
|
66
|
+
else:
|
|
67
|
+
mutator, args = args[0], args[1:]
|
|
68
|
+
|
|
69
|
+
if (state := await mutator.get()) is not None:
|
|
70
|
+
await state.exit()
|
|
71
|
+
|
|
72
|
+
new_state = await maybe_awaitable(self.construct_mutation(*args, **kwargs))
|
|
73
|
+
|
|
74
|
+
new_state.bind(mutator)
|
|
75
|
+
await new_state.enter()
|
|
76
|
+
|
|
77
|
+
return new_state
|
|
78
|
+
|
|
79
|
+
def __get__(self, instance: State, owner: typing.Any) -> typing.Self:
|
|
80
|
+
if instance is not None:
|
|
81
|
+
self.from_state = instance
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
__all__ = ("mutation",)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import dataclasses
|
|
3
|
+
import enum
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
from kungfu.library.monad.option import Option
|
|
7
|
+
|
|
8
|
+
if typing.TYPE_CHECKING:
|
|
9
|
+
from telegrinder.bot.rules.state import State, StateMeta
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclasses.dataclass
|
|
13
|
+
class StateData[Payload]:
|
|
14
|
+
key: str | enum.Enum
|
|
15
|
+
payload: Payload
|
|
16
|
+
|
|
17
|
+
async def save(self, user_id: int, storage: ABCStateStorage[Payload]) -> None:
|
|
18
|
+
await storage.set(user_id, key=self.key, payload=self.payload)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ABCStateStorage[Payload](abc.ABC):
|
|
22
|
+
@abc.abstractmethod
|
|
23
|
+
async def get(self, user_id: int) -> Option[StateData[Payload]]: ...
|
|
24
|
+
|
|
25
|
+
@abc.abstractmethod
|
|
26
|
+
async def delete(self, user_id: int) -> None: ...
|
|
27
|
+
|
|
28
|
+
@abc.abstractmethod
|
|
29
|
+
async def set(self, user_id: int, key: str | enum.Enum, payload: Payload) -> None: ...
|
|
30
|
+
|
|
31
|
+
def State(self, key: str | StateMeta | enum.Enum | None = None, /) -> State[Payload]: # noqa: N802
|
|
32
|
+
"""Can be used as a shortcut to get a state rule dependant on current storage."""
|
|
33
|
+
from telegrinder.bot.rules.state import State, StateMeta
|
|
34
|
+
|
|
35
|
+
return State(storage=self, key=key or StateMeta.ANY)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = ("ABCStateStorage", "StateData")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from kungfu.library.misc import from_optional
|
|
4
|
+
from kungfu.library.monad.option import Option
|
|
5
|
+
|
|
6
|
+
from telegrinder.tools.state_storage.abc import ABCStateStorage, StateData
|
|
7
|
+
|
|
8
|
+
type Payload = dict[str, typing.Any]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MemoryStateStorage[T = Payload](ABCStateStorage[T]):
|
|
12
|
+
storage: dict[int, StateData[T]]
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self.storage = {}
|
|
16
|
+
|
|
17
|
+
async def get(self, user_id: int) -> Option[StateData[T]]:
|
|
18
|
+
return from_optional(self.storage.get(user_id))
|
|
19
|
+
|
|
20
|
+
async def set(self, user_id: int, key: str, payload: T) -> None:
|
|
21
|
+
self.storage[user_id] = StateData(key, payload)
|
|
22
|
+
|
|
23
|
+
async def delete(self, user_id: int) -> None:
|
|
24
|
+
self.storage.pop(user_id)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = ("MemoryStateStorage",)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
def to_utf16_map(s: str, /) -> list[int]:
|
|
2
|
+
utf16_map = list[int]()
|
|
3
|
+
utf16_pos = 0
|
|
4
|
+
|
|
5
|
+
for char in s:
|
|
6
|
+
utf16_map.append(utf16_pos)
|
|
7
|
+
utf16_len = len(char.encode("utf-16-le")) // 2
|
|
8
|
+
utf16_pos += utf16_len
|
|
9
|
+
|
|
10
|
+
utf16_map.append(utf16_pos)
|
|
11
|
+
return utf16_map
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def utf16_to_py_index(utf16_map: list[int], utf16_index: int, /) -> int:
|
|
15
|
+
for index, u in enumerate(utf16_map):
|
|
16
|
+
if u >= utf16_index:
|
|
17
|
+
return index
|
|
18
|
+
|
|
19
|
+
return len(utf16_map) - 1
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__all__ = ("to_utf16_map", "utf16_to_py_index")
|