telegrinder 0.1.dev167__py3-none-any.whl → 0.1.dev169__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 +55 -44
- 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 +33 -30
- telegrinder/bot/dispatch/handler/func.py +33 -12
- 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 +74 -31
- 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 +86 -50
- telegrinder/bot/dispatch/waiter_machine/middleware.py +31 -7
- telegrinder/bot/dispatch/waiter_machine/short_state.py +26 -7
- 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 +13 -15
- 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/checkbox.py +5 -5
- telegrinder/bot/scenario/choice.py +5 -5
- telegrinder/model.py +49 -15
- telegrinder/modules.py +14 -6
- telegrinder/msgspec_utils.py +8 -17
- 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 +9 -14
- 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 +13 -3
- 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 +613 -401
- telegrinder/types/objects.py +1151 -757
- {telegrinder-0.1.dev167.dist-info → telegrinder-0.1.dev169.dist-info}/LICENSE +1 -1
- {telegrinder-0.1.dev167.dist-info → telegrinder-0.1.dev169.dist-info}/METADATA +9 -8
- telegrinder-0.1.dev169.dist-info/RECORD +143 -0
- telegrinder/bot/dispatch/composition.py +0 -88
- telegrinder-0.1.dev167.dist-info/RECORD +0 -137
- {telegrinder-0.1.dev167.dist-info → telegrinder-0.1.dev169.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")
|
|
@@ -33,13 +33,13 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
|
|
|
33
33
|
self,
|
|
34
34
|
waiter_machine: WaiterMachine,
|
|
35
35
|
chat_id: int,
|
|
36
|
-
|
|
36
|
+
message: str,
|
|
37
37
|
*,
|
|
38
38
|
ready_text: str = "Ready",
|
|
39
39
|
max_in_row: int = 3,
|
|
40
40
|
) -> None:
|
|
41
41
|
self.chat_id = chat_id
|
|
42
|
-
self.
|
|
42
|
+
self.message = message
|
|
43
43
|
self.choices: list[Choice] = []
|
|
44
44
|
self.ready = ready_text
|
|
45
45
|
self.max_in_row = max_in_row
|
|
@@ -58,7 +58,7 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
|
|
|
58
58
|
self.waiter_machine,
|
|
59
59
|
self.ready,
|
|
60
60
|
self.chat_id,
|
|
61
|
-
self.
|
|
61
|
+
self.message,
|
|
62
62
|
)
|
|
63
63
|
|
|
64
64
|
def get_markup(self) -> InlineKeyboardMarkup:
|
|
@@ -101,7 +101,7 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
|
|
|
101
101
|
# Toggle choice
|
|
102
102
|
self.choices[i].is_picked = not self.choices[i].is_picked
|
|
103
103
|
await cb.edit_text(
|
|
104
|
-
text=self.
|
|
104
|
+
text=self.message,
|
|
105
105
|
parse_mode=self.PARSE_MODE,
|
|
106
106
|
reply_markup=self.get_markup(),
|
|
107
107
|
)
|
|
@@ -118,7 +118,7 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
|
|
|
118
118
|
message = (
|
|
119
119
|
await api.send_message(
|
|
120
120
|
chat_id=self.chat_id,
|
|
121
|
-
text=self.
|
|
121
|
+
text=self.message,
|
|
122
122
|
parse_mode=self.PARSE_MODE,
|
|
123
123
|
reply_markup=self.get_markup(),
|
|
124
124
|
)
|
|
@@ -9,7 +9,7 @@ if typing.TYPE_CHECKING:
|
|
|
9
9
|
from telegrinder.bot.dispatch.view.abc import BaseStateView
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
class
|
|
12
|
+
class Choice(Checkbox):
|
|
13
13
|
async def handle(self, cb: CallbackQueryCute) -> bool:
|
|
14
14
|
code = cb.data.unwrap().replace(self.random_code + "/", "", 1)
|
|
15
15
|
if code == "ready":
|
|
@@ -22,9 +22,9 @@ class SingleChoice(Checkbox):
|
|
|
22
22
|
if choice.code == code:
|
|
23
23
|
self.choices[i].is_picked = True
|
|
24
24
|
await cb.ctx_api.edit_message_text(
|
|
25
|
+
text=self.message,
|
|
25
26
|
chat_id=cb.message.unwrap().v.chat.id,
|
|
26
27
|
message_id=cb.message.unwrap().v.message_id,
|
|
27
|
-
text=self.msg,
|
|
28
28
|
parse_mode=self.PARSE_MODE,
|
|
29
29
|
reply_markup=self.get_markup(),
|
|
30
30
|
)
|
|
@@ -36,10 +36,10 @@ class SingleChoice(Checkbox):
|
|
|
36
36
|
api: "API",
|
|
37
37
|
view: "BaseStateView[CallbackQueryCute]",
|
|
38
38
|
) -> tuple[str, int]:
|
|
39
|
-
if len(
|
|
39
|
+
if len(tuple(choice for choice in self.choices if choice.is_picked)) != 1:
|
|
40
40
|
raise ValueError("Exactly one choice must be picked")
|
|
41
41
|
choices, m_id = await super().wait(api, view)
|
|
42
|
-
return
|
|
42
|
+
return tuple(choices.keys())[tuple(choices.values()).index(True)], m_id
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
__all__ = ("
|
|
45
|
+
__all__ = ("Choice",)
|
telegrinder/model.py
CHANGED
|
@@ -45,14 +45,17 @@ def full_result(
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def get_params(params: dict[str, typing.Any]) -> dict[str, typing.Any]:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
validated_params = {}
|
|
49
|
+
for k, v in (
|
|
50
|
+
*params.pop("other", {}).items(),
|
|
51
|
+
*params.items(),
|
|
52
|
+
):
|
|
53
|
+
if isinstance(v, Proxy):
|
|
54
|
+
v = v.get()
|
|
55
|
+
if k == "self" or type(v) in (NoneType, Nothing):
|
|
56
|
+
continue
|
|
57
|
+
validated_params[k] = v.unwrap() if isinstance(v, Some) else v
|
|
58
|
+
return validated_params
|
|
56
59
|
|
|
57
60
|
|
|
58
61
|
class Model(msgspec.Struct, **MODEL_CONFIG):
|
|
@@ -69,9 +72,7 @@ class Model(msgspec.Struct, **MODEL_CONFIG):
|
|
|
69
72
|
if "model_as_dict" not in self.__dict__:
|
|
70
73
|
self.__dict__["model_as_dict"] = msgspec.structs.asdict(self)
|
|
71
74
|
return {
|
|
72
|
-
key: value
|
|
73
|
-
for key, value in self.__dict__["model_as_dict"].items()
|
|
74
|
-
if key not in exclude_fields
|
|
75
|
+
key: value for key, value in self.__dict__["model_as_dict"].items() if key not in exclude_fields
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
|
|
@@ -82,7 +83,7 @@ class DataConverter:
|
|
|
82
83
|
def __repr__(self) -> str:
|
|
83
84
|
return "<{}: {}>".format(
|
|
84
85
|
self.__class__.__name__,
|
|
85
|
-
", ".join(f"{k}={v!r}" for k, v in self.converters),
|
|
86
|
+
", ".join(f"{k}={v.__name__!r}" for k, v in self.converters.items()),
|
|
86
87
|
)
|
|
87
88
|
|
|
88
89
|
@property
|
|
@@ -129,9 +130,7 @@ class DataConverter:
|
|
|
129
130
|
serialize: bool = True,
|
|
130
131
|
) -> dict[str, typing.Any]:
|
|
131
132
|
return {
|
|
132
|
-
k: self(v, serialize=serialize)
|
|
133
|
-
for k, v in data.items()
|
|
134
|
-
if type(v) not in (NoneType, Nothing)
|
|
133
|
+
k: self(v, serialize=serialize) for k, v in data.items() if type(v) not in (NoneType, Nothing)
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
def convert_lst(
|
|
@@ -159,8 +158,43 @@ class DataConverter:
|
|
|
159
158
|
return data
|
|
160
159
|
|
|
161
160
|
|
|
161
|
+
class Proxy:
|
|
162
|
+
def __init__(self, cfg: "_ProxiedDict", key: str):
|
|
163
|
+
self.key = key
|
|
164
|
+
self.cfg = cfg
|
|
165
|
+
|
|
166
|
+
def get(self) -> typing.Any | None:
|
|
167
|
+
return self.cfg._defaults.get(self.key)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class _ProxiedDict(typing.Generic[T]):
|
|
171
|
+
def __init__(self, tp: type[T]) -> None:
|
|
172
|
+
self.type = tp
|
|
173
|
+
self._defaults = {}
|
|
174
|
+
|
|
175
|
+
def __setattribute__(self, name: str, value: typing.Any, /) -> None:
|
|
176
|
+
self._defaults[name] = value
|
|
177
|
+
|
|
178
|
+
def __getitem__(self, key: str, /) -> None:
|
|
179
|
+
return Proxy(self, key) # type: ignore
|
|
180
|
+
|
|
181
|
+
def __setitem__(self, key: str, value: typing.Any, /) -> None:
|
|
182
|
+
self._defaults[key] = value
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if typing.TYPE_CHECKING:
|
|
186
|
+
|
|
187
|
+
def ProxiedDict(typed_dct: type[T]) -> T | _ProxiedDict[T]: # noqa: N802
|
|
188
|
+
...
|
|
189
|
+
|
|
190
|
+
else:
|
|
191
|
+
ProxiedDict = _ProxiedDict
|
|
192
|
+
|
|
193
|
+
|
|
162
194
|
__all__ = (
|
|
195
|
+
"Proxy",
|
|
163
196
|
"DataConverter",
|
|
197
|
+
"ProxiedDict",
|
|
164
198
|
"MODEL_CONFIG",
|
|
165
199
|
"Model",
|
|
166
200
|
"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
|
@@ -24,9 +24,10 @@ else:
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
T = typing.TypeVar("T")
|
|
27
|
+
Type = typing.TypeVar("Type", bound=type | typing.Any)
|
|
27
28
|
Ts = typing.TypeVarTuple("Ts")
|
|
28
29
|
|
|
29
|
-
DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any],
|
|
30
|
+
DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any], typing.Any]
|
|
30
31
|
EncHook: typing.TypeAlias = typing.Callable[[T], typing.Any]
|
|
31
32
|
|
|
32
33
|
Nothing: typing.Final[fntypes.option.Nothing] = fntypes.option.Nothing()
|
|
@@ -67,9 +68,7 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
|
|
|
67
68
|
reverse = False
|
|
68
69
|
|
|
69
70
|
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
|
-
}
|
|
71
|
+
struct_fields_match_sums = {m: len(m.__struct_fields__) for m in struct_fields_match_sums}
|
|
73
72
|
reverse = True
|
|
74
73
|
|
|
75
74
|
union_types = (
|
|
@@ -144,8 +143,7 @@ class Decoder:
|
|
|
144
143
|
origin_type = t if isinstance((t := get_origin(tp)), type) else type(t)
|
|
145
144
|
if origin_type not in self.dec_hooks:
|
|
146
145
|
raise TypeError(
|
|
147
|
-
f"Unknown type `{repr_type(origin_type)}`. "
|
|
148
|
-
"You can implement decode hook for this type."
|
|
146
|
+
f"Unknown type `{repr_type(origin_type)}`. " "You can implement decode hook for this type."
|
|
149
147
|
)
|
|
150
148
|
return self.dec_hooks[origin_type](tp, obj)
|
|
151
149
|
|
|
@@ -173,7 +171,7 @@ class Decoder:
|
|
|
173
171
|
def decode(self, buf: str | bytes) -> typing.Any: ...
|
|
174
172
|
|
|
175
173
|
@typing.overload
|
|
176
|
-
def decode(self, buf: str | bytes, *,
|
|
174
|
+
def decode(self, buf: str | bytes, *, type: type[T]) -> T: ...
|
|
177
175
|
|
|
178
176
|
@typing.overload
|
|
179
177
|
def decode(
|
|
@@ -184,16 +182,10 @@ class Decoder:
|
|
|
184
182
|
strict: bool = True,
|
|
185
183
|
) -> T: ...
|
|
186
184
|
|
|
187
|
-
def decode(
|
|
188
|
-
self,
|
|
189
|
-
buf: str | bytes,
|
|
190
|
-
*,
|
|
191
|
-
type: type[T] = typing.Any, # type: ignore
|
|
192
|
-
strict: bool = True,
|
|
193
|
-
) -> T:
|
|
185
|
+
def decode(self, buf, *, type=object, strict=True):
|
|
194
186
|
return msgspec.json.decode(
|
|
195
187
|
buf,
|
|
196
|
-
type=type,
|
|
188
|
+
type=typing.Any if type is object else type,
|
|
197
189
|
strict=strict,
|
|
198
190
|
dec_hook=self.dec_hook,
|
|
199
191
|
)
|
|
@@ -240,8 +232,7 @@ class Encoder:
|
|
|
240
232
|
origin_type = get_origin(obj.__class__)
|
|
241
233
|
if origin_type not in self.enc_hooks:
|
|
242
234
|
raise NotImplementedError(
|
|
243
|
-
"Not implemented encode hook for "
|
|
244
|
-
f"object of type `{repr_type(origin_type)}`."
|
|
235
|
+
"Not implemented encode hook for " f"object of type `{repr_type(origin_type)}`."
|
|
245
236
|
)
|
|
246
237
|
return self.enc_hooks[origin_type](obj)
|
|
247
238
|
|
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",)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from fntypes import Nothing, Option, Some
|
|
4
|
+
|
|
5
|
+
from .base import DataNode
|
|
6
|
+
from .text import Text
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def single_split(s: str, separator: str) -> tuple[str, str]:
|
|
10
|
+
left, *right = s.split(separator, 1)
|
|
11
|
+
return left, (right[0] if right else "")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def cut_mention(text: str) -> tuple[str, Option[str]]:
|
|
15
|
+
left, right = single_split(text, "@")
|
|
16
|
+
return left, Some(right) if right else Nothing()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CommandInfo(DataNode):
|
|
21
|
+
name: str
|
|
22
|
+
arguments: str
|
|
23
|
+
mention: Option[str] = field(default_factory=Nothing)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
async def compose(cls, text: Text):
|
|
27
|
+
name, arguments = single_split(text, separator=" ")
|
|
28
|
+
name, mention = cut_mention(name)
|
|
29
|
+
return cls(name, arguments, mention)
|