telegrinder 0.1.dev169__py3-none-any.whl → 0.1.dev171__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/api/abc.py +7 -1
- telegrinder/api/api.py +12 -3
- telegrinder/api/error.py +2 -1
- telegrinder/bot/bot.py +6 -1
- telegrinder/bot/cute_types/base.py +144 -17
- telegrinder/bot/cute_types/callback_query.py +6 -1
- telegrinder/bot/cute_types/chat_member_updated.py +1 -2
- telegrinder/bot/cute_types/message.py +23 -11
- telegrinder/bot/cute_types/update.py +48 -0
- telegrinder/bot/cute_types/utils.py +2 -465
- telegrinder/bot/dispatch/__init__.py +2 -3
- telegrinder/bot/dispatch/abc.py +6 -3
- telegrinder/bot/dispatch/context.py +6 -6
- telegrinder/bot/dispatch/dispatch.py +61 -23
- telegrinder/bot/dispatch/handler/abc.py +2 -2
- telegrinder/bot/dispatch/handler/func.py +36 -17
- telegrinder/bot/dispatch/handler/message_reply.py +2 -2
- telegrinder/bot/dispatch/middleware/abc.py +2 -2
- telegrinder/bot/dispatch/process.py +10 -10
- telegrinder/bot/dispatch/return_manager/abc.py +3 -3
- telegrinder/bot/dispatch/view/abc.py +12 -15
- telegrinder/bot/dispatch/view/box.py +73 -62
- telegrinder/bot/dispatch/view/message.py +11 -3
- telegrinder/bot/dispatch/view/raw.py +3 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +2 -2
- telegrinder/bot/dispatch/waiter_machine/middleware.py +1 -1
- telegrinder/bot/dispatch/waiter_machine/short_state.py +2 -1
- telegrinder/bot/polling/polling.py +3 -3
- telegrinder/bot/rules/abc.py +11 -7
- telegrinder/bot/rules/adapter/event.py +7 -4
- telegrinder/bot/rules/adapter/node.py +1 -1
- telegrinder/bot/rules/command.py +5 -7
- telegrinder/bot/rules/func.py +1 -1
- telegrinder/bot/rules/fuzzy.py +1 -1
- telegrinder/bot/rules/integer.py +1 -2
- telegrinder/bot/rules/markup.py +3 -3
- telegrinder/bot/rules/message_entities.py +1 -1
- telegrinder/bot/rules/node.py +2 -2
- telegrinder/bot/rules/regex.py +1 -1
- telegrinder/bot/rules/rule_enum.py +1 -1
- telegrinder/bot/scenario/checkbox.py +2 -2
- telegrinder/model.py +87 -47
- telegrinder/modules.py +3 -3
- telegrinder/msgspec_utils.py +94 -13
- telegrinder/node/__init__.py +20 -11
- telegrinder/node/attachment.py +19 -16
- telegrinder/node/base.py +120 -24
- telegrinder/node/callback_query.py +5 -9
- telegrinder/node/command.py +6 -2
- telegrinder/node/composer.py +82 -54
- telegrinder/node/container.py +4 -4
- telegrinder/node/event.py +59 -0
- telegrinder/node/me.py +3 -0
- telegrinder/node/message.py +6 -10
- telegrinder/node/polymorphic.py +11 -12
- telegrinder/node/rule.py +27 -5
- telegrinder/node/source.py +10 -11
- telegrinder/node/text.py +4 -4
- telegrinder/node/update.py +1 -2
- telegrinder/py.typed +0 -0
- telegrinder/tools/__init__.py +2 -2
- telegrinder/tools/buttons.py +5 -10
- telegrinder/tools/error_handler/error.py +2 -0
- telegrinder/tools/error_handler/error_handler.py +1 -1
- telegrinder/tools/formatting/spec_html_formats.py +10 -10
- telegrinder/tools/global_context/__init__.py +2 -2
- telegrinder/tools/global_context/global_context.py +2 -2
- telegrinder/tools/global_context/telegrinder_ctx.py +4 -4
- telegrinder/tools/keyboard.py +2 -2
- telegrinder/tools/loop_wrapper/loop_wrapper.py +39 -5
- telegrinder/tools/magic.py +48 -15
- telegrinder/types/enums.py +1 -0
- telegrinder/types/methods.py +14 -5
- telegrinder/types/objects.py +3 -0
- {telegrinder-0.1.dev169.dist-info → telegrinder-0.1.dev171.dist-info}/METADATA +2 -2
- telegrinder-0.1.dev171.dist-info/RECORD +145 -0
- telegrinder-0.1.dev169.dist-info/RECORD +0 -143
- {telegrinder-0.1.dev169.dist-info → telegrinder-0.1.dev171.dist-info}/LICENSE +0 -0
- {telegrinder-0.1.dev169.dist-info → telegrinder-0.1.dev171.dist-info}/WHEEL +0 -0
telegrinder/msgspec_utils.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import dataclasses
|
|
1
2
|
import typing
|
|
2
3
|
|
|
3
4
|
import fntypes.option
|
|
5
|
+
import fntypes.result
|
|
4
6
|
import msgspec
|
|
5
7
|
from fntypes.co import Error, Ok, Result, Variative
|
|
6
8
|
|
|
@@ -8,10 +10,12 @@ if typing.TYPE_CHECKING:
|
|
|
8
10
|
from datetime import datetime
|
|
9
11
|
|
|
10
12
|
from fntypes.option import Option
|
|
13
|
+
from fntypes.result import Result
|
|
11
14
|
else:
|
|
12
15
|
from datetime import datetime as dt
|
|
13
16
|
|
|
14
17
|
Value = typing.TypeVar("Value")
|
|
18
|
+
Err = typing.TypeVar("Err")
|
|
15
19
|
|
|
16
20
|
datetime = type("datetime", (dt,), {})
|
|
17
21
|
|
|
@@ -19,13 +23,18 @@ else:
|
|
|
19
23
|
def __instancecheck__(cls, __instance: typing.Any) -> bool:
|
|
20
24
|
return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
|
|
21
25
|
|
|
26
|
+
class ResultMeta(type):
|
|
27
|
+
def __instancecheck__(cls, __instance: typing.Any) -> bool:
|
|
28
|
+
return isinstance(__instance, fntypes.result.Ok | fntypes.result.Error)
|
|
29
|
+
|
|
22
30
|
class Option(typing.Generic[Value], metaclass=OptionMeta):
|
|
23
31
|
pass
|
|
24
32
|
|
|
33
|
+
class Result(typing.Generic[Value, Err], metaclass=ResultMeta):
|
|
34
|
+
pass
|
|
35
|
+
|
|
25
36
|
|
|
26
37
|
T = typing.TypeVar("T")
|
|
27
|
-
Type = typing.TypeVar("Type", bound=type | typing.Any)
|
|
28
|
-
Ts = typing.TypeVarTuple("Ts")
|
|
29
38
|
|
|
30
39
|
DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any], typing.Any]
|
|
31
40
|
EncHook: typing.TypeAlias = typing.Callable[[T], typing.Any]
|
|
@@ -41,18 +50,62 @@ def repr_type(t: type) -> str:
|
|
|
41
50
|
return getattr(t, "__name__", repr(get_origin(t)))
|
|
42
51
|
|
|
43
52
|
|
|
44
|
-
def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T,
|
|
53
|
+
def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, str]:
|
|
45
54
|
try:
|
|
46
55
|
return Ok(decoder.convert(obj, type=t, strict=True))
|
|
47
|
-
except msgspec.ValidationError
|
|
48
|
-
return Error(
|
|
56
|
+
except msgspec.ValidationError:
|
|
57
|
+
return Error(
|
|
58
|
+
"Expected object of type `{}`, got `{}`.".format(
|
|
59
|
+
repr_type(t),
|
|
60
|
+
repr_type(type(obj)),
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def msgspec_to_builtins(
|
|
66
|
+
obj: typing.Any,
|
|
67
|
+
*,
|
|
68
|
+
str_keys: bool = False,
|
|
69
|
+
builtin_types: typing.Iterable[type[typing.Any]] | None = None,
|
|
70
|
+
order: typing.Literal["deterministic", "sorted"] | None = None,
|
|
71
|
+
) -> typing.Any:
|
|
72
|
+
return encoder.to_builtins(**locals())
|
|
49
73
|
|
|
50
74
|
|
|
51
75
|
def option_dec_hook(tp: type[Option[typing.Any]], obj: typing.Any) -> Option[typing.Any]:
|
|
52
|
-
|
|
53
|
-
return Nothing
|
|
76
|
+
orig_type = get_origin(tp)
|
|
54
77
|
(value_type,) = typing.get_args(tp) or (typing.Any,)
|
|
55
|
-
|
|
78
|
+
|
|
79
|
+
if obj is None and orig_type in (fntypes.option.Nothing, Option):
|
|
80
|
+
return fntypes.option.Nothing()
|
|
81
|
+
return fntypes.option.Some(msgspec_convert(obj, value_type).unwrap())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def result_dec_hook(
|
|
85
|
+
tp: type[Result[typing.Any, typing.Any]], obj: typing.Any
|
|
86
|
+
) -> Result[typing.Any, typing.Any]:
|
|
87
|
+
if not isinstance(obj, dict):
|
|
88
|
+
raise TypeError(f"Cannot parse to Result object of type `{repr_type(type(obj))}`.")
|
|
89
|
+
|
|
90
|
+
orig_type = get_origin(tp)
|
|
91
|
+
(first_type, second_type) = (
|
|
92
|
+
typing.get_args(tp) + (typing.Any,) if len(typing.get_args(tp)) == 1 else typing.get_args(tp)
|
|
93
|
+
) or (typing.Any, typing.Any)
|
|
94
|
+
|
|
95
|
+
if orig_type is Ok and "ok" in obj:
|
|
96
|
+
return Ok(msgspec_convert(obj["ok"], first_type).unwrap())
|
|
97
|
+
|
|
98
|
+
if orig_type is Error and "error" in obj:
|
|
99
|
+
return Error(msgspec_convert(obj["error"], first_type).unwrap())
|
|
100
|
+
|
|
101
|
+
if orig_type is Result:
|
|
102
|
+
match obj:
|
|
103
|
+
case {"ok": ok}:
|
|
104
|
+
return Ok(msgspec_convert(ok, first_type).unwrap())
|
|
105
|
+
case {"error": error}:
|
|
106
|
+
return Error(msgspec_convert(error, second_type).unwrap())
|
|
107
|
+
|
|
108
|
+
raise msgspec.ValidationError(f"Cannot parse object `{obj!r}` to Result.")
|
|
56
109
|
|
|
57
110
|
|
|
58
111
|
def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
|
|
@@ -93,6 +146,11 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
|
|
|
93
146
|
)
|
|
94
147
|
|
|
95
148
|
|
|
149
|
+
@typing.runtime_checkable
|
|
150
|
+
class DataclassInstance(typing.Protocol):
|
|
151
|
+
__dataclass_fields__: typing.ClassVar[dict[str, dataclasses.Field[typing.Any]]]
|
|
152
|
+
|
|
153
|
+
|
|
96
154
|
class Decoder:
|
|
97
155
|
"""Class `Decoder` for `msgspec` module with decode hook
|
|
98
156
|
for objects with the specified type.
|
|
@@ -111,7 +169,6 @@ class Decoder:
|
|
|
111
169
|
decoder.dec_hooks[dt] = lambda t, timestamp: t.fromtimestamp(timestamp)
|
|
112
170
|
|
|
113
171
|
decoder.dec_hook(dt, 1713354732) #> datetime.datetime(2024, 4, 17, 14, 52, 12)
|
|
114
|
-
decoder.dec_hook(int, "123") #> TypeError: Unknown type `int`. You can implement decode hook for this type.
|
|
115
172
|
|
|
116
173
|
decoder.convert("123", type=int, strict=False) #> 123
|
|
117
174
|
decoder.convert(1, type=Digit) #> <Digit.ONE: 1>
|
|
@@ -122,9 +179,14 @@ class Decoder:
|
|
|
122
179
|
|
|
123
180
|
def __init__(self) -> None:
|
|
124
181
|
self.dec_hooks: dict[typing.Any, DecHook[typing.Any]] = {
|
|
182
|
+
Result: result_dec_hook,
|
|
125
183
|
Option: option_dec_hook,
|
|
126
184
|
Variative: variative_dec_hook,
|
|
127
185
|
datetime: lambda t, obj: t.fromtimestamp(obj),
|
|
186
|
+
fntypes.result.Error: result_dec_hook,
|
|
187
|
+
fntypes.result.Ok: result_dec_hook,
|
|
188
|
+
fntypes.option.Some: option_dec_hook,
|
|
189
|
+
fntypes.option.Nothing: option_dec_hook,
|
|
128
190
|
}
|
|
129
191
|
|
|
130
192
|
def __repr__(self) -> str:
|
|
@@ -143,7 +205,7 @@ class Decoder:
|
|
|
143
205
|
origin_type = t if isinstance((t := get_origin(tp)), type) else type(t)
|
|
144
206
|
if origin_type not in self.dec_hooks:
|
|
145
207
|
raise TypeError(
|
|
146
|
-
f"Unknown type `{repr_type(origin_type)}`.
|
|
208
|
+
f"Unknown type `{repr_type(origin_type)}`. You can implement decode hook for this type."
|
|
147
209
|
)
|
|
148
210
|
return self.dec_hooks[origin_type](tp, obj)
|
|
149
211
|
|
|
@@ -201,8 +263,6 @@ class Encoder:
|
|
|
201
263
|
encoder.enc_hooks[dt] = lambda d: int(d.timestamp())
|
|
202
264
|
|
|
203
265
|
encoder.enc_hook(dt.now()) #> 1713354732
|
|
204
|
-
encoder.enc_hook(123) #> NotImplementedError: Not implemented encode hook for object of type `int`.
|
|
205
|
-
|
|
206
266
|
encoder.encode({'digit': Digit.ONE}) #> '{"digit":1}'
|
|
207
267
|
```
|
|
208
268
|
"""
|
|
@@ -211,6 +271,10 @@ class Encoder:
|
|
|
211
271
|
self.enc_hooks: dict[typing.Any, EncHook[typing.Any]] = {
|
|
212
272
|
fntypes.option.Some: lambda opt: opt.value,
|
|
213
273
|
fntypes.option.Nothing: lambda _: None,
|
|
274
|
+
fntypes.result.Ok: lambda ok: {"ok": ok.value},
|
|
275
|
+
fntypes.result.Error: lambda err: {
|
|
276
|
+
"error": (str(err.error) if isinstance(err.error, BaseException) else err.error)
|
|
277
|
+
},
|
|
214
278
|
Variative: lambda variative: variative.v,
|
|
215
279
|
datetime: lambda date: int(date.timestamp()),
|
|
216
280
|
}
|
|
@@ -232,7 +296,7 @@ class Encoder:
|
|
|
232
296
|
origin_type = get_origin(obj.__class__)
|
|
233
297
|
if origin_type not in self.enc_hooks:
|
|
234
298
|
raise NotImplementedError(
|
|
235
|
-
"Not implemented encode hook for
|
|
299
|
+
f"Not implemented encode hook for object of type `{repr_type(origin_type)}`."
|
|
236
300
|
)
|
|
237
301
|
return self.enc_hooks[origin_type](obj)
|
|
238
302
|
|
|
@@ -249,6 +313,22 @@ class Encoder:
|
|
|
249
313
|
buf = msgspec.json.encode(obj, enc_hook=self.enc_hook)
|
|
250
314
|
return buf.decode() if as_str else buf
|
|
251
315
|
|
|
316
|
+
def to_builtins(
|
|
317
|
+
self,
|
|
318
|
+
obj: typing.Any,
|
|
319
|
+
*,
|
|
320
|
+
str_keys: bool = False,
|
|
321
|
+
builtin_types: typing.Iterable[type[typing.Any]] | None = None,
|
|
322
|
+
order: typing.Literal["deterministic", "sorted"] | None = None,
|
|
323
|
+
) -> typing.Any:
|
|
324
|
+
return msgspec.to_builtins(
|
|
325
|
+
obj,
|
|
326
|
+
str_keys=str_keys,
|
|
327
|
+
builtin_types=builtin_types,
|
|
328
|
+
enc_hook=self.enc_hook,
|
|
329
|
+
order=order,
|
|
330
|
+
)
|
|
331
|
+
|
|
252
332
|
|
|
253
333
|
decoder: typing.Final[Decoder] = Decoder()
|
|
254
334
|
encoder: typing.Final[Encoder] = Encoder()
|
|
@@ -264,6 +344,7 @@ __all__ = (
|
|
|
264
344
|
"encoder",
|
|
265
345
|
"get_origin",
|
|
266
346
|
"msgspec_convert",
|
|
347
|
+
"msgspec_to_builtins",
|
|
267
348
|
"option_dec_hook",
|
|
268
349
|
"repr_type",
|
|
269
350
|
"variative_dec_hook",
|
telegrinder/node/__init__.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from .attachment import Attachment, Audio, Photo, Video
|
|
2
|
-
from .base import ComposeError, DataNode, Node, ScalarNode, is_node
|
|
2
|
+
from .base import BaseNode, ComposeError, DataNode, Node, ScalarNode, is_node, node_impl
|
|
3
|
+
from .callback_query import CallbackQueryNode
|
|
3
4
|
from .command import CommandInfo
|
|
4
5
|
from .composer import Composition, NodeCollection, NodeSession, compose_node, compose_nodes
|
|
5
6
|
from .container import ContainerNode
|
|
7
|
+
from .event import EventNode
|
|
6
8
|
from .me import Me
|
|
7
9
|
from .message import MessageNode
|
|
10
|
+
from .polymorphic import Polymorphic, impl
|
|
8
11
|
from .rule import RuleChain
|
|
9
12
|
from .scope import GLOBAL, PER_CALL, PER_EVENT, NodeScope, global_node, per_call, per_event
|
|
10
13
|
from .source import ChatSource, Source, UserSource
|
|
@@ -15,35 +18,41 @@ from .update import UpdateNode
|
|
|
15
18
|
__all__ = (
|
|
16
19
|
"Attachment",
|
|
17
20
|
"Audio",
|
|
21
|
+
"BaseNode",
|
|
22
|
+
"CallbackQueryNode",
|
|
18
23
|
"ChatSource",
|
|
24
|
+
"CommandInfo",
|
|
19
25
|
"ComposeError",
|
|
26
|
+
"Composition",
|
|
20
27
|
"ContainerNode",
|
|
21
28
|
"DataNode",
|
|
29
|
+
"EventNode",
|
|
30
|
+
"GLOBAL",
|
|
31
|
+
"Me",
|
|
22
32
|
"MessageNode",
|
|
23
33
|
"Node",
|
|
24
34
|
"NodeCollection",
|
|
35
|
+
"NodeScope",
|
|
25
36
|
"NodeSession",
|
|
37
|
+
"PER_CALL",
|
|
38
|
+
"PER_EVENT",
|
|
26
39
|
"Photo",
|
|
40
|
+
"Polymorphic",
|
|
27
41
|
"RuleChain",
|
|
28
42
|
"ScalarNode",
|
|
29
43
|
"Source",
|
|
30
44
|
"Text",
|
|
31
45
|
"TextInteger",
|
|
32
|
-
"UserSource",
|
|
33
46
|
"UpdateNode",
|
|
47
|
+
"UserSource",
|
|
34
48
|
"Video",
|
|
35
49
|
"compose_node",
|
|
50
|
+
"compose_nodes",
|
|
36
51
|
"generate_node",
|
|
37
|
-
"
|
|
52
|
+
"global_node",
|
|
53
|
+
"impl",
|
|
38
54
|
"is_node",
|
|
39
|
-
"
|
|
40
|
-
"NodeScope",
|
|
41
|
-
"PER_CALL",
|
|
42
|
-
"PER_EVENT",
|
|
55
|
+
"node_impl",
|
|
43
56
|
"per_call",
|
|
44
57
|
"per_event",
|
|
45
|
-
"CommandInfo",
|
|
46
|
-
"GLOBAL",
|
|
47
|
-
"global_node",
|
|
48
|
-
"Me",
|
|
49
58
|
)
|
telegrinder/node/attachment.py
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
|
-
from fntypes import Option, Some
|
|
4
|
+
from fntypes.co import Option, Some
|
|
5
5
|
from fntypes.option import Nothing
|
|
6
6
|
|
|
7
7
|
import telegrinder.types
|
|
8
|
+
from telegrinder.node.base import ComposeError, DataNode, ScalarNode
|
|
9
|
+
from telegrinder.node.message import MessageNode
|
|
8
10
|
|
|
9
|
-
from .base import ComposeError, DataNode, ScalarNode
|
|
10
|
-
from .message import MessageNode
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
@dataclasses.dataclass
|
|
12
|
+
@dataclasses.dataclass(slots=True)
|
|
14
13
|
class Attachment(DataNode):
|
|
15
14
|
attachment_type: typing.Literal["audio", "document", "photo", "poll", "video"]
|
|
16
15
|
audio: Option[telegrinder.types.Audio] = dataclasses.field(
|
|
17
|
-
default_factory=lambda: Nothing(),
|
|
16
|
+
default_factory=lambda: Nothing(),
|
|
17
|
+
kw_only=True,
|
|
18
18
|
)
|
|
19
19
|
document: Option[telegrinder.types.Document] = dataclasses.field(
|
|
20
|
-
default_factory=lambda: Nothing(),
|
|
20
|
+
default_factory=lambda: Nothing(),
|
|
21
|
+
kw_only=True,
|
|
21
22
|
)
|
|
22
23
|
photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
|
|
23
|
-
default_factory=lambda: Nothing(),
|
|
24
|
+
default_factory=lambda: Nothing(),
|
|
25
|
+
kw_only=True,
|
|
24
26
|
)
|
|
25
27
|
poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing(), kw_only=True)
|
|
26
28
|
video: Option[telegrinder.types.Video] = dataclasses.field(
|
|
27
|
-
default_factory=lambda: Nothing(),
|
|
29
|
+
default_factory=lambda: Nothing(),
|
|
30
|
+
kw_only=True,
|
|
28
31
|
)
|
|
29
32
|
|
|
30
33
|
@classmethod
|
|
@@ -33,17 +36,17 @@ class Attachment(DataNode):
|
|
|
33
36
|
match getattr(message, attachment_type, Nothing()):
|
|
34
37
|
case Some(attachment):
|
|
35
38
|
return cls(attachment_type, **{attachment_type: Some(attachment)})
|
|
36
|
-
return cls.compose_error("No attachment found in message")
|
|
39
|
+
return cls.compose_error("No attachment found in message.")
|
|
37
40
|
|
|
38
41
|
|
|
39
|
-
@dataclasses.dataclass
|
|
42
|
+
@dataclasses.dataclass(slots=True)
|
|
40
43
|
class Photo(DataNode):
|
|
41
44
|
sizes: list[telegrinder.types.PhotoSize]
|
|
42
45
|
|
|
43
46
|
@classmethod
|
|
44
47
|
async def compose(cls, attachment: Attachment) -> typing.Self:
|
|
45
48
|
if not attachment.photo:
|
|
46
|
-
raise ComposeError("Attachment is not
|
|
49
|
+
raise ComposeError("Attachment is not a photo.")
|
|
47
50
|
return cls(attachment.photo.unwrap())
|
|
48
51
|
|
|
49
52
|
|
|
@@ -51,7 +54,7 @@ class Video(ScalarNode, telegrinder.types.Video):
|
|
|
51
54
|
@classmethod
|
|
52
55
|
async def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
|
|
53
56
|
if not attachment.video:
|
|
54
|
-
raise ComposeError("Attachment is not
|
|
57
|
+
raise ComposeError("Attachment is not a video.")
|
|
55
58
|
return attachment.video.unwrap()
|
|
56
59
|
|
|
57
60
|
|
|
@@ -59,7 +62,7 @@ class Audio(ScalarNode, telegrinder.types.Audio):
|
|
|
59
62
|
@classmethod
|
|
60
63
|
async def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
|
|
61
64
|
if not attachment.audio:
|
|
62
|
-
raise ComposeError("Attachment is not an audio")
|
|
65
|
+
raise ComposeError("Attachment is not an audio.")
|
|
63
66
|
return attachment.audio.unwrap()
|
|
64
67
|
|
|
65
68
|
|
|
@@ -67,7 +70,7 @@ class Document(ScalarNode, telegrinder.types.Document):
|
|
|
67
70
|
@classmethod
|
|
68
71
|
async def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
|
|
69
72
|
if not attachment.document:
|
|
70
|
-
raise ComposeError("Attachment is not
|
|
73
|
+
raise ComposeError("Attachment is not a document.")
|
|
71
74
|
return attachment.document.unwrap()
|
|
72
75
|
|
|
73
76
|
|
|
@@ -75,7 +78,7 @@ class Poll(ScalarNode, telegrinder.types.Poll):
|
|
|
75
78
|
@classmethod
|
|
76
79
|
async def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
|
|
77
80
|
if not attachment.poll:
|
|
78
|
-
raise ComposeError("Attachment is not
|
|
81
|
+
raise ComposeError("Attachment is not a poll.")
|
|
79
82
|
return attachment.poll.unwrap()
|
|
80
83
|
|
|
81
84
|
|
telegrinder/node/base.py
CHANGED
|
@@ -1,19 +1,70 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import inspect
|
|
3
3
|
import typing
|
|
4
|
+
from types import AsyncGeneratorType
|
|
5
|
+
|
|
6
|
+
from telegrinder.api.api import API
|
|
7
|
+
from telegrinder.bot.cute_types.update import UpdateCute
|
|
8
|
+
from telegrinder.bot.dispatch.context import Context
|
|
9
|
+
from telegrinder.node.scope import NodeScope
|
|
10
|
+
from telegrinder.tools.magic import (
|
|
11
|
+
NODE_IMPL_MARK,
|
|
12
|
+
cache_magic_value,
|
|
13
|
+
get_annotations,
|
|
14
|
+
get_impls_by_key,
|
|
15
|
+
magic_bundle,
|
|
16
|
+
node_impl,
|
|
17
|
+
)
|
|
4
18
|
|
|
5
|
-
|
|
19
|
+
ComposeResult: typing.TypeAlias = typing.Awaitable[typing.Any] | typing.AsyncGenerator[typing.Any, None]
|
|
6
20
|
|
|
7
|
-
from .scope import NodeScope
|
|
8
21
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
)
|
|
22
|
+
def is_node(maybe_node: type[typing.Any]) -> typing.TypeGuard[type["Node"]]:
|
|
23
|
+
maybe_node = typing.get_origin(maybe_node) or maybe_node
|
|
24
|
+
return (
|
|
25
|
+
isinstance(maybe_node, type)
|
|
26
|
+
and issubclass(maybe_node, Node)
|
|
27
|
+
or isinstance(maybe_node, Node)
|
|
28
|
+
or hasattr(maybe_node, "as_node")
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@cache_magic_value("__compose_annotations__")
|
|
33
|
+
def get_compose_annotations(function: typing.Callable[..., typing.Any]) -> dict[str, typing.Any]:
|
|
34
|
+
return {k: v for k, v in get_annotations(function).items() if not is_node(v)}
|
|
35
|
+
|
|
14
36
|
|
|
37
|
+
@cache_magic_value("__nodes__")
|
|
38
|
+
def get_nodes(function: typing.Callable[..., typing.Any]) -> dict[str, type["Node"]]:
|
|
39
|
+
return {k: v for k, v in get_annotations(function).items() if is_node(v)}
|
|
15
40
|
|
|
16
|
-
|
|
41
|
+
|
|
42
|
+
@cache_magic_value("__is_generator__")
|
|
43
|
+
def is_generator(function: typing.Callable[..., typing.Any]) -> typing.TypeGuard[AsyncGeneratorType[typing.Any, None]]:
|
|
44
|
+
return inspect.isasyncgenfunction(function)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_node_impls(node_cls: type["Node"]) -> dict[str, typing.Any]:
|
|
48
|
+
if not hasattr(node_cls, "__node_impls__"):
|
|
49
|
+
impls = get_impls_by_key(node_cls, NODE_IMPL_MARK)
|
|
50
|
+
if issubclass(node_cls, BaseNode):
|
|
51
|
+
impls |= get_impls_by_key(BaseNode, NODE_IMPL_MARK)
|
|
52
|
+
setattr(node_cls, "__node_impls__", impls)
|
|
53
|
+
return getattr(node_cls, "__node_impls__")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_node_impl(
|
|
57
|
+
node: type[typing.Any],
|
|
58
|
+
node_impls: dict[str, typing.Callable[..., typing.Any]],
|
|
59
|
+
) -> typing.Callable[..., typing.Any] | None:
|
|
60
|
+
for n_impl in node_impls.values():
|
|
61
|
+
if "return" in n_impl.__annotations__ and node is n_impl.__annotations__["return"]:
|
|
62
|
+
return n_impl
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ComposeError(BaseException):
|
|
67
|
+
pass
|
|
17
68
|
|
|
18
69
|
|
|
19
70
|
class Node(abc.ABC):
|
|
@@ -25,13 +76,46 @@ class Node(abc.ABC):
|
|
|
25
76
|
def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
26
77
|
pass
|
|
27
78
|
|
|
79
|
+
@classmethod
|
|
80
|
+
async def compose_annotation(
|
|
81
|
+
cls,
|
|
82
|
+
annotation: typing.Any,
|
|
83
|
+
update: UpdateCute,
|
|
84
|
+
ctx: Context,
|
|
85
|
+
) -> typing.Any:
|
|
86
|
+
orig_annotation: type[typing.Any] = typing.get_origin(annotation) or annotation
|
|
87
|
+
n_impl = get_node_impl(orig_annotation, cls.get_node_impls())
|
|
88
|
+
if n_impl is None:
|
|
89
|
+
raise ComposeError(f"Node implementation for {orig_annotation!r} not found.")
|
|
90
|
+
|
|
91
|
+
result = n_impl(
|
|
92
|
+
cls,
|
|
93
|
+
**magic_bundle(
|
|
94
|
+
n_impl,
|
|
95
|
+
{"update": update, "context": ctx},
|
|
96
|
+
start_idx=0,
|
|
97
|
+
bundle_ctx=False,
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
if inspect.isawaitable(result):
|
|
101
|
+
return await result
|
|
102
|
+
return result
|
|
103
|
+
|
|
28
104
|
@classmethod
|
|
29
105
|
def compose_error(cls, error: str | None = None) -> typing.NoReturn:
|
|
30
106
|
raise ComposeError(error)
|
|
31
107
|
|
|
32
108
|
@classmethod
|
|
33
|
-
def get_sub_nodes(cls) -> dict[str, type[
|
|
34
|
-
return
|
|
109
|
+
def get_sub_nodes(cls) -> dict[str, type["Node"]]:
|
|
110
|
+
return get_nodes(cls.compose)
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def get_compose_annotations(cls) -> dict[str, typing.Any]:
|
|
114
|
+
return get_compose_annotations(cls.compose)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def get_node_impls(cls) -> dict[str, typing.Callable[..., typing.Any]]:
|
|
118
|
+
return get_node_impls(cls)
|
|
35
119
|
|
|
36
120
|
@classmethod
|
|
37
121
|
def as_node(cls) -> type[typing.Self]:
|
|
@@ -39,10 +123,29 @@ class Node(abc.ABC):
|
|
|
39
123
|
|
|
40
124
|
@classmethod
|
|
41
125
|
def is_generator(cls) -> bool:
|
|
42
|
-
return
|
|
126
|
+
return is_generator(cls.compose)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class BaseNode(Node, abc.ABC):
|
|
130
|
+
@classmethod
|
|
131
|
+
@abc.abstractmethod
|
|
132
|
+
def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
@node_impl
|
|
136
|
+
def compose_api(cls, update: UpdateCute) -> API:
|
|
137
|
+
return update.ctx_api
|
|
138
|
+
|
|
139
|
+
@node_impl
|
|
140
|
+
def compose_context(cls, context: Context) -> Context:
|
|
141
|
+
return context
|
|
43
142
|
|
|
143
|
+
@node_impl
|
|
144
|
+
def compose_update(cls, update: UpdateCute) -> UpdateCute:
|
|
145
|
+
return update
|
|
44
146
|
|
|
45
|
-
|
|
147
|
+
|
|
148
|
+
class DataNode(BaseNode, abc.ABC):
|
|
46
149
|
node = "data"
|
|
47
150
|
|
|
48
151
|
@typing.dataclass_transform()
|
|
@@ -52,7 +155,7 @@ class DataNode(Node, abc.ABC):
|
|
|
52
155
|
pass
|
|
53
156
|
|
|
54
157
|
|
|
55
|
-
class ScalarNodeProto(
|
|
158
|
+
class ScalarNodeProto(BaseNode, abc.ABC):
|
|
56
159
|
@classmethod
|
|
57
160
|
@abc.abstractmethod
|
|
58
161
|
async def compose(cls, *args, **kwargs) -> ComposeResult:
|
|
@@ -68,7 +171,6 @@ if typing.TYPE_CHECKING:
|
|
|
68
171
|
pass
|
|
69
172
|
|
|
70
173
|
else:
|
|
71
|
-
|
|
72
174
|
def create_node(cls, bases, dct):
|
|
73
175
|
dct.update(cls.__dict__)
|
|
74
176
|
return type(cls.__name__, bases, dct)
|
|
@@ -87,21 +189,15 @@ else:
|
|
|
87
189
|
pass
|
|
88
190
|
|
|
89
191
|
|
|
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
|
-
|
|
100
192
|
__all__ = (
|
|
193
|
+
"BaseNode",
|
|
101
194
|
"ComposeError",
|
|
102
195
|
"DataNode",
|
|
103
196
|
"Node",
|
|
104
197
|
"SCALAR_NODE",
|
|
105
198
|
"ScalarNode",
|
|
199
|
+
"ScalarNodeProto",
|
|
200
|
+
"get_compose_annotations",
|
|
201
|
+
"get_nodes",
|
|
106
202
|
"is_node",
|
|
107
203
|
)
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
from telegrinder.bot.cute_types import CallbackQueryCute
|
|
2
|
-
|
|
3
|
-
from .
|
|
4
|
-
from .update import UpdateNode
|
|
1
|
+
from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
|
|
2
|
+
from telegrinder.node.base import ComposeError, ScalarNode
|
|
3
|
+
from telegrinder.node.update import UpdateNode
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
class CallbackQueryNode(ScalarNode, CallbackQueryCute):
|
|
8
7
|
@classmethod
|
|
9
8
|
async def compose(cls, update: UpdateNode) -> CallbackQueryCute:
|
|
10
9
|
if not update.callback_query:
|
|
11
|
-
raise ComposeError
|
|
12
|
-
return
|
|
13
|
-
**update.callback_query.unwrap().to_dict(),
|
|
14
|
-
api=update.api,
|
|
15
|
-
)
|
|
10
|
+
raise ComposeError("Update is not a callback_query.")
|
|
11
|
+
return update.callback_query.unwrap()
|
|
16
12
|
|
|
17
13
|
|
|
18
14
|
__all__ = ("CallbackQueryNode",)
|
telegrinder/node/command.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import typing
|
|
1
2
|
from dataclasses import dataclass, field
|
|
2
3
|
|
|
3
4
|
from fntypes import Nothing, Option, Some
|
|
@@ -16,14 +17,17 @@ def cut_mention(text: str) -> tuple[str, Option[str]]:
|
|
|
16
17
|
return left, Some(right) if right else Nothing()
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
@dataclass
|
|
20
|
+
@dataclass(slots=True)
|
|
20
21
|
class CommandInfo(DataNode):
|
|
21
22
|
name: str
|
|
22
23
|
arguments: str
|
|
23
24
|
mention: Option[str] = field(default_factory=Nothing)
|
|
24
25
|
|
|
25
26
|
@classmethod
|
|
26
|
-
async def compose(cls, text: Text):
|
|
27
|
+
async def compose(cls, text: Text) -> typing.Self:
|
|
27
28
|
name, arguments = single_split(text, separator=" ")
|
|
28
29
|
name, mention = cut_mention(name)
|
|
29
30
|
return cls(name, arguments, mention)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = ("CommandInfo", "cut_mention", "single_split")
|