telegrinder 0.3.3.post1__py3-none-any.whl → 0.3.4.post1__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 +144 -144
- telegrinder/api/__init__.py +8 -8
- telegrinder/api/api.py +93 -93
- telegrinder/api/error.py +16 -16
- telegrinder/api/response.py +20 -20
- telegrinder/api/token.py +36 -36
- telegrinder/bot/__init__.py +66 -66
- telegrinder/bot/bot.py +76 -76
- telegrinder/bot/cute_types/__init__.py +17 -17
- telegrinder/bot/cute_types/base.py +258 -258
- telegrinder/bot/cute_types/callback_query.py +385 -385
- telegrinder/bot/cute_types/chat_join_request.py +61 -61
- telegrinder/bot/cute_types/chat_member_updated.py +160 -160
- telegrinder/bot/cute_types/inline_query.py +43 -43
- telegrinder/bot/cute_types/message.py +2637 -2637
- telegrinder/bot/cute_types/update.py +104 -109
- telegrinder/bot/cute_types/utils.py +95 -95
- telegrinder/bot/dispatch/__init__.py +55 -55
- telegrinder/bot/dispatch/abc.py +77 -77
- telegrinder/bot/dispatch/context.py +98 -98
- telegrinder/bot/dispatch/dispatch.py +202 -202
- telegrinder/bot/dispatch/handler/__init__.py +13 -13
- telegrinder/bot/dispatch/handler/abc.py +24 -24
- telegrinder/bot/dispatch/handler/audio_reply.py +44 -44
- telegrinder/bot/dispatch/handler/base.py +57 -57
- telegrinder/bot/dispatch/handler/document_reply.py +44 -44
- telegrinder/bot/dispatch/handler/func.py +135 -135
- telegrinder/bot/dispatch/handler/media_group_reply.py +43 -43
- telegrinder/bot/dispatch/handler/message_reply.py +36 -36
- telegrinder/bot/dispatch/handler/photo_reply.py +44 -44
- telegrinder/bot/dispatch/handler/sticker_reply.py +37 -37
- telegrinder/bot/dispatch/handler/video_reply.py +44 -44
- telegrinder/bot/dispatch/middleware/__init__.py +3 -3
- telegrinder/bot/dispatch/middleware/abc.py +22 -16
- telegrinder/bot/dispatch/process.py +157 -132
- telegrinder/bot/dispatch/return_manager/__init__.py +13 -13
- telegrinder/bot/dispatch/return_manager/abc.py +108 -108
- telegrinder/bot/dispatch/return_manager/callback_query.py +20 -20
- telegrinder/bot/dispatch/return_manager/inline_query.py +15 -15
- telegrinder/bot/dispatch/return_manager/message.py +36 -36
- telegrinder/bot/dispatch/view/__init__.py +13 -13
- telegrinder/bot/dispatch/view/abc.py +41 -41
- telegrinder/bot/dispatch/view/base.py +200 -200
- telegrinder/bot/dispatch/view/box.py +129 -129
- telegrinder/bot/dispatch/view/callback_query.py +17 -17
- telegrinder/bot/dispatch/view/chat_join_request.py +16 -16
- telegrinder/bot/dispatch/view/chat_member.py +39 -39
- telegrinder/bot/dispatch/view/inline_query.py +17 -17
- telegrinder/bot/dispatch/view/message.py +44 -44
- telegrinder/bot/dispatch/view/raw.py +114 -114
- telegrinder/bot/dispatch/waiter_machine/__init__.py +17 -17
- telegrinder/bot/dispatch/waiter_machine/actions.py +13 -13
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +8 -8
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +55 -55
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +57 -57
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +51 -51
- telegrinder/bot/dispatch/waiter_machine/hasher/state.py +19 -19
- telegrinder/bot/dispatch/waiter_machine/machine.py +172 -167
- telegrinder/bot/dispatch/waiter_machine/middleware.py +89 -89
- telegrinder/bot/dispatch/waiter_machine/short_state.py +68 -68
- telegrinder/bot/polling/__init__.py +4 -4
- telegrinder/bot/polling/abc.py +25 -25
- telegrinder/bot/polling/polling.py +131 -131
- telegrinder/bot/rules/__init__.py +62 -62
- telegrinder/bot/rules/abc.py +213 -213
- telegrinder/bot/rules/adapter/__init__.py +12 -9
- telegrinder/bot/rules/adapter/abc.py +31 -29
- telegrinder/bot/rules/adapter/errors.py +5 -5
- telegrinder/bot/rules/adapter/event.py +65 -67
- telegrinder/bot/rules/adapter/node.py +48 -48
- telegrinder/bot/rules/adapter/raw_event.py +27 -0
- telegrinder/bot/rules/adapter/raw_update.py +30 -30
- telegrinder/bot/rules/callback_data.py +170 -170
- telegrinder/bot/rules/chat_join.py +46 -46
- telegrinder/bot/rules/command.py +126 -126
- telegrinder/bot/rules/enum_text.py +36 -36
- telegrinder/bot/rules/func.py +26 -26
- telegrinder/bot/rules/fuzzy.py +24 -24
- telegrinder/bot/rules/inline.py +60 -60
- telegrinder/bot/rules/integer.py +20 -20
- telegrinder/bot/rules/is_from.py +127 -127
- telegrinder/bot/rules/markup.py +43 -43
- telegrinder/bot/rules/mention.py +14 -14
- telegrinder/bot/rules/message.py +17 -17
- telegrinder/bot/rules/message_entities.py +35 -35
- telegrinder/bot/rules/node.py +27 -27
- telegrinder/bot/rules/regex.py +37 -37
- telegrinder/bot/rules/rule_enum.py +72 -72
- telegrinder/bot/rules/start.py +42 -42
- telegrinder/bot/rules/state.py +37 -37
- telegrinder/bot/rules/text.py +33 -33
- telegrinder/bot/rules/update.py +15 -15
- telegrinder/bot/scenario/__init__.py +5 -5
- telegrinder/bot/scenario/abc.py +19 -19
- telegrinder/bot/scenario/checkbox.py +176 -167
- telegrinder/bot/scenario/choice.py +51 -46
- telegrinder/client/__init__.py +4 -4
- telegrinder/client/abc.py +75 -75
- telegrinder/client/aiohttp.py +130 -130
- telegrinder/model.py +320 -295
- telegrinder/modules.py +237 -237
- telegrinder/msgspec_json.py +14 -14
- telegrinder/msgspec_utils.py +410 -410
- telegrinder/node/__init__.py +0 -0
- telegrinder/node/attachment.py +87 -87
- telegrinder/node/base.py +166 -166
- telegrinder/node/callback_query.py +53 -53
- telegrinder/node/command.py +33 -33
- telegrinder/node/composer.py +198 -198
- telegrinder/node/container.py +27 -27
- telegrinder/node/event.py +65 -65
- telegrinder/node/me.py +16 -16
- telegrinder/node/message.py +14 -14
- telegrinder/node/polymorphic.py +48 -48
- telegrinder/node/rule.py +76 -76
- telegrinder/node/scope.py +38 -38
- telegrinder/node/source.py +71 -71
- telegrinder/node/text.py +41 -41
- telegrinder/node/tools/__init__.py +3 -3
- telegrinder/node/tools/generator.py +40 -40
- telegrinder/node/update.py +15 -15
- telegrinder/rules.py +0 -0
- telegrinder/tools/__init__.py +74 -74
- telegrinder/tools/buttons.py +79 -79
- telegrinder/tools/error_handler/__init__.py +7 -7
- telegrinder/tools/error_handler/abc.py +33 -33
- telegrinder/tools/error_handler/error.py +9 -9
- telegrinder/tools/error_handler/error_handler.py +193 -193
- telegrinder/tools/formatting/__init__.py +46 -46
- telegrinder/tools/formatting/html.py +283 -283
- telegrinder/tools/formatting/links.py +33 -33
- telegrinder/tools/formatting/spec_html_formats.py +111 -111
- telegrinder/tools/functional.py +12 -12
- telegrinder/tools/global_context/__init__.py +7 -7
- telegrinder/tools/global_context/abc.py +63 -63
- telegrinder/tools/global_context/global_context.py +412 -412
- telegrinder/tools/global_context/telegrinder_ctx.py +27 -27
- telegrinder/tools/i18n/__init__.py +7 -7
- telegrinder/tools/i18n/abc.py +30 -30
- telegrinder/tools/i18n/middleware/__init__.py +3 -3
- telegrinder/tools/i18n/middleware/abc.py +25 -25
- telegrinder/tools/i18n/simple.py +43 -43
- telegrinder/tools/kb_set/__init__.py +4 -4
- telegrinder/tools/kb_set/base.py +15 -15
- telegrinder/tools/kb_set/yaml.py +63 -63
- telegrinder/tools/keyboard.py +132 -132
- telegrinder/tools/limited_dict.py +37 -37
- telegrinder/tools/loop_wrapper/__init__.py +4 -4
- telegrinder/tools/loop_wrapper/abc.py +15 -15
- telegrinder/tools/loop_wrapper/loop_wrapper.py +224 -224
- telegrinder/tools/magic.py +157 -157
- telegrinder/tools/parse_mode.py +6 -6
- telegrinder/tools/state_storage/__init__.py +4 -4
- telegrinder/tools/state_storage/abc.py +35 -35
- telegrinder/tools/state_storage/memory.py +25 -25
- telegrinder/types/__init__.py +260 -260
- telegrinder/types/enums.py +701 -701
- telegrinder/types/methods.py +4633 -4633
- telegrinder/types/objects.py +6950 -8561
- telegrinder/verification_utils.py +32 -32
- {telegrinder-0.3.3.post1.dist-info → telegrinder-0.3.4.post1.dist-info}/LICENSE +22 -22
- {telegrinder-0.3.3.post1.dist-info → telegrinder-0.3.4.post1.dist-info}/METADATA +1 -1
- telegrinder-0.3.4.post1.dist-info/RECORD +165 -0
- telegrinder-0.3.3.post1.dist-info/RECORD +0 -164
- {telegrinder-0.3.3.post1.dist-info → telegrinder-0.3.4.post1.dist-info}/WHEEL +0 -0
|
@@ -1,258 +1,258 @@
|
|
|
1
|
-
import dataclasses
|
|
2
|
-
import inspect
|
|
3
|
-
import typing
|
|
4
|
-
from functools import wraps
|
|
5
|
-
|
|
6
|
-
import msgspec
|
|
7
|
-
import typing_extensions
|
|
8
|
-
from fntypes.result import Result
|
|
9
|
-
|
|
10
|
-
from telegrinder.api.api import API
|
|
11
|
-
from telegrinder.model import Model, get_params
|
|
12
|
-
|
|
13
|
-
F = typing.TypeVar("F", bound=typing.Callable[..., typing.Any])
|
|
14
|
-
Cute = typing.TypeVar("Cute", bound="BaseCute")
|
|
15
|
-
Update = typing_extensions.TypeVar("Update", bound=Model)
|
|
16
|
-
CtxAPI = typing_extensions.TypeVar("CtxAPI", bound=API, default=API)
|
|
17
|
-
|
|
18
|
-
Executor: typing.TypeAlias = typing.Callable[
|
|
19
|
-
[Cute, str, dict[str, typing.Any]],
|
|
20
|
-
typing.Awaitable[Result[typing.Any, typing.Any]],
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
if typing.TYPE_CHECKING:
|
|
24
|
-
|
|
25
|
-
class BaseCute(Model, typing.Generic[Update, CtxAPI]):
|
|
26
|
-
api: API
|
|
27
|
-
|
|
28
|
-
@classmethod
|
|
29
|
-
def from_update(cls, update: Update, bound_api: API) -> typing.Self: ...
|
|
30
|
-
|
|
31
|
-
@property
|
|
32
|
-
def ctx_api(self) -> CtxAPI: ...
|
|
33
|
-
|
|
34
|
-
def to_dict(
|
|
35
|
-
self,
|
|
36
|
-
*,
|
|
37
|
-
exclude_fields: set[str] | None = None,
|
|
38
|
-
) -> dict[str, typing.Any]:
|
|
39
|
-
"""
|
|
40
|
-
:param exclude_fields: Cute model field names to exclude from the dictionary representation of this cute model.
|
|
41
|
-
:return: A dictionary representation of this cute model.
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
...
|
|
45
|
-
|
|
46
|
-
def to_full_dict(
|
|
47
|
-
self,
|
|
48
|
-
*,
|
|
49
|
-
exclude_fields: set[str] | None = None,
|
|
50
|
-
) -> dict[str, typing.Any]:
|
|
51
|
-
"""
|
|
52
|
-
:param exclude_fields: Cute model field names to exclude from the dictionary representation of this cute model.
|
|
53
|
-
:return: A dictionary representation of this model including all models, structs, custom types.
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
...
|
|
57
|
-
|
|
58
|
-
else:
|
|
59
|
-
import msgspec
|
|
60
|
-
from fntypes.co import Nothing, Some, Variative
|
|
61
|
-
|
|
62
|
-
from telegrinder.msgspec_utils import Option, decoder, encoder
|
|
63
|
-
from telegrinder.msgspec_utils import get_class_annotations as _get_class_annotations
|
|
64
|
-
|
|
65
|
-
def _get_cute_from_generic(generic_args):
|
|
66
|
-
for arg in generic_args:
|
|
67
|
-
orig_arg = typing.get_origin(arg) or arg
|
|
68
|
-
|
|
69
|
-
if not isinstance(orig_arg, type):
|
|
70
|
-
continue
|
|
71
|
-
if orig_arg in (Variative, Some, Option):
|
|
72
|
-
return _get_cute_from_generic(typing.get_args(arg))
|
|
73
|
-
if issubclass(arg, BaseCute):
|
|
74
|
-
return arg
|
|
75
|
-
|
|
76
|
-
return None
|
|
77
|
-
|
|
78
|
-
def _get_cute_annotations(annotations):
|
|
79
|
-
cute_annotations = {}
|
|
80
|
-
|
|
81
|
-
for key, hint in annotations.items():
|
|
82
|
-
if not isinstance(hint, type):
|
|
83
|
-
if (cute := _get_cute_from_generic(typing.get_args(hint))) is not None:
|
|
84
|
-
cute_annotations[key] = cute
|
|
85
|
-
|
|
86
|
-
elif issubclass(hint, BaseCute):
|
|
87
|
-
cute_annotations[key] = hint
|
|
88
|
-
|
|
89
|
-
return cute_annotations
|
|
90
|
-
|
|
91
|
-
def _get_value(value):
|
|
92
|
-
while isinstance(value, Variative | Some):
|
|
93
|
-
if isinstance(value, Variative):
|
|
94
|
-
value = value.v
|
|
95
|
-
if isinstance(value, Some):
|
|
96
|
-
value = value.value
|
|
97
|
-
return value
|
|
98
|
-
|
|
99
|
-
class BaseCute(typing.Generic[Update, CtxAPI]):
|
|
100
|
-
def __init_subclass__(cls, *args, **kwargs):
|
|
101
|
-
super().__init_subclass__(*args, **kwargs)
|
|
102
|
-
|
|
103
|
-
if not cls.__bases__ or not issubclass(cls.__bases__[0], BaseCute):
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
cls.__is_solved_annotations__ = False
|
|
107
|
-
cls.__cute_annotations__ = None
|
|
108
|
-
|
|
109
|
-
@classmethod
|
|
110
|
-
def from_update(cls, update, bound_api):
|
|
111
|
-
if not cls.__is_solved_annotations__:
|
|
112
|
-
cls.__is_solved_annotations__ = True
|
|
113
|
-
cls.__annotations__ = _get_class_annotations(cls)
|
|
114
|
-
|
|
115
|
-
if cls.__cute_annotations__ is None:
|
|
116
|
-
cls.__cute_annotations__ = _get_cute_annotations(cls.__annotations__)
|
|
117
|
-
|
|
118
|
-
return cls(
|
|
119
|
-
**{
|
|
120
|
-
field: decoder.convert(
|
|
121
|
-
cls.__cute_annotations__[field].from_update(_get_value(value), bound_api=bound_api),
|
|
122
|
-
type=cls.__annotations__[field],
|
|
123
|
-
)
|
|
124
|
-
if field in cls.__cute_annotations__ and not isinstance(value, Nothing)
|
|
125
|
-
else value
|
|
126
|
-
for field, value in update.to_dict().items()
|
|
127
|
-
},
|
|
128
|
-
api=bound_api,
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
@property
|
|
132
|
-
def ctx_api(self):
|
|
133
|
-
return self.api
|
|
134
|
-
|
|
135
|
-
def _to_dict(self, dct_name, exclude_fields, full):
|
|
136
|
-
if dct_name not in self.__dict__:
|
|
137
|
-
self.__dict__[dct_name] = (
|
|
138
|
-
msgspec.structs.asdict(self)
|
|
139
|
-
if not full
|
|
140
|
-
else encoder.to_builtins(
|
|
141
|
-
{
|
|
142
|
-
k: field.to_dict(exclude_fields=exclude_fields)
|
|
143
|
-
if isinstance(field := _get_value(getattr(self, k)), BaseCute)
|
|
144
|
-
else field
|
|
145
|
-
for k in self.__struct_fields__
|
|
146
|
-
if k not in exclude_fields
|
|
147
|
-
},
|
|
148
|
-
order="deterministic",
|
|
149
|
-
)
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
if not exclude_fields:
|
|
153
|
-
return self.__dict__[dct_name]
|
|
154
|
-
|
|
155
|
-
return {key: value for key, value in self.__dict__[dct_name].items() if key not in exclude_fields}
|
|
156
|
-
|
|
157
|
-
def to_dict(self, *, exclude_fields=None):
|
|
158
|
-
exclude_fields = exclude_fields or set()
|
|
159
|
-
return self._to_dict("model_as_dict", exclude_fields={"api"} | exclude_fields, full=False)
|
|
160
|
-
|
|
161
|
-
def to_full_dict(self, *, exclude_fields=None):
|
|
162
|
-
exclude_fields = exclude_fields or set()
|
|
163
|
-
return self._to_dict("model_as_full_dict", exclude_fields={"api"} | exclude_fields, full=True)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def compose_method_params(
|
|
167
|
-
params: dict[str, typing.Any],
|
|
168
|
-
update: Cute,
|
|
169
|
-
*,
|
|
170
|
-
default_params: set[str | tuple[str, str]] | None = None,
|
|
171
|
-
validators: dict[str, typing.Callable[[Cute], bool]] | None = None,
|
|
172
|
-
) -> dict[str, typing.Any]:
|
|
173
|
-
"""Compose method `params` from `update` by `default_params` and `validators`.
|
|
174
|
-
|
|
175
|
-
:param params: Method params.
|
|
176
|
-
:param update: Update object.
|
|
177
|
-
:param default_params: Default params. \
|
|
178
|
-
(`str`) - Attribute name to be get from `update` if param is undefined. \
|
|
179
|
-
(`tuple[str, str]`): tuple[0] - Parameter name to be set in `params`, \
|
|
180
|
-
tuple[1] - attribute name to be get from `update`.
|
|
181
|
-
:param validators: Validators mapping (`str, Callable`), key - `Parameter name` \
|
|
182
|
-
for which the validator will be applied, value - `Validator`, if returned `True` \
|
|
183
|
-
parameter will be set, otherwise will not.
|
|
184
|
-
:return: Composed params.
|
|
185
|
-
"""
|
|
186
|
-
|
|
187
|
-
default_params = default_params or set()
|
|
188
|
-
validators = validators or {}
|
|
189
|
-
|
|
190
|
-
for param in default_params:
|
|
191
|
-
param_name = param if isinstance(param, str) else param[0]
|
|
192
|
-
if param_name not in params:
|
|
193
|
-
if param_name in validators and not validators[param_name](update):
|
|
194
|
-
continue
|
|
195
|
-
params[param_name] = getattr(update, param if isinstance(param, str) else param[1])
|
|
196
|
-
|
|
197
|
-
return params
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def shortcut(
|
|
201
|
-
method_name: str,
|
|
202
|
-
*,
|
|
203
|
-
executor: Executor[Cute] | None = None,
|
|
204
|
-
custom_params: set[str] | None = None,
|
|
205
|
-
):
|
|
206
|
-
def wrapper(func: F) -> F:
|
|
207
|
-
@wraps(func)
|
|
208
|
-
async def inner(
|
|
209
|
-
self: Cute,
|
|
210
|
-
*args: typing.Any,
|
|
211
|
-
**kwargs: typing.Any,
|
|
212
|
-
) -> typing.Any:
|
|
213
|
-
if executor is None:
|
|
214
|
-
return await func(self, *args, **kwargs)
|
|
215
|
-
|
|
216
|
-
if not hasattr(func, "_signature_params"):
|
|
217
|
-
setattr(
|
|
218
|
-
func,
|
|
219
|
-
"_signature_params",
|
|
220
|
-
{k: p for k, p in inspect.signature(func).parameters.items() if k != "self"},
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
signature_params: dict[str, inspect.Parameter] = getattr(func, "_signature_params")
|
|
224
|
-
params: dict[str, typing.Any] = {}
|
|
225
|
-
index = 0
|
|
226
|
-
|
|
227
|
-
for k, p in signature_params.items():
|
|
228
|
-
if p.kind in (p.POSITIONAL_OR_KEYWORD, p.POSITIONAL_ONLY) and len(args) > index:
|
|
229
|
-
params[k] = args[index]
|
|
230
|
-
index += 1
|
|
231
|
-
continue
|
|
232
|
-
|
|
233
|
-
if p.kind in (p.VAR_KEYWORD, p.VAR_POSITIONAL):
|
|
234
|
-
params[k] = kwargs.copy() if p.kind is p.VAR_KEYWORD else args[index:]
|
|
235
|
-
continue
|
|
236
|
-
|
|
237
|
-
params[k] = kwargs.pop(k, p.default) if p.default is not p.empty else kwargs.pop(k)
|
|
238
|
-
|
|
239
|
-
return await executor(self, method_name, get_params(params))
|
|
240
|
-
|
|
241
|
-
inner.__shortcut__ = Shortcut( # type: ignore
|
|
242
|
-
method_name=method_name,
|
|
243
|
-
executor=executor,
|
|
244
|
-
custom_params=custom_params or set(),
|
|
245
|
-
)
|
|
246
|
-
return inner # type: ignore
|
|
247
|
-
|
|
248
|
-
return wrapper
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
@dataclasses.dataclass(slots=True, frozen=True)
|
|
252
|
-
class Shortcut:
|
|
253
|
-
method_name: str
|
|
254
|
-
executor: Executor | None = dataclasses.field(default=None, kw_only=True)
|
|
255
|
-
custom_params: set[str] = dataclasses.field(default_factory=lambda: set(), kw_only=True)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
__all__ = ("BaseCute", "Shortcut", "compose_method_params", "shortcut")
|
|
1
|
+
import dataclasses
|
|
2
|
+
import inspect
|
|
3
|
+
import typing
|
|
4
|
+
from functools import wraps
|
|
5
|
+
|
|
6
|
+
import msgspec
|
|
7
|
+
import typing_extensions
|
|
8
|
+
from fntypes.result import Result
|
|
9
|
+
|
|
10
|
+
from telegrinder.api.api import API
|
|
11
|
+
from telegrinder.model import Model, get_params
|
|
12
|
+
|
|
13
|
+
F = typing.TypeVar("F", bound=typing.Callable[..., typing.Any])
|
|
14
|
+
Cute = typing.TypeVar("Cute", bound="BaseCute")
|
|
15
|
+
Update = typing_extensions.TypeVar("Update", bound=Model)
|
|
16
|
+
CtxAPI = typing_extensions.TypeVar("CtxAPI", bound=API, default=API)
|
|
17
|
+
|
|
18
|
+
Executor: typing.TypeAlias = typing.Callable[
|
|
19
|
+
[Cute, str, dict[str, typing.Any]],
|
|
20
|
+
typing.Awaitable[Result[typing.Any, typing.Any]],
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
if typing.TYPE_CHECKING:
|
|
24
|
+
|
|
25
|
+
class BaseCute(Model, typing.Generic[Update, CtxAPI]):
|
|
26
|
+
api: API
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_update(cls, update: Update, bound_api: API) -> typing.Self: ...
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def ctx_api(self) -> CtxAPI: ...
|
|
33
|
+
|
|
34
|
+
def to_dict(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
exclude_fields: set[str] | None = None,
|
|
38
|
+
) -> dict[str, typing.Any]:
|
|
39
|
+
"""
|
|
40
|
+
:param exclude_fields: Cute model field names to exclude from the dictionary representation of this cute model.
|
|
41
|
+
:return: A dictionary representation of this cute model.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
def to_full_dict(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
exclude_fields: set[str] | None = None,
|
|
50
|
+
) -> dict[str, typing.Any]:
|
|
51
|
+
"""
|
|
52
|
+
:param exclude_fields: Cute model field names to exclude from the dictionary representation of this cute model.
|
|
53
|
+
:return: A dictionary representation of this model including all models, structs, custom types.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
else:
|
|
59
|
+
import msgspec
|
|
60
|
+
from fntypes.co import Nothing, Some, Variative
|
|
61
|
+
|
|
62
|
+
from telegrinder.msgspec_utils import Option, decoder, encoder
|
|
63
|
+
from telegrinder.msgspec_utils import get_class_annotations as _get_class_annotations
|
|
64
|
+
|
|
65
|
+
def _get_cute_from_generic(generic_args):
|
|
66
|
+
for arg in generic_args:
|
|
67
|
+
orig_arg = typing.get_origin(arg) or arg
|
|
68
|
+
|
|
69
|
+
if not isinstance(orig_arg, type):
|
|
70
|
+
continue
|
|
71
|
+
if orig_arg in (Variative, Some, Option):
|
|
72
|
+
return _get_cute_from_generic(typing.get_args(arg))
|
|
73
|
+
if issubclass(arg, BaseCute):
|
|
74
|
+
return arg
|
|
75
|
+
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
def _get_cute_annotations(annotations):
|
|
79
|
+
cute_annotations = {}
|
|
80
|
+
|
|
81
|
+
for key, hint in annotations.items():
|
|
82
|
+
if not isinstance(hint, type):
|
|
83
|
+
if (cute := _get_cute_from_generic(typing.get_args(hint))) is not None:
|
|
84
|
+
cute_annotations[key] = cute
|
|
85
|
+
|
|
86
|
+
elif issubclass(hint, BaseCute):
|
|
87
|
+
cute_annotations[key] = hint
|
|
88
|
+
|
|
89
|
+
return cute_annotations
|
|
90
|
+
|
|
91
|
+
def _get_value(value):
|
|
92
|
+
while isinstance(value, Variative | Some):
|
|
93
|
+
if isinstance(value, Variative):
|
|
94
|
+
value = value.v
|
|
95
|
+
if isinstance(value, Some):
|
|
96
|
+
value = value.value
|
|
97
|
+
return value
|
|
98
|
+
|
|
99
|
+
class BaseCute(typing.Generic[Update, CtxAPI]):
|
|
100
|
+
def __init_subclass__(cls, *args, **kwargs):
|
|
101
|
+
super().__init_subclass__(*args, **kwargs)
|
|
102
|
+
|
|
103
|
+
if not cls.__bases__ or not issubclass(cls.__bases__[0], BaseCute):
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
cls.__is_solved_annotations__ = False
|
|
107
|
+
cls.__cute_annotations__ = None
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def from_update(cls, update, bound_api):
|
|
111
|
+
if not cls.__is_solved_annotations__:
|
|
112
|
+
cls.__is_solved_annotations__ = True
|
|
113
|
+
cls.__annotations__ = _get_class_annotations(cls)
|
|
114
|
+
|
|
115
|
+
if cls.__cute_annotations__ is None:
|
|
116
|
+
cls.__cute_annotations__ = _get_cute_annotations(cls.__annotations__)
|
|
117
|
+
|
|
118
|
+
return cls(
|
|
119
|
+
**{
|
|
120
|
+
field: decoder.convert(
|
|
121
|
+
cls.__cute_annotations__[field].from_update(_get_value(value), bound_api=bound_api),
|
|
122
|
+
type=cls.__annotations__[field],
|
|
123
|
+
)
|
|
124
|
+
if field in cls.__cute_annotations__ and not isinstance(value, Nothing)
|
|
125
|
+
else value
|
|
126
|
+
for field, value in update.to_dict().items()
|
|
127
|
+
},
|
|
128
|
+
api=bound_api,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def ctx_api(self):
|
|
133
|
+
return self.api
|
|
134
|
+
|
|
135
|
+
def _to_dict(self, dct_name, exclude_fields, full):
|
|
136
|
+
if dct_name not in self.__dict__:
|
|
137
|
+
self.__dict__[dct_name] = (
|
|
138
|
+
msgspec.structs.asdict(self)
|
|
139
|
+
if not full
|
|
140
|
+
else encoder.to_builtins(
|
|
141
|
+
{
|
|
142
|
+
k: field.to_dict(exclude_fields=exclude_fields)
|
|
143
|
+
if isinstance(field := _get_value(getattr(self, k)), BaseCute)
|
|
144
|
+
else field
|
|
145
|
+
for k in self.__struct_fields__
|
|
146
|
+
if k not in exclude_fields
|
|
147
|
+
},
|
|
148
|
+
order="deterministic",
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if not exclude_fields:
|
|
153
|
+
return self.__dict__[dct_name]
|
|
154
|
+
|
|
155
|
+
return {key: value for key, value in self.__dict__[dct_name].items() if key not in exclude_fields}
|
|
156
|
+
|
|
157
|
+
def to_dict(self, *, exclude_fields=None):
|
|
158
|
+
exclude_fields = exclude_fields or set()
|
|
159
|
+
return self._to_dict("model_as_dict", exclude_fields={"api"} | exclude_fields, full=False)
|
|
160
|
+
|
|
161
|
+
def to_full_dict(self, *, exclude_fields=None):
|
|
162
|
+
exclude_fields = exclude_fields or set()
|
|
163
|
+
return self._to_dict("model_as_full_dict", exclude_fields={"api"} | exclude_fields, full=True)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def compose_method_params(
|
|
167
|
+
params: dict[str, typing.Any],
|
|
168
|
+
update: Cute,
|
|
169
|
+
*,
|
|
170
|
+
default_params: set[str | tuple[str, str]] | None = None,
|
|
171
|
+
validators: dict[str, typing.Callable[[Cute], bool]] | None = None,
|
|
172
|
+
) -> dict[str, typing.Any]:
|
|
173
|
+
"""Compose method `params` from `update` by `default_params` and `validators`.
|
|
174
|
+
|
|
175
|
+
:param params: Method params.
|
|
176
|
+
:param update: Update object.
|
|
177
|
+
:param default_params: Default params. \
|
|
178
|
+
(`str`) - Attribute name to be get from `update` if param is undefined. \
|
|
179
|
+
(`tuple[str, str]`): tuple[0] - Parameter name to be set in `params`, \
|
|
180
|
+
tuple[1] - attribute name to be get from `update`.
|
|
181
|
+
:param validators: Validators mapping (`str, Callable`), key - `Parameter name` \
|
|
182
|
+
for which the validator will be applied, value - `Validator`, if returned `True` \
|
|
183
|
+
parameter will be set, otherwise will not.
|
|
184
|
+
:return: Composed params.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
default_params = default_params or set()
|
|
188
|
+
validators = validators or {}
|
|
189
|
+
|
|
190
|
+
for param in default_params:
|
|
191
|
+
param_name = param if isinstance(param, str) else param[0]
|
|
192
|
+
if param_name not in params:
|
|
193
|
+
if param_name in validators and not validators[param_name](update):
|
|
194
|
+
continue
|
|
195
|
+
params[param_name] = getattr(update, param if isinstance(param, str) else param[1])
|
|
196
|
+
|
|
197
|
+
return params
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def shortcut(
|
|
201
|
+
method_name: str,
|
|
202
|
+
*,
|
|
203
|
+
executor: Executor[Cute] | None = None,
|
|
204
|
+
custom_params: set[str] | None = None,
|
|
205
|
+
):
|
|
206
|
+
def wrapper(func: F) -> F:
|
|
207
|
+
@wraps(func)
|
|
208
|
+
async def inner(
|
|
209
|
+
self: Cute,
|
|
210
|
+
*args: typing.Any,
|
|
211
|
+
**kwargs: typing.Any,
|
|
212
|
+
) -> typing.Any:
|
|
213
|
+
if executor is None:
|
|
214
|
+
return await func(self, *args, **kwargs)
|
|
215
|
+
|
|
216
|
+
if not hasattr(func, "_signature_params"):
|
|
217
|
+
setattr(
|
|
218
|
+
func,
|
|
219
|
+
"_signature_params",
|
|
220
|
+
{k: p for k, p in inspect.signature(func).parameters.items() if k != "self"},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
signature_params: dict[str, inspect.Parameter] = getattr(func, "_signature_params")
|
|
224
|
+
params: dict[str, typing.Any] = {}
|
|
225
|
+
index = 0
|
|
226
|
+
|
|
227
|
+
for k, p in signature_params.items():
|
|
228
|
+
if p.kind in (p.POSITIONAL_OR_KEYWORD, p.POSITIONAL_ONLY) and len(args) > index:
|
|
229
|
+
params[k] = args[index]
|
|
230
|
+
index += 1
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
if p.kind in (p.VAR_KEYWORD, p.VAR_POSITIONAL):
|
|
234
|
+
params[k] = kwargs.copy() if p.kind is p.VAR_KEYWORD else args[index:]
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
params[k] = kwargs.pop(k, p.default) if p.default is not p.empty else kwargs.pop(k)
|
|
238
|
+
|
|
239
|
+
return await executor(self, method_name, get_params(params))
|
|
240
|
+
|
|
241
|
+
inner.__shortcut__ = Shortcut( # type: ignore
|
|
242
|
+
method_name=method_name,
|
|
243
|
+
executor=executor,
|
|
244
|
+
custom_params=custom_params or set(),
|
|
245
|
+
)
|
|
246
|
+
return inner # type: ignore
|
|
247
|
+
|
|
248
|
+
return wrapper
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@dataclasses.dataclass(slots=True, frozen=True)
|
|
252
|
+
class Shortcut:
|
|
253
|
+
method_name: str
|
|
254
|
+
executor: Executor | None = dataclasses.field(default=None, kw_only=True)
|
|
255
|
+
custom_params: set[str] = dataclasses.field(default_factory=lambda: set(), kw_only=True)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
__all__ = ("BaseCute", "Shortcut", "compose_method_params", "shortcut")
|