telegrinder 0.1.dev170__py3-none-any.whl → 0.2.0__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 +2 -2
- telegrinder/api/__init__.py +1 -2
- telegrinder/api/api.py +15 -6
- telegrinder/api/error.py +2 -1
- telegrinder/api/token.py +36 -0
- telegrinder/bot/__init__.py +12 -6
- telegrinder/bot/bot.py +18 -6
- telegrinder/bot/cute_types/__init__.py +7 -7
- telegrinder/bot/cute_types/base.py +122 -20
- telegrinder/bot/cute_types/callback_query.py +10 -6
- telegrinder/bot/cute_types/chat_join_request.py +4 -5
- telegrinder/bot/cute_types/chat_member_updated.py +4 -6
- telegrinder/bot/cute_types/inline_query.py +3 -4
- telegrinder/bot/cute_types/message.py +32 -21
- telegrinder/bot/cute_types/update.py +51 -4
- telegrinder/bot/cute_types/utils.py +3 -466
- telegrinder/bot/dispatch/__init__.py +10 -11
- telegrinder/bot/dispatch/abc.py +8 -5
- telegrinder/bot/dispatch/context.py +17 -8
- telegrinder/bot/dispatch/dispatch.py +71 -48
- telegrinder/bot/dispatch/handler/__init__.py +3 -3
- telegrinder/bot/dispatch/handler/abc.py +4 -4
- telegrinder/bot/dispatch/handler/func.py +46 -22
- telegrinder/bot/dispatch/handler/message_reply.py +6 -7
- telegrinder/bot/dispatch/middleware/__init__.py +1 -1
- telegrinder/bot/dispatch/middleware/abc.py +2 -2
- telegrinder/bot/dispatch/process.py +38 -19
- telegrinder/bot/dispatch/return_manager/__init__.py +4 -4
- telegrinder/bot/dispatch/return_manager/abc.py +3 -3
- telegrinder/bot/dispatch/return_manager/callback_query.py +1 -2
- telegrinder/bot/dispatch/return_manager/inline_query.py +1 -2
- telegrinder/bot/dispatch/return_manager/message.py +1 -2
- telegrinder/bot/dispatch/view/__init__.py +8 -8
- telegrinder/bot/dispatch/view/abc.py +18 -16
- telegrinder/bot/dispatch/view/box.py +75 -64
- telegrinder/bot/dispatch/view/callback_query.py +1 -2
- telegrinder/bot/dispatch/view/chat_join_request.py +1 -2
- telegrinder/bot/dispatch/view/chat_member.py +16 -2
- telegrinder/bot/dispatch/view/inline_query.py +1 -2
- telegrinder/bot/dispatch/view/message.py +12 -5
- telegrinder/bot/dispatch/view/raw.py +9 -8
- telegrinder/bot/dispatch/waiter_machine/__init__.py +3 -3
- telegrinder/bot/dispatch/waiter_machine/machine.py +12 -8
- telegrinder/bot/dispatch/waiter_machine/middleware.py +1 -1
- telegrinder/bot/dispatch/waiter_machine/short_state.py +4 -3
- telegrinder/bot/polling/abc.py +1 -1
- telegrinder/bot/polling/polling.py +6 -6
- telegrinder/bot/rules/__init__.py +20 -20
- telegrinder/bot/rules/abc.py +57 -43
- telegrinder/bot/rules/adapter/__init__.py +5 -5
- telegrinder/bot/rules/adapter/abc.py +6 -3
- telegrinder/bot/rules/adapter/errors.py +2 -1
- telegrinder/bot/rules/adapter/event.py +28 -13
- telegrinder/bot/rules/adapter/node.py +28 -22
- telegrinder/bot/rules/adapter/raw_update.py +13 -5
- telegrinder/bot/rules/callback_data.py +4 -4
- telegrinder/bot/rules/chat_join.py +4 -4
- telegrinder/bot/rules/command.py +5 -7
- telegrinder/bot/rules/func.py +2 -2
- telegrinder/bot/rules/fuzzy.py +1 -1
- telegrinder/bot/rules/inline.py +3 -3
- telegrinder/bot/rules/integer.py +1 -2
- telegrinder/bot/rules/markup.py +5 -3
- telegrinder/bot/rules/message_entities.py +2 -2
- telegrinder/bot/rules/node.py +2 -2
- telegrinder/bot/rules/regex.py +1 -1
- telegrinder/bot/rules/rule_enum.py +1 -1
- telegrinder/bot/rules/text.py +1 -2
- telegrinder/bot/rules/update.py +1 -2
- telegrinder/bot/scenario/abc.py +2 -2
- telegrinder/bot/scenario/checkbox.py +3 -4
- telegrinder/bot/scenario/choice.py +1 -2
- telegrinder/model.py +89 -45
- telegrinder/modules.py +3 -3
- telegrinder/msgspec_utils.py +85 -57
- telegrinder/node/__init__.py +17 -10
- telegrinder/node/attachment.py +19 -16
- telegrinder/node/base.py +46 -22
- telegrinder/node/callback_query.py +5 -9
- telegrinder/node/command.py +6 -2
- telegrinder/node/composer.py +102 -77
- telegrinder/node/container.py +3 -3
- telegrinder/node/event.py +68 -0
- telegrinder/node/me.py +3 -0
- telegrinder/node/message.py +6 -10
- telegrinder/node/polymorphic.py +15 -10
- telegrinder/node/rule.py +20 -6
- telegrinder/node/scope.py +9 -1
- telegrinder/node/source.py +21 -11
- telegrinder/node/text.py +4 -4
- telegrinder/node/update.py +7 -4
- telegrinder/py.typed +0 -0
- telegrinder/rules.py +59 -0
- telegrinder/tools/__init__.py +2 -2
- telegrinder/tools/buttons.py +5 -10
- telegrinder/tools/error_handler/abc.py +2 -2
- telegrinder/tools/error_handler/error.py +2 -0
- telegrinder/tools/error_handler/error_handler.py +6 -6
- telegrinder/tools/formatting/spec_html_formats.py +10 -10
- telegrinder/tools/global_context/__init__.py +2 -2
- telegrinder/tools/global_context/global_context.py +3 -3
- telegrinder/tools/global_context/telegrinder_ctx.py +4 -4
- telegrinder/tools/keyboard.py +3 -3
- telegrinder/tools/loop_wrapper/loop_wrapper.py +47 -13
- telegrinder/tools/magic.py +96 -18
- telegrinder/types/__init__.py +1 -0
- telegrinder/types/enums.py +2 -0
- telegrinder/types/methods.py +91 -15
- telegrinder/types/objects.py +49 -24
- telegrinder/verification_utils.py +1 -3
- {telegrinder-0.1.dev170.dist-info → telegrinder-0.2.0.dist-info}/METADATA +2 -2
- telegrinder-0.2.0.dist-info/RECORD +145 -0
- telegrinder/api/abc.py +0 -73
- telegrinder-0.1.dev170.dist-info/RECORD +0 -143
- {telegrinder-0.1.dev170.dist-info → telegrinder-0.2.0.dist-info}/LICENSE +0 -0
- {telegrinder-0.1.dev170.dist-info → telegrinder-0.2.0.dist-info}/WHEEL +0 -0
telegrinder/msgspec_utils.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import dataclasses
|
|
1
2
|
import typing
|
|
2
3
|
|
|
3
4
|
import fntypes.option
|
|
@@ -9,10 +10,16 @@ if typing.TYPE_CHECKING:
|
|
|
9
10
|
from datetime import datetime
|
|
10
11
|
|
|
11
12
|
from fntypes.option import Option
|
|
12
|
-
|
|
13
|
+
|
|
14
|
+
def get_class_annotations(obj: typing.Any) -> dict[str, type[typing.Any]]: ...
|
|
15
|
+
|
|
16
|
+
def get_type_hints(obj: typing.Any) -> dict[str, type[typing.Any]]: ...
|
|
17
|
+
|
|
13
18
|
else:
|
|
14
19
|
from datetime import datetime as dt
|
|
15
20
|
|
|
21
|
+
from msgspec._utils import get_class_annotations, get_type_hints
|
|
22
|
+
|
|
16
23
|
Value = typing.TypeVar("Value")
|
|
17
24
|
Err = typing.TypeVar("Err")
|
|
18
25
|
|
|
@@ -22,20 +29,12 @@ else:
|
|
|
22
29
|
def __instancecheck__(cls, __instance: typing.Any) -> bool:
|
|
23
30
|
return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
|
|
24
31
|
|
|
25
|
-
class ResultMeta(type):
|
|
26
|
-
def __instancecheck__(cls, __instance: typing.Any) -> bool:
|
|
27
|
-
return isinstance(__instance, fntypes.result.Ok | fntypes.result.Error)
|
|
28
32
|
|
|
29
33
|
class Option(typing.Generic[Value], metaclass=OptionMeta):
|
|
30
34
|
pass
|
|
31
35
|
|
|
32
|
-
class Result(typing.Generic[Value, Err], metaclass=ResultMeta):
|
|
33
|
-
pass
|
|
34
|
-
|
|
35
36
|
|
|
36
37
|
T = typing.TypeVar("T")
|
|
37
|
-
Type = typing.TypeVar("Type", bound=type | typing.Any)
|
|
38
|
-
Ts = typing.TypeVarTuple("Ts")
|
|
39
38
|
|
|
40
39
|
DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any], typing.Any]
|
|
41
40
|
EncHook: typing.TypeAlias = typing.Callable[[T], typing.Any]
|
|
@@ -47,10 +46,29 @@ def get_origin(t: type[T]) -> type[T]:
|
|
|
47
46
|
return typing.cast(T, typing.get_origin(t)) or t
|
|
48
47
|
|
|
49
48
|
|
|
50
|
-
def repr_type(t:
|
|
49
|
+
def repr_type(t: typing.Any) -> str:
|
|
51
50
|
return getattr(t, "__name__", repr(get_origin(t)))
|
|
52
51
|
|
|
53
52
|
|
|
53
|
+
def is_common_type(type_: typing.Any) -> typing.TypeGuard[type[typing.Any]]:
|
|
54
|
+
if not isinstance(type_, type):
|
|
55
|
+
return False
|
|
56
|
+
return (
|
|
57
|
+
type_ in (str, int, float, bool, None, Variative)
|
|
58
|
+
or issubclass(type_, msgspec.Struct)
|
|
59
|
+
or hasattr(type_, "__dataclass_fields__")
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def type_check(obj: typing.Any, t: typing.Any) -> bool:
|
|
64
|
+
return (
|
|
65
|
+
isinstance(obj, t)
|
|
66
|
+
if isinstance(t, type)
|
|
67
|
+
and issubclass(t, msgspec.Struct)
|
|
68
|
+
else type(obj) in t if isinstance(t, tuple) else type(obj) is t
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
54
72
|
def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, str]:
|
|
55
73
|
try:
|
|
56
74
|
return Ok(decoder.convert(obj, type=t, strict=True))
|
|
@@ -63,68 +81,68 @@ def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, str]:
|
|
|
63
81
|
)
|
|
64
82
|
|
|
65
83
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
def msgspec_to_builtins(
|
|
85
|
+
obj: typing.Any,
|
|
86
|
+
*,
|
|
87
|
+
str_keys: bool = False,
|
|
88
|
+
builtin_types: typing.Iterable[type[typing.Any]] | None = None,
|
|
89
|
+
order: typing.Literal["deterministic", "sorted"] | None = None,
|
|
90
|
+
) -> fntypes.result.Result[typing.Any, msgspec.ValidationError]:
|
|
91
|
+
try:
|
|
92
|
+
return Ok(encoder.to_builtins(**locals()))
|
|
93
|
+
except msgspec.ValidationError as exc:
|
|
94
|
+
return Error(exc)
|
|
73
95
|
|
|
74
96
|
|
|
75
|
-
def
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if not isinstance(obj, dict):
|
|
79
|
-
raise TypeError(f"Cannot parse to Result object of type `{repr_type(type(obj))}`.")
|
|
97
|
+
def option_dec_hook(tp: type[Option[typing.Any]], obj: typing.Any) -> Option[typing.Any]:
|
|
98
|
+
if obj is None:
|
|
99
|
+
return Nothing
|
|
80
100
|
|
|
81
|
-
|
|
82
|
-
(
|
|
83
|
-
|
|
84
|
-
) or (typing.Any, typing.Any)
|
|
101
|
+
(value_type,) = typing.get_args(tp) or (typing.Any,)
|
|
102
|
+
orig_value_type = typing.get_origin(value_type) or value_type
|
|
103
|
+
orig_obj = obj
|
|
85
104
|
|
|
86
|
-
if
|
|
87
|
-
|
|
105
|
+
if not isinstance(orig_obj, dict | list) and is_common_type(orig_value_type):
|
|
106
|
+
if orig_value_type is Variative:
|
|
107
|
+
obj = value_type(orig_obj) # type: ignore
|
|
108
|
+
orig_value_type = typing.get_args(value_type)
|
|
88
109
|
|
|
89
|
-
|
|
90
|
-
|
|
110
|
+
if not type_check(orig_obj, orig_value_type):
|
|
111
|
+
raise TypeError(f"Expected `{repr_type(orig_value_type)}`, got `{repr_type(type(orig_obj))}`.")
|
|
91
112
|
|
|
92
|
-
|
|
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())
|
|
113
|
+
return fntypes.option.Some(obj)
|
|
98
114
|
|
|
99
|
-
|
|
115
|
+
return fntypes.option.Some(decoder.convert(orig_obj, type=value_type))
|
|
100
116
|
|
|
101
117
|
|
|
102
118
|
def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
|
|
103
119
|
union_types = typing.get_args(tp)
|
|
104
120
|
|
|
105
121
|
if isinstance(obj, dict):
|
|
106
|
-
|
|
122
|
+
models_struct_fields: dict[type[msgspec.Struct], int] = {
|
|
107
123
|
m: sum(1 for k in obj if k in m.__struct_fields__)
|
|
108
124
|
for m in union_types
|
|
109
125
|
if issubclass(get_origin(m), msgspec.Struct)
|
|
110
126
|
}
|
|
111
|
-
union_types = tuple(t for t in union_types if t not in
|
|
127
|
+
union_types = tuple(t for t in union_types if t not in models_struct_fields)
|
|
112
128
|
reverse = False
|
|
113
129
|
|
|
114
|
-
if len(set(
|
|
115
|
-
|
|
130
|
+
if len(set(models_struct_fields.values())) != len(models_struct_fields.values()):
|
|
131
|
+
models_struct_fields = {m: len(m.__struct_fields__) for m in models_struct_fields}
|
|
116
132
|
reverse = True
|
|
117
133
|
|
|
118
134
|
union_types = (
|
|
119
135
|
*sorted(
|
|
120
|
-
|
|
121
|
-
key=lambda k:
|
|
136
|
+
models_struct_fields,
|
|
137
|
+
key=lambda k: models_struct_fields[k],
|
|
122
138
|
reverse=reverse,
|
|
123
139
|
),
|
|
124
140
|
*union_types,
|
|
125
141
|
)
|
|
126
142
|
|
|
127
143
|
for t in union_types:
|
|
144
|
+
if not isinstance(obj, dict | list) and is_common_type(t) and type_check(obj, t):
|
|
145
|
+
return tp(obj)
|
|
128
146
|
match msgspec_convert(obj, t):
|
|
129
147
|
case Ok(value):
|
|
130
148
|
return tp(value)
|
|
@@ -137,6 +155,11 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
|
|
|
137
155
|
)
|
|
138
156
|
|
|
139
157
|
|
|
158
|
+
@typing.runtime_checkable
|
|
159
|
+
class DataclassInstance(typing.Protocol):
|
|
160
|
+
__dataclass_fields__: typing.ClassVar[dict[str, dataclasses.Field[typing.Any]]]
|
|
161
|
+
|
|
162
|
+
|
|
140
163
|
class Decoder:
|
|
141
164
|
"""Class `Decoder` for `msgspec` module with decode hook
|
|
142
165
|
for objects with the specified type.
|
|
@@ -155,7 +178,6 @@ class Decoder:
|
|
|
155
178
|
decoder.dec_hooks[dt] = lambda t, timestamp: t.fromtimestamp(timestamp)
|
|
156
179
|
|
|
157
180
|
decoder.dec_hook(dt, 1713354732) #> datetime.datetime(2024, 4, 17, 14, 52, 12)
|
|
158
|
-
decoder.dec_hook(int, "123") #> TypeError: Unknown type `int`. You can implement decode hook for this type.
|
|
159
181
|
|
|
160
182
|
decoder.convert("123", type=int, strict=False) #> 123
|
|
161
183
|
decoder.convert(1, type=Digit) #> <Digit.ONE: 1>
|
|
@@ -166,12 +188,9 @@ class Decoder:
|
|
|
166
188
|
|
|
167
189
|
def __init__(self) -> None:
|
|
168
190
|
self.dec_hooks: dict[typing.Any, DecHook[typing.Any]] = {
|
|
169
|
-
Result: result_dec_hook,
|
|
170
191
|
Option: option_dec_hook,
|
|
171
192
|
Variative: variative_dec_hook,
|
|
172
193
|
datetime: lambda t, obj: t.fromtimestamp(obj),
|
|
173
|
-
fntypes.result.Error: result_dec_hook,
|
|
174
|
-
fntypes.result.Ok: result_dec_hook,
|
|
175
194
|
fntypes.option.Some: option_dec_hook,
|
|
176
195
|
fntypes.option.Nothing: option_dec_hook,
|
|
177
196
|
}
|
|
@@ -250,8 +269,6 @@ class Encoder:
|
|
|
250
269
|
encoder.enc_hooks[dt] = lambda d: int(d.timestamp())
|
|
251
270
|
|
|
252
271
|
encoder.enc_hook(dt.now()) #> 1713354732
|
|
253
|
-
encoder.enc_hook(123) #> NotImplementedError: Not implemented encode hook for object of type `int`.
|
|
254
|
-
|
|
255
272
|
encoder.encode({'digit': Digit.ONE}) #> '{"digit":1}'
|
|
256
273
|
```
|
|
257
274
|
"""
|
|
@@ -260,10 +277,6 @@ class Encoder:
|
|
|
260
277
|
self.enc_hooks: dict[typing.Any, EncHook[typing.Any]] = {
|
|
261
278
|
fntypes.option.Some: lambda opt: opt.value,
|
|
262
279
|
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
|
-
},
|
|
267
280
|
Variative: lambda variative: variative.v,
|
|
268
281
|
datetime: lambda date: int(date.timestamp()),
|
|
269
282
|
}
|
|
@@ -302,6 +315,22 @@ class Encoder:
|
|
|
302
315
|
buf = msgspec.json.encode(obj, enc_hook=self.enc_hook)
|
|
303
316
|
return buf.decode() if as_str else buf
|
|
304
317
|
|
|
318
|
+
def to_builtins(
|
|
319
|
+
self,
|
|
320
|
+
obj: typing.Any,
|
|
321
|
+
*,
|
|
322
|
+
str_keys: bool = False,
|
|
323
|
+
builtin_types: typing.Iterable[type[typing.Any]] | None = None,
|
|
324
|
+
order: typing.Literal["deterministic", "sorted"] | None = None,
|
|
325
|
+
) -> typing.Any:
|
|
326
|
+
return msgspec.to_builtins(
|
|
327
|
+
obj,
|
|
328
|
+
str_keys=str_keys,
|
|
329
|
+
builtin_types=builtin_types,
|
|
330
|
+
enc_hook=self.enc_hook,
|
|
331
|
+
order=order,
|
|
332
|
+
)
|
|
333
|
+
|
|
305
334
|
|
|
306
335
|
decoder: typing.Final[Decoder] = Decoder()
|
|
307
336
|
encoder: typing.Final[Encoder] = Encoder()
|
|
@@ -315,9 +344,8 @@ __all__ = (
|
|
|
315
344
|
"datetime",
|
|
316
345
|
"decoder",
|
|
317
346
|
"encoder",
|
|
318
|
-
"get_origin",
|
|
319
347
|
"msgspec_convert",
|
|
320
|
-
"
|
|
321
|
-
"
|
|
322
|
-
"
|
|
348
|
+
"get_class_annotations",
|
|
349
|
+
"get_type_hints",
|
|
350
|
+
"msgspec_to_builtins",
|
|
323
351
|
)
|
telegrinder/node/__init__.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from .attachment import Attachment, Audio, Photo, Video
|
|
2
2
|
from .base import ComposeError, DataNode, Node, ScalarNode, is_node
|
|
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,39 @@ from .update import UpdateNode
|
|
|
15
18
|
__all__ = (
|
|
16
19
|
"Attachment",
|
|
17
20
|
"Audio",
|
|
21
|
+
"CallbackQueryNode",
|
|
18
22
|
"ChatSource",
|
|
23
|
+
"CommandInfo",
|
|
19
24
|
"ComposeError",
|
|
25
|
+
"Composition",
|
|
20
26
|
"ContainerNode",
|
|
21
27
|
"DataNode",
|
|
28
|
+
"EventNode",
|
|
29
|
+
"GLOBAL",
|
|
30
|
+
"Me",
|
|
22
31
|
"MessageNode",
|
|
23
32
|
"Node",
|
|
24
33
|
"NodeCollection",
|
|
34
|
+
"NodeScope",
|
|
25
35
|
"NodeSession",
|
|
36
|
+
"PER_CALL",
|
|
37
|
+
"PER_EVENT",
|
|
26
38
|
"Photo",
|
|
39
|
+
"Polymorphic",
|
|
27
40
|
"RuleChain",
|
|
28
41
|
"ScalarNode",
|
|
29
42
|
"Source",
|
|
30
43
|
"Text",
|
|
31
44
|
"TextInteger",
|
|
32
|
-
"UserSource",
|
|
33
45
|
"UpdateNode",
|
|
46
|
+
"UserSource",
|
|
34
47
|
"Video",
|
|
35
48
|
"compose_node",
|
|
49
|
+
"compose_nodes",
|
|
36
50
|
"generate_node",
|
|
37
|
-
"
|
|
51
|
+
"global_node",
|
|
52
|
+
"impl",
|
|
38
53
|
"is_node",
|
|
39
|
-
"compose_nodes",
|
|
40
|
-
"NodeScope",
|
|
41
|
-
"PER_CALL",
|
|
42
|
-
"PER_EVENT",
|
|
43
54
|
"per_call",
|
|
44
55
|
"per_event",
|
|
45
|
-
"CommandInfo",
|
|
46
|
-
"GLOBAL",
|
|
47
|
-
"global_node",
|
|
48
|
-
"Me",
|
|
49
56
|
)
|
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,52 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import inspect
|
|
3
3
|
import typing
|
|
4
|
+
from types import AsyncGeneratorType
|
|
4
5
|
|
|
5
|
-
from telegrinder.
|
|
6
|
+
from telegrinder.node.scope import NodeScope
|
|
7
|
+
from telegrinder.tools.magic import (
|
|
8
|
+
cache_magic_value,
|
|
9
|
+
get_annotations,
|
|
10
|
+
)
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
ComposeResult: typing.TypeAlias = typing.Awaitable[typing.Any] | typing.AsyncGenerator[typing.Any, None]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_node(maybe_node: type[typing.Any]) -> typing.TypeGuard[type["Node"]]:
|
|
16
|
+
maybe_node = typing.get_origin(maybe_node) or maybe_node
|
|
17
|
+
return (
|
|
18
|
+
isinstance(maybe_node, type)
|
|
19
|
+
and issubclass(maybe_node, Node)
|
|
20
|
+
or isinstance(maybe_node, Node)
|
|
21
|
+
or hasattr(maybe_node, "as_node")
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@cache_magic_value("__nodes__")
|
|
26
|
+
def get_nodes(function: typing.Callable[..., typing.Any]) -> dict[str, type["Node"]]:
|
|
27
|
+
return {k: v for k, v in get_annotations(function).items() if is_node(v)}
|
|
8
28
|
|
|
9
|
-
ComposeResult: typing.TypeAlias = (
|
|
10
|
-
typing.Coroutine[typing.Any, typing.Any, typing.Any]
|
|
11
|
-
| typing.AsyncGenerator[typing.Any, None]
|
|
12
|
-
| typing.Any
|
|
13
|
-
)
|
|
14
29
|
|
|
30
|
+
@cache_magic_value("__is_generator__")
|
|
31
|
+
def is_generator(function: typing.Callable[..., typing.Any]) -> typing.TypeGuard[AsyncGeneratorType[typing.Any, None]]:
|
|
32
|
+
return inspect.isasyncgenfunction(function)
|
|
15
33
|
|
|
16
|
-
|
|
34
|
+
|
|
35
|
+
def get_node_calc_lst(node: type["Node"]) -> list[type["Node"]]:
|
|
36
|
+
""" Returns flattened list of node types in ordering required to calculate given node. Provides caching for passed node type """
|
|
37
|
+
if calc_lst := getattr(node, "__nodes_calc_lst__", None):
|
|
38
|
+
return calc_lst
|
|
39
|
+
nodes_lst: list[type["Node"]] = []
|
|
40
|
+
annotations = list(node.as_node().get_subnodes().values())
|
|
41
|
+
for node_type in annotations:
|
|
42
|
+
nodes_lst.extend(get_node_calc_lst(node_type))
|
|
43
|
+
calc_lst = [*nodes_lst, node]
|
|
44
|
+
setattr(node, "__nodes_calc_lst__", calc_lst)
|
|
45
|
+
return calc_lst
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ComposeError(BaseException):
|
|
49
|
+
pass
|
|
17
50
|
|
|
18
51
|
|
|
19
52
|
class Node(abc.ABC):
|
|
@@ -30,8 +63,8 @@ class Node(abc.ABC):
|
|
|
30
63
|
raise ComposeError(error)
|
|
31
64
|
|
|
32
65
|
@classmethod
|
|
33
|
-
def
|
|
34
|
-
return
|
|
66
|
+
def get_subnodes(cls) -> dict[str, type["Node"]]:
|
|
67
|
+
return get_nodes(cls.compose)
|
|
35
68
|
|
|
36
69
|
@classmethod
|
|
37
70
|
def as_node(cls) -> type[typing.Self]:
|
|
@@ -39,7 +72,7 @@ class Node(abc.ABC):
|
|
|
39
72
|
|
|
40
73
|
@classmethod
|
|
41
74
|
def is_generator(cls) -> bool:
|
|
42
|
-
return
|
|
75
|
+
return is_generator(cls.compose)
|
|
43
76
|
|
|
44
77
|
|
|
45
78
|
class DataNode(Node, abc.ABC):
|
|
@@ -68,7 +101,6 @@ if typing.TYPE_CHECKING:
|
|
|
68
101
|
pass
|
|
69
102
|
|
|
70
103
|
else:
|
|
71
|
-
|
|
72
104
|
def create_node(cls, bases, dct):
|
|
73
105
|
dct.update(cls.__dict__)
|
|
74
106
|
return type(cls.__name__, bases, dct)
|
|
@@ -87,21 +119,13 @@ else:
|
|
|
87
119
|
pass
|
|
88
120
|
|
|
89
121
|
|
|
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
122
|
__all__ = (
|
|
101
123
|
"ComposeError",
|
|
102
124
|
"DataNode",
|
|
103
125
|
"Node",
|
|
104
126
|
"SCALAR_NODE",
|
|
105
127
|
"ScalarNode",
|
|
128
|
+
"ScalarNodeProto",
|
|
129
|
+
"get_nodes",
|
|
106
130
|
"is_node",
|
|
107
131
|
)
|
|
@@ -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")
|