telegrinder 0.1.dev168__py3-none-any.whl → 0.1.dev170__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 +9 -3
- telegrinder/bot/__init__.py +7 -5
- telegrinder/bot/cute_types/base.py +12 -14
- telegrinder/bot/cute_types/callback_query.py +54 -43
- telegrinder/bot/cute_types/chat_join_request.py +8 -7
- telegrinder/bot/cute_types/chat_member_updated.py +23 -17
- telegrinder/bot/cute_types/inline_query.py +1 -1
- telegrinder/bot/cute_types/message.py +331 -183
- telegrinder/bot/cute_types/update.py +4 -8
- telegrinder/bot/cute_types/utils.py +1 -5
- telegrinder/bot/dispatch/__init__.py +2 -3
- telegrinder/bot/dispatch/abc.py +4 -0
- telegrinder/bot/dispatch/context.py +9 -4
- telegrinder/bot/dispatch/dispatch.py +35 -33
- telegrinder/bot/dispatch/handler/func.py +34 -13
- telegrinder/bot/dispatch/handler/message_reply.py +6 -3
- telegrinder/bot/dispatch/middleware/abc.py +4 -4
- telegrinder/bot/dispatch/process.py +40 -13
- telegrinder/bot/dispatch/return_manager/abc.py +12 -12
- telegrinder/bot/dispatch/return_manager/callback_query.py +1 -3
- telegrinder/bot/dispatch/return_manager/inline_query.py +1 -3
- telegrinder/bot/dispatch/view/abc.py +37 -45
- telegrinder/bot/dispatch/view/box.py +66 -50
- telegrinder/bot/dispatch/view/message.py +1 -5
- telegrinder/bot/dispatch/view/raw.py +6 -6
- telegrinder/bot/dispatch/waiter_machine/__init__.py +2 -1
- telegrinder/bot/dispatch/waiter_machine/machine.py +77 -35
- telegrinder/bot/dispatch/waiter_machine/middleware.py +31 -7
- telegrinder/bot/dispatch/waiter_machine/short_state.py +17 -8
- telegrinder/bot/polling/polling.py +4 -4
- telegrinder/bot/rules/__init__.py +9 -6
- telegrinder/bot/rules/abc.py +99 -22
- telegrinder/bot/rules/adapter/__init__.py +4 -1
- telegrinder/bot/rules/adapter/abc.py +11 -6
- telegrinder/bot/rules/adapter/errors.py +1 -2
- telegrinder/bot/rules/adapter/event.py +14 -9
- telegrinder/bot/rules/adapter/node.py +42 -0
- telegrinder/bot/rules/callback_data.py +4 -4
- telegrinder/bot/rules/chat_join.py +3 -2
- telegrinder/bot/rules/command.py +26 -14
- telegrinder/bot/rules/enum_text.py +5 -5
- telegrinder/bot/rules/func.py +6 -6
- telegrinder/bot/rules/fuzzy.py +5 -7
- telegrinder/bot/rules/inline.py +4 -5
- telegrinder/bot/rules/integer.py +10 -8
- telegrinder/bot/rules/is_from.py +63 -91
- telegrinder/bot/rules/markup.py +5 -5
- telegrinder/bot/rules/mention.py +4 -4
- telegrinder/bot/rules/message.py +1 -1
- telegrinder/bot/rules/node.py +27 -0
- telegrinder/bot/rules/regex.py +5 -5
- telegrinder/bot/rules/rule_enum.py +4 -4
- telegrinder/bot/rules/start.py +5 -5
- telegrinder/bot/rules/text.py +9 -13
- telegrinder/bot/rules/update.py +4 -4
- telegrinder/bot/scenario/__init__.py +3 -3
- telegrinder/bot/scenario/choice.py +2 -3
- telegrinder/model.py +51 -16
- telegrinder/modules.py +14 -6
- telegrinder/msgspec_utils.py +67 -23
- telegrinder/node/__init__.py +26 -8
- telegrinder/node/attachment.py +13 -9
- telegrinder/node/base.py +27 -14
- telegrinder/node/callback_query.py +18 -0
- telegrinder/node/command.py +29 -0
- telegrinder/node/composer.py +119 -30
- telegrinder/node/me.py +14 -0
- telegrinder/node/message.py +2 -4
- telegrinder/node/polymorphic.py +44 -0
- telegrinder/node/rule.py +26 -22
- telegrinder/node/scope.py +36 -0
- telegrinder/node/source.py +37 -10
- telegrinder/node/text.py +11 -5
- telegrinder/node/tools/__init__.py +2 -2
- telegrinder/node/tools/generator.py +6 -6
- telegrinder/tools/__init__.py +8 -13
- telegrinder/tools/buttons.py +23 -17
- telegrinder/tools/error_handler/error_handler.py +11 -14
- telegrinder/tools/formatting/__init__.py +0 -6
- telegrinder/tools/formatting/html.py +10 -12
- telegrinder/tools/formatting/links.py +0 -5
- telegrinder/tools/formatting/spec_html_formats.py +0 -11
- telegrinder/tools/global_context/abc.py +1 -3
- telegrinder/tools/global_context/global_context.py +6 -16
- telegrinder/tools/i18n/simple.py +1 -3
- telegrinder/tools/kb_set/yaml.py +1 -2
- telegrinder/tools/keyboard.py +7 -8
- telegrinder/tools/limited_dict.py +1 -1
- telegrinder/tools/loop_wrapper/loop_wrapper.py +6 -5
- telegrinder/tools/magic.py +27 -5
- telegrinder/types/__init__.py +20 -0
- telegrinder/types/enums.py +37 -31
- telegrinder/types/methods.py +608 -327
- telegrinder/types/objects.py +1139 -716
- {telegrinder-0.1.dev168.dist-info → telegrinder-0.1.dev170.dist-info}/LICENSE +1 -1
- {telegrinder-0.1.dev168.dist-info → telegrinder-0.1.dev170.dist-info}/METADATA +6 -5
- telegrinder-0.1.dev170.dist-info/RECORD +143 -0
- telegrinder/bot/dispatch/composition.py +0 -88
- telegrinder-0.1.dev168.dist-info/RECORD +0 -137
- {telegrinder-0.1.dev168.dist-info → telegrinder-0.1.dev170.dist-info}/WHEEL +0 -0
telegrinder/bot/rules/text.py
CHANGED
|
@@ -1,28 +1,24 @@
|
|
|
1
|
+
from telegrinder import node
|
|
1
2
|
from telegrinder.bot.dispatch.context import Context
|
|
2
3
|
from telegrinder.tools.i18n.base import ABCTranslator
|
|
3
4
|
|
|
4
|
-
from .abc import
|
|
5
|
-
from .
|
|
5
|
+
from .abc import ABCRule, with_caching_translations
|
|
6
|
+
from .node import NodeRule
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
class HasText(
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
class HasText(NodeRule):
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
super().__init__(node.text.Text)
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
class
|
|
14
|
-
pass
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class Text(TextMessageRule):
|
|
14
|
+
class Text(ABCRule):
|
|
18
15
|
def __init__(self, texts: str | list[str], *, ignore_case: bool = False) -> None:
|
|
19
16
|
if not isinstance(texts, list):
|
|
20
17
|
texts = [texts]
|
|
21
18
|
self.texts = texts if not ignore_case else list(map(str.lower, texts))
|
|
22
19
|
self.ignore_case = ignore_case
|
|
23
20
|
|
|
24
|
-
async def check(self,
|
|
25
|
-
text = message.text.unwrap()
|
|
21
|
+
async def check(self, text: node.text.Text, ctx: Context) -> bool:
|
|
26
22
|
return (text if not self.ignore_case else text.lower()) in self.texts
|
|
27
23
|
|
|
28
24
|
@with_caching_translations
|
|
@@ -33,4 +29,4 @@ class Text(TextMessageRule):
|
|
|
33
29
|
)
|
|
34
30
|
|
|
35
31
|
|
|
36
|
-
__all__ = ("HasText", "Text"
|
|
32
|
+
__all__ = ("HasText", "Text")
|
telegrinder/bot/rules/update.py
CHANGED
|
@@ -2,15 +2,15 @@ from telegrinder.bot.cute_types.update import UpdateCute
|
|
|
2
2
|
from telegrinder.bot.dispatch.context import Context
|
|
3
3
|
from telegrinder.types.enums import UpdateType
|
|
4
4
|
|
|
5
|
-
from .abc import ABCRule
|
|
5
|
+
from .abc import ABCRule
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
class
|
|
8
|
+
class IsUpdateType(ABCRule):
|
|
9
9
|
def __init__(self, update_type: UpdateType, /) -> None:
|
|
10
10
|
self.update_type = update_type
|
|
11
11
|
|
|
12
12
|
async def check(self, event: UpdateCute, ctx: Context) -> bool:
|
|
13
|
-
return event.update_type
|
|
13
|
+
return event.update_type == self.update_type
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
__all__ = ("
|
|
16
|
+
__all__ = ("IsUpdateType",)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from .abc import ABCScenario
|
|
2
|
-
from .checkbox import Checkbox
|
|
3
|
-
from .choice import
|
|
2
|
+
from .checkbox import Checkbox
|
|
3
|
+
from .choice import Choice
|
|
4
4
|
|
|
5
|
-
__all__ = ("ABCScenario", "Checkbox", "Choice"
|
|
5
|
+
__all__ = ("ABCScenario", "Checkbox", "Choice")
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
3
|
from telegrinder.bot.cute_types import CallbackQueryCute
|
|
4
|
-
from telegrinder.bot.dispatch.waiter_machine import WaiterMachine
|
|
5
4
|
|
|
6
5
|
from .checkbox import Checkbox
|
|
7
6
|
|
|
@@ -10,7 +9,7 @@ if typing.TYPE_CHECKING:
|
|
|
10
9
|
from telegrinder.bot.dispatch.view.abc import BaseStateView
|
|
11
10
|
|
|
12
11
|
|
|
13
|
-
class
|
|
12
|
+
class Choice(Checkbox):
|
|
14
13
|
async def handle(self, cb: CallbackQueryCute) -> bool:
|
|
15
14
|
code = cb.data.unwrap().replace(self.random_code + "/", "", 1)
|
|
16
15
|
if code == "ready":
|
|
@@ -43,4 +42,4 @@ class SingleChoice(Checkbox):
|
|
|
43
42
|
return tuple(choices.keys())[tuple(choices.values()).index(True)], m_id
|
|
44
43
|
|
|
45
44
|
|
|
46
|
-
__all__ = ("
|
|
45
|
+
__all__ = ("Choice",)
|
telegrinder/model.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import enum
|
|
3
|
+
import keyword
|
|
3
4
|
import secrets
|
|
4
5
|
import typing
|
|
5
6
|
from datetime import datetime
|
|
@@ -19,7 +20,7 @@ T = typing.TypeVar("T")
|
|
|
19
20
|
MODEL_CONFIG: typing.Final[dict[str, typing.Any]] = {
|
|
20
21
|
"omit_defaults": True,
|
|
21
22
|
"dict": True,
|
|
22
|
-
"rename": {"
|
|
23
|
+
"rename": {kw + "_": kw for kw in keyword.kwlist},
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
|
|
@@ -45,14 +46,17 @@ def full_result(
|
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
def get_params(params: dict[str, typing.Any]) -> dict[str, typing.Any]:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
validated_params = {}
|
|
50
|
+
for k, v in (
|
|
51
|
+
*params.pop("other", {}).items(),
|
|
52
|
+
*params.items(),
|
|
53
|
+
):
|
|
54
|
+
if isinstance(v, Proxy):
|
|
55
|
+
v = v.get()
|
|
56
|
+
if k == "self" or type(v) in (NoneType, Nothing):
|
|
57
|
+
continue
|
|
58
|
+
validated_params[k] = v.unwrap() if isinstance(v, Some) else v
|
|
59
|
+
return validated_params
|
|
56
60
|
|
|
57
61
|
|
|
58
62
|
class Model(msgspec.Struct, **MODEL_CONFIG):
|
|
@@ -69,9 +73,7 @@ class Model(msgspec.Struct, **MODEL_CONFIG):
|
|
|
69
73
|
if "model_as_dict" not in self.__dict__:
|
|
70
74
|
self.__dict__["model_as_dict"] = msgspec.structs.asdict(self)
|
|
71
75
|
return {
|
|
72
|
-
key: value
|
|
73
|
-
for key, value in self.__dict__["model_as_dict"].items()
|
|
74
|
-
if key not in exclude_fields
|
|
76
|
+
key: value for key, value in self.__dict__["model_as_dict"].items() if key not in exclude_fields
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
|
|
@@ -82,7 +84,7 @@ class DataConverter:
|
|
|
82
84
|
def __repr__(self) -> str:
|
|
83
85
|
return "<{}: {}>".format(
|
|
84
86
|
self.__class__.__name__,
|
|
85
|
-
", ".join(f"{k}={v!r}" for k, v in self.converters),
|
|
87
|
+
", ".join(f"{k}={v.__name__!r}" for k, v in self.converters.items()),
|
|
86
88
|
)
|
|
87
89
|
|
|
88
90
|
@property
|
|
@@ -129,9 +131,7 @@ class DataConverter:
|
|
|
129
131
|
serialize: bool = True,
|
|
130
132
|
) -> dict[str, typing.Any]:
|
|
131
133
|
return {
|
|
132
|
-
k: self(v, serialize=serialize)
|
|
133
|
-
for k, v in data.items()
|
|
134
|
-
if type(v) not in (NoneType, Nothing)
|
|
134
|
+
k: self(v, serialize=serialize) for k, v in data.items() if type(v) not in (NoneType, Nothing)
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
def convert_lst(
|
|
@@ -159,8 +159,43 @@ class DataConverter:
|
|
|
159
159
|
return data
|
|
160
160
|
|
|
161
161
|
|
|
162
|
+
class Proxy:
|
|
163
|
+
def __init__(self, cfg: "_ProxiedDict", key: str):
|
|
164
|
+
self.key = key
|
|
165
|
+
self.cfg = cfg
|
|
166
|
+
|
|
167
|
+
def get(self) -> typing.Any | None:
|
|
168
|
+
return self.cfg._defaults.get(self.key)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class _ProxiedDict(typing.Generic[T]):
|
|
172
|
+
def __init__(self, tp: type[T]) -> None:
|
|
173
|
+
self.type = tp
|
|
174
|
+
self._defaults = {}
|
|
175
|
+
|
|
176
|
+
def __setattribute__(self, name: str, value: typing.Any, /) -> None:
|
|
177
|
+
self._defaults[name] = value
|
|
178
|
+
|
|
179
|
+
def __getitem__(self, key: str, /) -> None:
|
|
180
|
+
return Proxy(self, key) # type: ignore
|
|
181
|
+
|
|
182
|
+
def __setitem__(self, key: str, value: typing.Any, /) -> None:
|
|
183
|
+
self._defaults[key] = value
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if typing.TYPE_CHECKING:
|
|
187
|
+
|
|
188
|
+
def ProxiedDict(typed_dct: type[T]) -> T | _ProxiedDict[T]: # noqa: N802
|
|
189
|
+
...
|
|
190
|
+
|
|
191
|
+
else:
|
|
192
|
+
ProxiedDict = _ProxiedDict
|
|
193
|
+
|
|
194
|
+
|
|
162
195
|
__all__ = (
|
|
196
|
+
"Proxy",
|
|
163
197
|
"DataConverter",
|
|
198
|
+
"ProxiedDict",
|
|
164
199
|
"MODEL_CONFIG",
|
|
165
200
|
"Model",
|
|
166
201
|
"full_result",
|
telegrinder/modules.py
CHANGED
|
@@ -40,10 +40,13 @@ class LoggerModule(typing.Protocol):
|
|
|
40
40
|
|
|
41
41
|
logger: LoggerModule
|
|
42
42
|
json: JSONModule = choice_in_order(
|
|
43
|
-
["orjson", "ujson", "hyperjson"],
|
|
43
|
+
["orjson", "ujson", "hyperjson"],
|
|
44
|
+
default="telegrinder.msgspec_json",
|
|
45
|
+
do_import=True,
|
|
44
46
|
)
|
|
45
|
-
logging_module = choice_in_order(["loguru"], default="logging")
|
|
46
47
|
logging_level = os.getenv("LOGGER_LEVEL", default="DEBUG").upper()
|
|
48
|
+
logging_module = choice_in_order(["loguru"], default="logging")
|
|
49
|
+
asyncio_module = choice_in_order(["uvloop"], default="asyncio")
|
|
47
50
|
|
|
48
51
|
if logging_module == "loguru":
|
|
49
52
|
import os
|
|
@@ -201,9 +204,7 @@ elif logging_module == "logging":
|
|
|
201
204
|
kwargs: dict[str, object],
|
|
202
205
|
) -> tuple[LogMessage | object, tuple[object, ...], dict[str, object]]:
|
|
203
206
|
log_kwargs = {
|
|
204
|
-
key: kwargs[key]
|
|
205
|
-
for key in inspect.getfullargspec(self.logger._log).args[1:]
|
|
206
|
-
if key in kwargs
|
|
207
|
+
key: kwargs[key] for key in inspect.getfullargspec(self.logger._log).args[1:] if key in kwargs
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
if isinstance(msg, str):
|
|
@@ -214,10 +215,17 @@ elif logging_module == "logging":
|
|
|
214
215
|
handler = logging.StreamHandler(sys.stderr)
|
|
215
216
|
handler.setFormatter(TelegrinderLoggingFormatter())
|
|
216
217
|
logger = logging.getLogger("telegrinder") # type: ignore
|
|
217
|
-
logger.setLevel(
|
|
218
|
+
logger.setLevel(logging_level) # type: ignore
|
|
218
219
|
logger.addHandler(handler) # type: ignore
|
|
219
220
|
logger = TelegrinderLoggingStyleAdapter(logger) # type: ignore
|
|
220
221
|
|
|
222
|
+
if asyncio_module == "uvloop":
|
|
223
|
+
import asyncio
|
|
224
|
+
|
|
225
|
+
import uvloop # type: ignore
|
|
226
|
+
|
|
227
|
+
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) # type: ignore
|
|
228
|
+
|
|
221
229
|
|
|
222
230
|
def _set_logger_level(level):
|
|
223
231
|
level = level.upper()
|
telegrinder/msgspec_utils.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
3
|
import fntypes.option
|
|
4
|
+
import fntypes.result
|
|
4
5
|
import msgspec
|
|
5
6
|
from fntypes.co import Error, Ok, Result, Variative
|
|
6
7
|
|
|
@@ -8,10 +9,12 @@ if typing.TYPE_CHECKING:
|
|
|
8
9
|
from datetime import datetime
|
|
9
10
|
|
|
10
11
|
from fntypes.option import Option
|
|
12
|
+
from fntypes.result import Result
|
|
11
13
|
else:
|
|
12
14
|
from datetime import datetime as dt
|
|
13
15
|
|
|
14
16
|
Value = typing.TypeVar("Value")
|
|
17
|
+
Err = typing.TypeVar("Err")
|
|
15
18
|
|
|
16
19
|
datetime = type("datetime", (dt,), {})
|
|
17
20
|
|
|
@@ -19,14 +22,22 @@ else:
|
|
|
19
22
|
def __instancecheck__(cls, __instance: typing.Any) -> bool:
|
|
20
23
|
return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
|
|
21
24
|
|
|
25
|
+
class ResultMeta(type):
|
|
26
|
+
def __instancecheck__(cls, __instance: typing.Any) -> bool:
|
|
27
|
+
return isinstance(__instance, fntypes.result.Ok | fntypes.result.Error)
|
|
28
|
+
|
|
22
29
|
class Option(typing.Generic[Value], metaclass=OptionMeta):
|
|
23
30
|
pass
|
|
24
31
|
|
|
32
|
+
class Result(typing.Generic[Value, Err], metaclass=ResultMeta):
|
|
33
|
+
pass
|
|
34
|
+
|
|
25
35
|
|
|
26
36
|
T = typing.TypeVar("T")
|
|
37
|
+
Type = typing.TypeVar("Type", bound=type | typing.Any)
|
|
27
38
|
Ts = typing.TypeVarTuple("Ts")
|
|
28
39
|
|
|
29
|
-
DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any],
|
|
40
|
+
DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any], typing.Any]
|
|
30
41
|
EncHook: typing.TypeAlias = typing.Callable[[T], typing.Any]
|
|
31
42
|
|
|
32
43
|
Nothing: typing.Final[fntypes.option.Nothing] = fntypes.option.Nothing()
|
|
@@ -40,18 +51,52 @@ def repr_type(t: type) -> str:
|
|
|
40
51
|
return getattr(t, "__name__", repr(get_origin(t)))
|
|
41
52
|
|
|
42
53
|
|
|
43
|
-
def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T,
|
|
54
|
+
def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, str]:
|
|
44
55
|
try:
|
|
45
56
|
return Ok(decoder.convert(obj, type=t, strict=True))
|
|
46
|
-
except msgspec.ValidationError
|
|
47
|
-
return Error(
|
|
57
|
+
except msgspec.ValidationError:
|
|
58
|
+
return Error(
|
|
59
|
+
"Expected object of type `{}`, got `{}`.".format(
|
|
60
|
+
repr_type(t),
|
|
61
|
+
repr_type(type(obj)),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
48
64
|
|
|
49
65
|
|
|
50
66
|
def option_dec_hook(tp: type[Option[typing.Any]], obj: typing.Any) -> Option[typing.Any]:
|
|
51
|
-
|
|
52
|
-
return Nothing
|
|
67
|
+
orig_type = get_origin(tp)
|
|
53
68
|
(value_type,) = typing.get_args(tp) or (typing.Any,)
|
|
54
|
-
|
|
69
|
+
|
|
70
|
+
if obj is None and orig_type in (fntypes.option.Nothing, Option):
|
|
71
|
+
return fntypes.option.Nothing()
|
|
72
|
+
return fntypes.option.Some(msgspec_convert(obj, value_type).unwrap())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def result_dec_hook(
|
|
76
|
+
tp: type[Result[typing.Any, typing.Any]], obj: typing.Any
|
|
77
|
+
) -> Result[typing.Any, typing.Any]:
|
|
78
|
+
if not isinstance(obj, dict):
|
|
79
|
+
raise TypeError(f"Cannot parse to Result object of type `{repr_type(type(obj))}`.")
|
|
80
|
+
|
|
81
|
+
orig_type = get_origin(tp)
|
|
82
|
+
(first_type, second_type) = (
|
|
83
|
+
typing.get_args(tp) + (typing.Any,) if len(typing.get_args(tp)) == 1 else typing.get_args(tp)
|
|
84
|
+
) or (typing.Any, typing.Any)
|
|
85
|
+
|
|
86
|
+
if orig_type is Ok and "ok" in obj:
|
|
87
|
+
return Ok(msgspec_convert(obj["ok"], first_type).unwrap())
|
|
88
|
+
|
|
89
|
+
if orig_type is Error and "error" in obj:
|
|
90
|
+
return Error(msgspec_convert(obj["error"], first_type).unwrap())
|
|
91
|
+
|
|
92
|
+
if orig_type is Result:
|
|
93
|
+
match obj:
|
|
94
|
+
case {"ok": ok}:
|
|
95
|
+
return Ok(msgspec_convert(ok, first_type).unwrap())
|
|
96
|
+
case {"error": error}:
|
|
97
|
+
return Error(msgspec_convert(error, second_type).unwrap())
|
|
98
|
+
|
|
99
|
+
raise msgspec.ValidationError(f"Cannot parse object `{obj!r}` to Result.")
|
|
55
100
|
|
|
56
101
|
|
|
57
102
|
def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
|
|
@@ -67,9 +112,7 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
|
|
|
67
112
|
reverse = False
|
|
68
113
|
|
|
69
114
|
if len(set(struct_fields_match_sums.values())) != len(struct_fields_match_sums.values()):
|
|
70
|
-
struct_fields_match_sums = {
|
|
71
|
-
m: len(m.__struct_fields__) for m in struct_fields_match_sums
|
|
72
|
-
}
|
|
115
|
+
struct_fields_match_sums = {m: len(m.__struct_fields__) for m in struct_fields_match_sums}
|
|
73
116
|
reverse = True
|
|
74
117
|
|
|
75
118
|
union_types = (
|
|
@@ -123,9 +166,14 @@ class Decoder:
|
|
|
123
166
|
|
|
124
167
|
def __init__(self) -> None:
|
|
125
168
|
self.dec_hooks: dict[typing.Any, DecHook[typing.Any]] = {
|
|
169
|
+
Result: result_dec_hook,
|
|
126
170
|
Option: option_dec_hook,
|
|
127
171
|
Variative: variative_dec_hook,
|
|
128
172
|
datetime: lambda t, obj: t.fromtimestamp(obj),
|
|
173
|
+
fntypes.result.Error: result_dec_hook,
|
|
174
|
+
fntypes.result.Ok: result_dec_hook,
|
|
175
|
+
fntypes.option.Some: option_dec_hook,
|
|
176
|
+
fntypes.option.Nothing: option_dec_hook,
|
|
129
177
|
}
|
|
130
178
|
|
|
131
179
|
def __repr__(self) -> str:
|
|
@@ -144,8 +192,7 @@ class Decoder:
|
|
|
144
192
|
origin_type = t if isinstance((t := get_origin(tp)), type) else type(t)
|
|
145
193
|
if origin_type not in self.dec_hooks:
|
|
146
194
|
raise TypeError(
|
|
147
|
-
f"Unknown type `{repr_type(origin_type)}`. "
|
|
148
|
-
"You can implement decode hook for this type."
|
|
195
|
+
f"Unknown type `{repr_type(origin_type)}`. You can implement decode hook for this type."
|
|
149
196
|
)
|
|
150
197
|
return self.dec_hooks[origin_type](tp, obj)
|
|
151
198
|
|
|
@@ -173,7 +220,7 @@ class Decoder:
|
|
|
173
220
|
def decode(self, buf: str | bytes) -> typing.Any: ...
|
|
174
221
|
|
|
175
222
|
@typing.overload
|
|
176
|
-
def decode(self, buf: str | bytes, *,
|
|
223
|
+
def decode(self, buf: str | bytes, *, type: type[T]) -> T: ...
|
|
177
224
|
|
|
178
225
|
@typing.overload
|
|
179
226
|
def decode(
|
|
@@ -184,16 +231,10 @@ class Decoder:
|
|
|
184
231
|
strict: bool = True,
|
|
185
232
|
) -> T: ...
|
|
186
233
|
|
|
187
|
-
def decode(
|
|
188
|
-
self,
|
|
189
|
-
buf: str | bytes,
|
|
190
|
-
*,
|
|
191
|
-
type: type[T] = typing.Any, # type: ignore
|
|
192
|
-
strict: bool = True,
|
|
193
|
-
) -> T:
|
|
234
|
+
def decode(self, buf, *, type=object, strict=True):
|
|
194
235
|
return msgspec.json.decode(
|
|
195
236
|
buf,
|
|
196
|
-
type=type,
|
|
237
|
+
type=typing.Any if type is object else type,
|
|
197
238
|
strict=strict,
|
|
198
239
|
dec_hook=self.dec_hook,
|
|
199
240
|
)
|
|
@@ -219,6 +260,10 @@ class Encoder:
|
|
|
219
260
|
self.enc_hooks: dict[typing.Any, EncHook[typing.Any]] = {
|
|
220
261
|
fntypes.option.Some: lambda opt: opt.value,
|
|
221
262
|
fntypes.option.Nothing: lambda _: None,
|
|
263
|
+
fntypes.result.Ok: lambda ok: {"ok": ok.value},
|
|
264
|
+
fntypes.result.Error: lambda err: {
|
|
265
|
+
"error": (str(err.error) if isinstance(err.error, BaseException) else err.error)
|
|
266
|
+
},
|
|
222
267
|
Variative: lambda variative: variative.v,
|
|
223
268
|
datetime: lambda date: int(date.timestamp()),
|
|
224
269
|
}
|
|
@@ -240,8 +285,7 @@ class Encoder:
|
|
|
240
285
|
origin_type = get_origin(obj.__class__)
|
|
241
286
|
if origin_type not in self.enc_hooks:
|
|
242
287
|
raise NotImplementedError(
|
|
243
|
-
"Not implemented encode hook for "
|
|
244
|
-
f"object of type `{repr_type(origin_type)}`."
|
|
288
|
+
f"Not implemented encode hook for object of type `{repr_type(origin_type)}`."
|
|
245
289
|
)
|
|
246
290
|
return self.enc_hooks[origin_type](obj)
|
|
247
291
|
|
telegrinder/node/__init__.py
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
from .attachment import Attachment, Audio, Photo, Video
|
|
2
|
-
from .base import ComposeError, DataNode, Node, ScalarNode
|
|
3
|
-
from .
|
|
2
|
+
from .base import ComposeError, DataNode, Node, ScalarNode, is_node
|
|
3
|
+
from .command import CommandInfo
|
|
4
|
+
from .composer import Composition, NodeCollection, NodeSession, compose_node, compose_nodes
|
|
4
5
|
from .container import ContainerNode
|
|
6
|
+
from .me import Me
|
|
5
7
|
from .message import MessageNode
|
|
6
|
-
from .rule import
|
|
7
|
-
from .
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
8
|
+
from .rule import RuleChain
|
|
9
|
+
from .scope import GLOBAL, PER_CALL, PER_EVENT, NodeScope, global_node, per_call, per_event
|
|
10
|
+
from .source import ChatSource, Source, UserSource
|
|
11
|
+
from .text import Text, TextInteger
|
|
12
|
+
from .tools import generate_node
|
|
10
13
|
from .update import UpdateNode
|
|
11
14
|
|
|
12
15
|
__all__ = (
|
|
13
16
|
"Attachment",
|
|
14
17
|
"Audio",
|
|
18
|
+
"ChatSource",
|
|
15
19
|
"ComposeError",
|
|
16
20
|
"ContainerNode",
|
|
17
21
|
"DataNode",
|
|
@@ -20,12 +24,26 @@ __all__ = (
|
|
|
20
24
|
"NodeCollection",
|
|
21
25
|
"NodeSession",
|
|
22
26
|
"Photo",
|
|
23
|
-
"
|
|
27
|
+
"RuleChain",
|
|
24
28
|
"ScalarNode",
|
|
25
29
|
"Source",
|
|
26
30
|
"Text",
|
|
31
|
+
"TextInteger",
|
|
32
|
+
"UserSource",
|
|
27
33
|
"UpdateNode",
|
|
28
34
|
"Video",
|
|
29
35
|
"compose_node",
|
|
30
|
-
"
|
|
36
|
+
"generate_node",
|
|
37
|
+
"Composition",
|
|
38
|
+
"is_node",
|
|
39
|
+
"compose_nodes",
|
|
40
|
+
"NodeScope",
|
|
41
|
+
"PER_CALL",
|
|
42
|
+
"PER_EVENT",
|
|
43
|
+
"per_call",
|
|
44
|
+
"per_event",
|
|
45
|
+
"CommandInfo",
|
|
46
|
+
"GLOBAL",
|
|
47
|
+
"global_node",
|
|
48
|
+
"Me",
|
|
31
49
|
)
|
telegrinder/node/attachment.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
|
+
from fntypes import Option, Some
|
|
4
5
|
from fntypes.option import Nothing
|
|
5
6
|
|
|
6
7
|
import telegrinder.types
|
|
7
|
-
from telegrinder.msgspec_utils import Option
|
|
8
8
|
|
|
9
9
|
from .base import ComposeError, DataNode, ScalarNode
|
|
10
10
|
from .message import MessageNode
|
|
@@ -13,22 +13,26 @@ from .message import MessageNode
|
|
|
13
13
|
@dataclasses.dataclass
|
|
14
14
|
class Attachment(DataNode):
|
|
15
15
|
attachment_type: typing.Literal["audio", "document", "photo", "poll", "video"]
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
audio: Option[telegrinder.types.Audio] = dataclasses.field(
|
|
17
|
+
default_factory=lambda: Nothing(), kw_only=True
|
|
18
|
+
)
|
|
18
19
|
document: Option[telegrinder.types.Document] = dataclasses.field(
|
|
19
|
-
default_factory=lambda: Nothing()
|
|
20
|
+
default_factory=lambda: Nothing(), kw_only=True
|
|
20
21
|
)
|
|
21
22
|
photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
|
|
22
|
-
default_factory=lambda: Nothing()
|
|
23
|
+
default_factory=lambda: Nothing(), kw_only=True
|
|
24
|
+
)
|
|
25
|
+
poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing(), kw_only=True)
|
|
26
|
+
video: Option[telegrinder.types.Video] = dataclasses.field(
|
|
27
|
+
default_factory=lambda: Nothing(), kw_only=True
|
|
23
28
|
)
|
|
24
|
-
poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing())
|
|
25
|
-
video: Option[telegrinder.types.Video] = dataclasses.field(default_factory=lambda: Nothing())
|
|
26
29
|
|
|
27
30
|
@classmethod
|
|
28
31
|
async def compose(cls, message: MessageNode) -> "Attachment":
|
|
29
32
|
for attachment_type in ("audio", "document", "photo", "poll", "video"):
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
match getattr(message, attachment_type, Nothing()):
|
|
34
|
+
case Some(attachment):
|
|
35
|
+
return cls(attachment_type, **{attachment_type: Some(attachment)})
|
|
32
36
|
return cls.compose_error("No attachment found in message")
|
|
33
37
|
|
|
34
38
|
|
telegrinder/node/base.py
CHANGED
|
@@ -2,8 +2,14 @@ import abc
|
|
|
2
2
|
import inspect
|
|
3
3
|
import typing
|
|
4
4
|
|
|
5
|
+
from telegrinder.tools.magic import get_annotations
|
|
6
|
+
|
|
7
|
+
from .scope import NodeScope
|
|
8
|
+
|
|
5
9
|
ComposeResult: typing.TypeAlias = (
|
|
6
|
-
typing.Coroutine[typing.Any, typing.Any, typing.Any]
|
|
10
|
+
typing.Coroutine[typing.Any, typing.Any, typing.Any]
|
|
11
|
+
| typing.AsyncGenerator[typing.Any, None]
|
|
12
|
+
| typing.Any
|
|
7
13
|
)
|
|
8
14
|
|
|
9
15
|
|
|
@@ -12,10 +18,11 @@ class ComposeError(BaseException): ...
|
|
|
12
18
|
|
|
13
19
|
class Node(abc.ABC):
|
|
14
20
|
node: str = "node"
|
|
21
|
+
scope: NodeScope = NodeScope.PER_EVENT
|
|
15
22
|
|
|
16
23
|
@classmethod
|
|
17
24
|
@abc.abstractmethod
|
|
18
|
-
def compose(cls, *args
|
|
25
|
+
def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
19
26
|
pass
|
|
20
27
|
|
|
21
28
|
@classmethod
|
|
@@ -24,15 +31,7 @@ class Node(abc.ABC):
|
|
|
24
31
|
|
|
25
32
|
@classmethod
|
|
26
33
|
def get_sub_nodes(cls) -> dict[str, type[typing.Self]]:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
sub_nodes = {}
|
|
30
|
-
for name, param in parameters.items():
|
|
31
|
-
if param.annotation is inspect._empty:
|
|
32
|
-
continue
|
|
33
|
-
node = param.annotation
|
|
34
|
-
sub_nodes[name] = node
|
|
35
|
-
return sub_nodes
|
|
34
|
+
return get_annotations(cls.compose)
|
|
36
35
|
|
|
37
36
|
@classmethod
|
|
38
37
|
def as_node(cls) -> type[typing.Self]:
|
|
@@ -49,14 +48,14 @@ class DataNode(Node, abc.ABC):
|
|
|
49
48
|
@typing.dataclass_transform()
|
|
50
49
|
@classmethod
|
|
51
50
|
@abc.abstractmethod
|
|
52
|
-
async def compose(cls, *args
|
|
51
|
+
async def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
53
52
|
pass
|
|
54
53
|
|
|
55
54
|
|
|
56
55
|
class ScalarNodeProto(Node, abc.ABC):
|
|
57
56
|
@classmethod
|
|
58
57
|
@abc.abstractmethod
|
|
59
|
-
async def compose(cls, *args
|
|
58
|
+
async def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
60
59
|
pass
|
|
61
60
|
|
|
62
61
|
|
|
@@ -78,17 +77,31 @@ else:
|
|
|
78
77
|
return type(
|
|
79
78
|
"Scalar",
|
|
80
79
|
(SCALAR_NODE,),
|
|
81
|
-
{
|
|
80
|
+
{
|
|
81
|
+
"as_node": classmethod(lambda cls: create_node(cls, bases, dct)),
|
|
82
|
+
"scope": Node.scope,
|
|
83
|
+
},
|
|
82
84
|
)
|
|
83
85
|
|
|
84
86
|
class ScalarNode(ScalarNodeProto, abc.ABC, metaclass=create_class):
|
|
85
87
|
pass
|
|
86
88
|
|
|
87
89
|
|
|
90
|
+
def is_node(maybe_node: type[typing.Any]) -> typing.TypeGuard[type[Node]]:
|
|
91
|
+
maybe_node = typing.get_origin(maybe_node) or maybe_node
|
|
92
|
+
return (
|
|
93
|
+
isinstance(maybe_node, type)
|
|
94
|
+
and issubclass(maybe_node, Node)
|
|
95
|
+
or isinstance(maybe_node, Node)
|
|
96
|
+
or hasattr(maybe_node, "as_node")
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
88
100
|
__all__ = (
|
|
89
101
|
"ComposeError",
|
|
90
102
|
"DataNode",
|
|
91
103
|
"Node",
|
|
92
104
|
"SCALAR_NODE",
|
|
93
105
|
"ScalarNode",
|
|
106
|
+
"is_node",
|
|
94
107
|
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from telegrinder.bot.cute_types import CallbackQueryCute
|
|
2
|
+
|
|
3
|
+
from .base import ComposeError, ScalarNode
|
|
4
|
+
from .update import UpdateNode
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CallbackQueryNode(ScalarNode, CallbackQueryCute):
|
|
8
|
+
@classmethod
|
|
9
|
+
async def compose(cls, update: UpdateNode) -> CallbackQueryCute:
|
|
10
|
+
if not update.callback_query:
|
|
11
|
+
raise ComposeError
|
|
12
|
+
return CallbackQueryCute(
|
|
13
|
+
**update.callback_query.unwrap().to_dict(),
|
|
14
|
+
api=update.api,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ("CallbackQueryNode",)
|