telegrinder 0.1.dev165__py3-none-any.whl → 0.1.dev167__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 +20 -0
- telegrinder/api/abc.py +1 -1
- telegrinder/api/api.py +8 -6
- telegrinder/api/error.py +2 -3
- telegrinder/bot/__init__.py +12 -0
- telegrinder/bot/bot.py +2 -2
- telegrinder/bot/cute_types/__init__.py +4 -0
- telegrinder/bot/cute_types/base.py +10 -10
- telegrinder/bot/cute_types/callback_query.py +1 -3
- telegrinder/bot/cute_types/chat_join_request.py +65 -0
- telegrinder/bot/cute_types/chat_member_updated.py +246 -0
- telegrinder/bot/cute_types/message.py +44 -38
- telegrinder/bot/cute_types/update.py +5 -4
- telegrinder/bot/cute_types/utils.py +40 -20
- telegrinder/bot/dispatch/__init__.py +8 -1
- telegrinder/bot/dispatch/composition.py +7 -7
- telegrinder/bot/dispatch/context.py +9 -10
- telegrinder/bot/dispatch/dispatch.py +30 -22
- telegrinder/bot/dispatch/handler/abc.py +1 -0
- telegrinder/bot/dispatch/handler/func.py +21 -5
- telegrinder/bot/dispatch/handler/message_reply.py +2 -3
- telegrinder/bot/dispatch/middleware/abc.py +2 -4
- telegrinder/bot/dispatch/process.py +4 -3
- telegrinder/bot/dispatch/return_manager/__init__.py +1 -1
- telegrinder/bot/dispatch/return_manager/abc.py +28 -20
- telegrinder/bot/dispatch/return_manager/callback_query.py +4 -2
- telegrinder/bot/dispatch/return_manager/inline_query.py +4 -2
- telegrinder/bot/dispatch/return_manager/message.py +8 -4
- telegrinder/bot/dispatch/view/__init__.py +8 -2
- telegrinder/bot/dispatch/view/abc.py +27 -23
- telegrinder/bot/dispatch/view/box.py +74 -11
- telegrinder/bot/dispatch/view/callback_query.py +1 -3
- telegrinder/bot/dispatch/view/chat_join_request.py +17 -0
- telegrinder/bot/dispatch/view/chat_member.py +26 -0
- telegrinder/bot/dispatch/view/message.py +23 -1
- telegrinder/bot/dispatch/view/raw.py +112 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +41 -26
- telegrinder/bot/dispatch/waiter_machine/middleware.py +14 -7
- telegrinder/bot/dispatch/waiter_machine/short_state.py +10 -7
- telegrinder/bot/polling/polling.py +2 -4
- telegrinder/bot/rules/__init__.py +20 -12
- telegrinder/bot/rules/abc.py +0 -9
- telegrinder/bot/rules/adapter/event.py +29 -22
- telegrinder/bot/rules/callback_data.py +15 -18
- telegrinder/bot/rules/chat_join.py +47 -0
- telegrinder/bot/rules/enum_text.py +7 -2
- telegrinder/bot/rules/fuzzy.py +1 -2
- telegrinder/bot/rules/inline.py +3 -3
- telegrinder/bot/rules/is_from.py +39 -51
- telegrinder/bot/rules/markup.py +1 -2
- telegrinder/bot/rules/mention.py +1 -4
- telegrinder/bot/rules/message.py +17 -0
- telegrinder/bot/rules/message_entities.py +1 -1
- telegrinder/bot/rules/regex.py +1 -2
- telegrinder/bot/rules/rule_enum.py +1 -3
- telegrinder/bot/rules/start.py +7 -7
- telegrinder/bot/rules/text.py +2 -1
- telegrinder/bot/rules/update.py +16 -0
- telegrinder/bot/scenario/checkbox.py +5 -7
- telegrinder/client/aiohttp.py +5 -7
- telegrinder/model.py +37 -22
- telegrinder/modules.py +15 -33
- telegrinder/msgspec_utils.py +34 -35
- telegrinder/node/__init__.py +12 -12
- telegrinder/node/attachment.py +21 -7
- telegrinder/node/base.py +14 -13
- telegrinder/node/composer.py +5 -5
- telegrinder/node/container.py +1 -1
- telegrinder/node/message.py +3 -1
- telegrinder/node/rule.py +4 -4
- telegrinder/node/source.py +6 -2
- telegrinder/node/text.py +3 -1
- telegrinder/node/tools/generator.py +1 -1
- telegrinder/tools/__init__.py +3 -1
- telegrinder/tools/buttons.py +4 -6
- telegrinder/tools/error_handler/abc.py +1 -2
- telegrinder/tools/error_handler/error.py +3 -6
- telegrinder/tools/error_handler/error_handler.py +34 -24
- telegrinder/tools/formatting/html.py +9 -5
- telegrinder/tools/formatting/links.py +1 -3
- telegrinder/tools/formatting/spec_html_formats.py +1 -1
- telegrinder/tools/global_context/abc.py +3 -1
- telegrinder/tools/global_context/global_context.py +13 -31
- telegrinder/tools/global_context/telegrinder_ctx.py +1 -1
- telegrinder/tools/i18n/base.py +4 -3
- telegrinder/tools/i18n/middleware/base.py +1 -3
- telegrinder/tools/i18n/simple.py +1 -3
- telegrinder/tools/keyboard.py +1 -1
- telegrinder/tools/limited_dict.py +27 -0
- telegrinder/tools/loop_wrapper/loop_wrapper.py +18 -14
- telegrinder/tools/magic.py +1 -1
- telegrinder/types/__init__.py +236 -0
- telegrinder/types/enums.py +34 -0
- telegrinder/types/methods.py +52 -47
- telegrinder/types/objects.py +533 -90
- telegrinder/verification_utils.py +4 -1
- {telegrinder-0.1.dev165.dist-info → telegrinder-0.1.dev167.dist-info}/METADATA +1 -1
- telegrinder-0.1.dev167.dist-info/RECORD +137 -0
- telegrinder-0.1.dev165.dist-info/RECORD +0 -128
- {telegrinder-0.1.dev165.dist-info → telegrinder-0.1.dev167.dist-info}/LICENSE +0 -0
- {telegrinder-0.1.dev165.dist-info → telegrinder-0.1.dev167.dist-info}/WHEEL +0 -0
telegrinder/msgspec_utils.py
CHANGED
|
@@ -15,15 +15,14 @@ else:
|
|
|
15
15
|
|
|
16
16
|
datetime = type("datetime", (dt,), {})
|
|
17
17
|
|
|
18
|
-
|
|
19
18
|
class OptionMeta(type):
|
|
20
19
|
def __instancecheck__(cls, __instance: typing.Any) -> bool:
|
|
21
20
|
return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
|
|
22
21
|
|
|
23
|
-
|
|
24
22
|
class Option(typing.Generic[Value], metaclass=OptionMeta):
|
|
25
23
|
pass
|
|
26
24
|
|
|
25
|
+
|
|
27
26
|
T = typing.TypeVar("T")
|
|
28
27
|
Ts = typing.TypeVarTuple("Ts")
|
|
29
28
|
|
|
@@ -51,13 +50,13 @@ def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, msgspec.Validation
|
|
|
51
50
|
def option_dec_hook(tp: type[Option[typing.Any]], obj: typing.Any) -> Option[typing.Any]:
|
|
52
51
|
if obj is None:
|
|
53
52
|
return Nothing
|
|
54
|
-
value_type, = typing.get_args(tp) or (typing.Any,)
|
|
53
|
+
(value_type,) = typing.get_args(tp) or (typing.Any,)
|
|
55
54
|
return msgspec_convert({"value": obj}, fntypes.option.Some[value_type]).unwrap()
|
|
56
55
|
|
|
57
56
|
|
|
58
57
|
def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
|
|
59
58
|
union_types = typing.get_args(tp)
|
|
60
|
-
|
|
59
|
+
|
|
61
60
|
if isinstance(obj, dict):
|
|
62
61
|
struct_fields_match_sums: dict[type[msgspec.Struct], int] = {
|
|
63
62
|
m: sum(1 for k in obj if k in m.__struct_fields__)
|
|
@@ -68,14 +67,20 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
|
|
|
68
67
|
reverse = False
|
|
69
68
|
|
|
70
69
|
if len(set(struct_fields_match_sums.values())) != len(struct_fields_match_sums.values()):
|
|
71
|
-
struct_fields_match_sums = {
|
|
70
|
+
struct_fields_match_sums = {
|
|
71
|
+
m: len(m.__struct_fields__) for m in struct_fields_match_sums
|
|
72
|
+
}
|
|
72
73
|
reverse = True
|
|
73
74
|
|
|
74
75
|
union_types = (
|
|
75
|
-
*sorted(
|
|
76
|
+
*sorted(
|
|
77
|
+
struct_fields_match_sums,
|
|
78
|
+
key=lambda k: struct_fields_match_sums[k],
|
|
79
|
+
reverse=reverse,
|
|
80
|
+
),
|
|
76
81
|
*union_types,
|
|
77
82
|
)
|
|
78
|
-
|
|
83
|
+
|
|
79
84
|
for t in union_types:
|
|
80
85
|
match msgspec_convert(obj, t):
|
|
81
86
|
case Ok(value):
|
|
@@ -92,7 +97,7 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
|
|
|
92
97
|
class Decoder:
|
|
93
98
|
"""Class `Decoder` for `msgspec` module with decode hook
|
|
94
99
|
for objects with the specified type.
|
|
95
|
-
|
|
100
|
+
|
|
96
101
|
```
|
|
97
102
|
import enum
|
|
98
103
|
|
|
@@ -122,7 +127,7 @@ class Decoder:
|
|
|
122
127
|
Variative: variative_dec_hook,
|
|
123
128
|
datetime: lambda t, obj: t.fromtimestamp(obj),
|
|
124
129
|
}
|
|
125
|
-
|
|
130
|
+
|
|
126
131
|
def __repr__(self) -> str:
|
|
127
132
|
return "<{}: dec_hooks={!r}>".format(
|
|
128
133
|
self.__class__.__name__,
|
|
@@ -132,9 +137,9 @@ class Decoder:
|
|
|
132
137
|
def add_dec_hook(self, t: T): # type: ignore
|
|
133
138
|
def decorator(func: DecHook[T]) -> DecHook[T]:
|
|
134
139
|
return self.dec_hooks.setdefault(get_origin(t), func) # type: ignore
|
|
135
|
-
|
|
140
|
+
|
|
136
141
|
return decorator
|
|
137
|
-
|
|
142
|
+
|
|
138
143
|
def dec_hook(self, tp: type[typing.Any], obj: object) -> object:
|
|
139
144
|
origin_type = t if isinstance((t := get_origin(tp)), type) else type(t)
|
|
140
145
|
if origin_type not in self.dec_hooks:
|
|
@@ -143,7 +148,7 @@ class Decoder:
|
|
|
143
148
|
"You can implement decode hook for this type."
|
|
144
149
|
)
|
|
145
150
|
return self.dec_hooks[origin_type](tp, obj)
|
|
146
|
-
|
|
151
|
+
|
|
147
152
|
def convert(
|
|
148
153
|
self,
|
|
149
154
|
obj: object,
|
|
@@ -163,14 +168,12 @@ class Decoder:
|
|
|
163
168
|
builtin_types=builtin_types,
|
|
164
169
|
str_keys=str_keys,
|
|
165
170
|
)
|
|
166
|
-
|
|
171
|
+
|
|
167
172
|
@typing.overload
|
|
168
|
-
def decode(self, buf: str | bytes) -> typing.Any:
|
|
169
|
-
|
|
170
|
-
|
|
173
|
+
def decode(self, buf: str | bytes) -> typing.Any: ...
|
|
174
|
+
|
|
171
175
|
@typing.overload
|
|
172
|
-
def decode(self, buf: str | bytes, *, strict: bool = True) -> typing.Any:
|
|
173
|
-
...
|
|
176
|
+
def decode(self, buf: str | bytes, *, strict: bool = True) -> typing.Any: ...
|
|
174
177
|
|
|
175
178
|
@typing.overload
|
|
176
179
|
def decode(
|
|
@@ -179,8 +182,7 @@ class Decoder:
|
|
|
179
182
|
*,
|
|
180
183
|
type: type[T],
|
|
181
184
|
strict: bool = True,
|
|
182
|
-
) -> T:
|
|
183
|
-
...
|
|
185
|
+
) -> T: ...
|
|
184
186
|
|
|
185
187
|
def decode(
|
|
186
188
|
self,
|
|
@@ -199,7 +201,7 @@ class Decoder:
|
|
|
199
201
|
|
|
200
202
|
class Encoder:
|
|
201
203
|
"""Class `Encoder` for `msgspec` module with encode hooks for objects.
|
|
202
|
-
|
|
204
|
+
|
|
203
205
|
```
|
|
204
206
|
from datetime import datetime as dt
|
|
205
207
|
|
|
@@ -231,9 +233,9 @@ class Encoder:
|
|
|
231
233
|
def decorator(func: EncHook[T]) -> EncHook[T]:
|
|
232
234
|
encode_hook = self.enc_hooks.setdefault(get_origin(t), func)
|
|
233
235
|
return func if encode_hook is not func else encode_hook
|
|
234
|
-
|
|
236
|
+
|
|
235
237
|
return decorator
|
|
236
|
-
|
|
238
|
+
|
|
237
239
|
def enc_hook(self, obj: object) -> object:
|
|
238
240
|
origin_type = get_origin(obj.__class__)
|
|
239
241
|
if origin_type not in self.enc_hooks:
|
|
@@ -244,16 +246,13 @@ class Encoder:
|
|
|
244
246
|
return self.enc_hooks[origin_type](obj)
|
|
245
247
|
|
|
246
248
|
@typing.overload
|
|
247
|
-
def encode(self, obj: typing.Any) -> str:
|
|
248
|
-
|
|
249
|
-
|
|
249
|
+
def encode(self, obj: typing.Any) -> str: ...
|
|
250
|
+
|
|
250
251
|
@typing.overload
|
|
251
|
-
def encode(self, obj: typing.Any, *, as_str: typing.Literal[True]) -> str:
|
|
252
|
-
...
|
|
252
|
+
def encode(self, obj: typing.Any, *, as_str: typing.Literal[True]) -> str: ...
|
|
253
253
|
|
|
254
254
|
@typing.overload
|
|
255
|
-
def encode(self, obj: typing.Any, *, as_str: typing.Literal[False]) -> bytes:
|
|
256
|
-
...
|
|
255
|
+
def encode(self, obj: typing.Any, *, as_str: typing.Literal[False]) -> bytes: ...
|
|
257
256
|
|
|
258
257
|
def encode(self, obj: typing.Any, *, as_str: bool = True) -> str | bytes:
|
|
259
258
|
buf = msgspec.json.encode(obj, enc_hook=self.enc_hook)
|
|
@@ -267,14 +266,14 @@ encoder: typing.Final[Encoder] = Encoder()
|
|
|
267
266
|
__all__ = (
|
|
268
267
|
"Decoder",
|
|
269
268
|
"Encoder",
|
|
270
|
-
"Option",
|
|
271
269
|
"Nothing",
|
|
270
|
+
"Option",
|
|
271
|
+
"datetime",
|
|
272
|
+
"decoder",
|
|
273
|
+
"encoder",
|
|
272
274
|
"get_origin",
|
|
273
|
-
"repr_type",
|
|
274
275
|
"msgspec_convert",
|
|
275
276
|
"option_dec_hook",
|
|
277
|
+
"repr_type",
|
|
276
278
|
"variative_dec_hook",
|
|
277
|
-
"datetime",
|
|
278
|
-
"decoder",
|
|
279
|
-
"encoder",
|
|
280
279
|
)
|
telegrinder/node/__init__.py
CHANGED
|
@@ -10,22 +10,22 @@ from .tools import generate
|
|
|
10
10
|
from .update import UpdateNode
|
|
11
11
|
|
|
12
12
|
__all__ = (
|
|
13
|
-
"Node",
|
|
14
|
-
"DataNode",
|
|
15
|
-
"ScalarNode",
|
|
16
13
|
"Attachment",
|
|
17
|
-
"Photo",
|
|
18
|
-
"Video",
|
|
19
|
-
"Text",
|
|
20
14
|
"Audio",
|
|
21
|
-
"UpdateNode",
|
|
22
|
-
"compose_node",
|
|
23
15
|
"ComposeError",
|
|
16
|
+
"ContainerNode",
|
|
17
|
+
"DataNode",
|
|
24
18
|
"MessageNode",
|
|
25
|
-
"
|
|
26
|
-
"NodeSession",
|
|
19
|
+
"Node",
|
|
27
20
|
"NodeCollection",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
21
|
+
"NodeSession",
|
|
22
|
+
"Photo",
|
|
30
23
|
"RuleContext",
|
|
24
|
+
"ScalarNode",
|
|
25
|
+
"Source",
|
|
26
|
+
"Text",
|
|
27
|
+
"UpdateNode",
|
|
28
|
+
"Video",
|
|
29
|
+
"compose_node",
|
|
30
|
+
"generate",
|
|
31
31
|
)
|
telegrinder/node/attachment.py
CHANGED
|
@@ -15,8 +15,12 @@ class Attachment(DataNode):
|
|
|
15
15
|
attachment_type: typing.Literal["audio", "document", "photo", "poll", "video"]
|
|
16
16
|
_: dataclasses.KW_ONLY
|
|
17
17
|
audio: Option[telegrinder.types.Audio] = dataclasses.field(default_factory=lambda: Nothing())
|
|
18
|
-
document: Option[telegrinder.types.Document] = dataclasses.field(
|
|
19
|
-
|
|
18
|
+
document: Option[telegrinder.types.Document] = dataclasses.field(
|
|
19
|
+
default_factory=lambda: Nothing()
|
|
20
|
+
)
|
|
21
|
+
photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
|
|
22
|
+
default_factory=lambda: Nothing()
|
|
23
|
+
)
|
|
20
24
|
poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing())
|
|
21
25
|
video: Option[telegrinder.types.Video] = dataclasses.field(default_factory=lambda: Nothing())
|
|
22
26
|
|
|
@@ -34,31 +38,41 @@ class Photo(DataNode):
|
|
|
34
38
|
|
|
35
39
|
@classmethod
|
|
36
40
|
async def compose(cls, attachment: Attachment) -> typing.Self:
|
|
37
|
-
|
|
41
|
+
if not attachment.photo:
|
|
42
|
+
raise ComposeError("Attachment is not an photo")
|
|
43
|
+
return cls(attachment.photo.unwrap())
|
|
38
44
|
|
|
39
45
|
|
|
40
46
|
class Video(ScalarNode, telegrinder.types.Video):
|
|
41
47
|
@classmethod
|
|
42
48
|
async def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
|
|
43
|
-
|
|
49
|
+
if not attachment.video:
|
|
50
|
+
raise ComposeError("Attachment is not an video")
|
|
51
|
+
return attachment.video.unwrap()
|
|
44
52
|
|
|
45
53
|
|
|
46
54
|
class Audio(ScalarNode, telegrinder.types.Audio):
|
|
47
55
|
@classmethod
|
|
48
56
|
async def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
|
|
49
|
-
|
|
57
|
+
if not attachment.audio:
|
|
58
|
+
raise ComposeError("Attachment is not an audio")
|
|
59
|
+
return attachment.audio.unwrap()
|
|
50
60
|
|
|
51
61
|
|
|
52
62
|
class Document(ScalarNode, telegrinder.types.Document):
|
|
53
63
|
@classmethod
|
|
54
64
|
async def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
|
|
55
|
-
|
|
65
|
+
if not attachment.document:
|
|
66
|
+
raise ComposeError("Attachment is not an document")
|
|
67
|
+
return attachment.document.unwrap()
|
|
56
68
|
|
|
57
69
|
|
|
58
70
|
class Poll(ScalarNode, telegrinder.types.Poll):
|
|
59
71
|
@classmethod
|
|
60
72
|
async def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
|
|
61
|
-
|
|
73
|
+
if not attachment.poll:
|
|
74
|
+
raise ComposeError("Attachment is not an poll")
|
|
75
|
+
return attachment.poll.unwrap()
|
|
62
76
|
|
|
63
77
|
|
|
64
78
|
__all__ = (
|
telegrinder/node/base.py
CHANGED
|
@@ -2,11 +2,12 @@ import abc
|
|
|
2
2
|
import inspect
|
|
3
3
|
import typing
|
|
4
4
|
|
|
5
|
-
ComposeResult: typing.TypeAlias =
|
|
5
|
+
ComposeResult: typing.TypeAlias = (
|
|
6
|
+
typing.Coroutine[typing.Any, typing.Any, typing.Any] | typing.AsyncGenerator[typing.Any, None]
|
|
7
|
+
)
|
|
6
8
|
|
|
7
9
|
|
|
8
|
-
class ComposeError(BaseException):
|
|
9
|
-
...
|
|
10
|
+
class ComposeError(BaseException): ...
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class Node(abc.ABC):
|
|
@@ -24,7 +25,7 @@ class Node(abc.ABC):
|
|
|
24
25
|
@classmethod
|
|
25
26
|
def get_sub_nodes(cls) -> dict[str, type[typing.Self]]:
|
|
26
27
|
parameters = inspect.signature(cls.compose).parameters
|
|
27
|
-
|
|
28
|
+
|
|
28
29
|
sub_nodes = {}
|
|
29
30
|
for name, param in parameters.items():
|
|
30
31
|
if param.annotation is inspect._empty:
|
|
@@ -32,11 +33,11 @@ class Node(abc.ABC):
|
|
|
32
33
|
node = param.annotation
|
|
33
34
|
sub_nodes[name] = node
|
|
34
35
|
return sub_nodes
|
|
35
|
-
|
|
36
|
+
|
|
36
37
|
@classmethod
|
|
37
38
|
def as_node(cls) -> type[typing.Self]:
|
|
38
39
|
return cls
|
|
39
|
-
|
|
40
|
+
|
|
40
41
|
@classmethod
|
|
41
42
|
def is_generator(cls) -> bool:
|
|
42
43
|
return inspect.isasyncgenfunction(cls.compose)
|
|
@@ -64,9 +65,9 @@ SCALAR_NODE = type("ScalarNode", (), {"node": "scalar"})
|
|
|
64
65
|
|
|
65
66
|
if typing.TYPE_CHECKING:
|
|
66
67
|
|
|
67
|
-
class ScalarNode(ScalarNodeProto, abc.ABC):
|
|
68
|
+
class ScalarNode(ScalarNodeProto, abc.ABC):
|
|
68
69
|
pass
|
|
69
|
-
|
|
70
|
+
|
|
70
71
|
else:
|
|
71
72
|
|
|
72
73
|
def create_node(cls, bases, dct):
|
|
@@ -75,8 +76,8 @@ else:
|
|
|
75
76
|
|
|
76
77
|
def create_class(name, bases, dct):
|
|
77
78
|
return type(
|
|
78
|
-
"Scalar",
|
|
79
|
-
(SCALAR_NODE,),
|
|
79
|
+
"Scalar",
|
|
80
|
+
(SCALAR_NODE,),
|
|
80
81
|
{"as_node": classmethod(lambda cls: create_node(cls, bases, dct))},
|
|
81
82
|
)
|
|
82
83
|
|
|
@@ -85,9 +86,9 @@ else:
|
|
|
85
86
|
|
|
86
87
|
|
|
87
88
|
__all__ = (
|
|
88
|
-
"
|
|
89
|
-
"SCALAR_NODE",
|
|
89
|
+
"ComposeError",
|
|
90
90
|
"DataNode",
|
|
91
91
|
"Node",
|
|
92
|
-
"
|
|
92
|
+
"SCALAR_NODE",
|
|
93
|
+
"ScalarNode",
|
|
93
94
|
)
|
telegrinder/node/composer.py
CHANGED
|
@@ -6,7 +6,7 @@ from telegrinder.node import Node
|
|
|
6
6
|
|
|
7
7
|
class NodeSession:
|
|
8
8
|
def __init__(
|
|
9
|
-
self,
|
|
9
|
+
self,
|
|
10
10
|
value: typing.Any,
|
|
11
11
|
subnodes: dict[str, typing.Self],
|
|
12
12
|
generator: typing.AsyncGenerator[typing.Any, None] | None = None,
|
|
@@ -14,11 +14,11 @@ class NodeSession:
|
|
|
14
14
|
self.value = value
|
|
15
15
|
self.subnodes = subnodes
|
|
16
16
|
self.generator = generator
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
async def close(self, with_value: typing.Any | None = None) -> None:
|
|
19
19
|
for subnode in self.subnodes.values():
|
|
20
20
|
await subnode.close()
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
if self.generator is None:
|
|
23
23
|
return
|
|
24
24
|
try:
|
|
@@ -36,7 +36,7 @@ class NodeCollection:
|
|
|
36
36
|
|
|
37
37
|
def values(self) -> dict[str, typing.Any]:
|
|
38
38
|
return {name: session.value for name, session in self.sessions.items()}
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
async def close_all(self, with_value: typing.Any | None = None) -> None:
|
|
41
41
|
for session in self.sessions.values():
|
|
42
42
|
await session.close(with_value)
|
|
@@ -64,7 +64,7 @@ async def compose_node(
|
|
|
64
64
|
else:
|
|
65
65
|
generator = None
|
|
66
66
|
value = await _node.compose(**context.values()) # type: ignore
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
return NodeSession(value, context.sessions, generator)
|
|
69
69
|
|
|
70
70
|
|
telegrinder/node/container.py
CHANGED
|
@@ -13,7 +13,7 @@ class ContainerNode(Node):
|
|
|
13
13
|
@classmethod
|
|
14
14
|
def get_sub_nodes(cls) -> dict[str, type["Node"]]:
|
|
15
15
|
return {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
@classmethod
|
|
18
18
|
def link_nodes(cls, linked_nodes: list[type[Node]]) -> type["ContainerNode"]:
|
|
19
19
|
return type("_ContainerNode", (cls,), {"linked_nodes": linked_nodes})
|
telegrinder/node/message.py
CHANGED
|
@@ -9,8 +9,10 @@ from .update import UpdateNode
|
|
|
9
9
|
class MessageNode(ScalarNode, MessageCute):
|
|
10
10
|
@classmethod
|
|
11
11
|
async def compose(cls, update: UpdateNode) -> typing.Self:
|
|
12
|
+
if not update.message:
|
|
13
|
+
raise ComposeError
|
|
12
14
|
return cls(
|
|
13
|
-
**update.message.
|
|
15
|
+
**update.message.unwrap().to_dict(),
|
|
14
16
|
api=update.api,
|
|
15
17
|
)
|
|
16
18
|
|
telegrinder/node/rule.py
CHANGED
|
@@ -26,7 +26,7 @@ class RuleContext(dict):
|
|
|
26
26
|
@classmethod
|
|
27
27
|
def as_node(cls) -> type[typing.Self]:
|
|
28
28
|
return cls
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
@classmethod
|
|
31
31
|
def get_sub_nodes(cls) -> dict:
|
|
32
32
|
return {"update": UpdateNode}
|
|
@@ -37,16 +37,16 @@ class RuleContext(dict):
|
|
|
37
37
|
|
|
38
38
|
def __new__(cls, *rules: ABCRule) -> type[Node]:
|
|
39
39
|
return type("_RuleNode", (cls,), {"dataclass": dict, "rules": rules}) # type: ignore
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
def __class_getitem__(cls, item: tuple[ABCRule, ...]) -> typing.Self:
|
|
42
42
|
if not isinstance(item, tuple):
|
|
43
43
|
item = (item,)
|
|
44
44
|
return cls(*item)
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
@staticmethod
|
|
47
47
|
def generate_dataclass(cls_: type["RuleContext"]): # noqa: ANN205
|
|
48
48
|
return dataclasses.dataclass(type(cls_.__name__, (object,), dict(cls_.__dict__)))
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
def __init_subclass__(cls) -> None:
|
|
51
51
|
if cls.__name__ == "_RuleNode":
|
|
52
52
|
return
|
telegrinder/node/source.py
CHANGED
|
@@ -21,9 +21,13 @@ class Source(DataNode):
|
|
|
21
21
|
chat=message.chat,
|
|
22
22
|
thread_id=message.message_thread_id.unwrap_or_none(),
|
|
23
23
|
)
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
async def send(self, text: str) -> Message:
|
|
26
|
-
result = await self.api.send_message(
|
|
26
|
+
result = await self.api.send_message(
|
|
27
|
+
chat_id=self.chat.id,
|
|
28
|
+
message_thread_id=self.thread_id,
|
|
29
|
+
text=text,
|
|
30
|
+
)
|
|
27
31
|
return result.unwrap()
|
|
28
32
|
|
|
29
33
|
|
telegrinder/node/text.py
CHANGED
|
@@ -7,7 +7,9 @@ from .message import MessageNode
|
|
|
7
7
|
class Text(ScalarNode, str):
|
|
8
8
|
@classmethod
|
|
9
9
|
async def compose(cls, message: MessageNode) -> typing.Self:
|
|
10
|
-
|
|
10
|
+
if not message.text:
|
|
11
|
+
raise ComposeError("Message has no text")
|
|
12
|
+
return cls(message.text.unwrap())
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
__all__ = ("Text",)
|
|
@@ -20,7 +20,7 @@ def error_on_none(value: T | None) -> T:
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def generate(
|
|
23
|
-
subnodes: tuple[type[Node], ...],
|
|
23
|
+
subnodes: tuple[type[Node], ...],
|
|
24
24
|
func: typing.Callable[..., typing.Any],
|
|
25
25
|
casts: tuple[typing.Callable, ...] = (cast_false_to_none, error_on_none),
|
|
26
26
|
) -> type[ContainerNode]:
|
telegrinder/tools/__init__.py
CHANGED
|
@@ -66,6 +66,7 @@ from .keyboard import (
|
|
|
66
66
|
Keyboard,
|
|
67
67
|
RowButtons,
|
|
68
68
|
)
|
|
69
|
+
from .limited_dict import LimitedDict
|
|
69
70
|
from .loop_wrapper import ABCLoopWrapper, DelayedTask, Lifespan, LoopWrapper
|
|
70
71
|
from .magic import magic_bundle, resolve_arg_names
|
|
71
72
|
from .parse_mode import ParseMode
|
|
@@ -98,8 +99,9 @@ __all__ = (
|
|
|
98
99
|
"Keyboard",
|
|
99
100
|
"KeyboardSetBase",
|
|
100
101
|
"KeyboardSetYAML",
|
|
101
|
-
"Link",
|
|
102
102
|
"Lifespan",
|
|
103
|
+
"Link",
|
|
104
|
+
"LimitedDict",
|
|
103
105
|
"LoopWrapper",
|
|
104
106
|
"Mention",
|
|
105
107
|
"ParseMode",
|
telegrinder/tools/buttons.py
CHANGED
|
@@ -63,15 +63,13 @@ class InlineButton(BaseButton):
|
|
|
63
63
|
url: str | None = None
|
|
64
64
|
login_url: dict[str, typing.Any] | LoginUrl | None = None
|
|
65
65
|
pay: bool | None = None
|
|
66
|
-
callback_data:
|
|
67
|
-
str | dict[str, typing.Any] | DataclassInstance | msgspec.Struct | None
|
|
68
|
-
) = None
|
|
66
|
+
callback_data: str | dict[str, typing.Any] | DataclassInstance | msgspec.Struct | None = None
|
|
69
67
|
callback_game: dict[str, typing.Any] | CallbackGame | None = None
|
|
70
68
|
switch_inline_query: str | None = None
|
|
71
69
|
switch_inline_query_current_chat: str | None = None
|
|
72
|
-
switch_inline_query_chosen_chat: (
|
|
73
|
-
|
|
74
|
-
)
|
|
70
|
+
switch_inline_query_chosen_chat: dict[str, typing.Any] | SwitchInlineQueryChosenChat | None = (
|
|
71
|
+
None
|
|
72
|
+
)
|
|
75
73
|
web_app: dict[str, typing.Any] | WebAppInfo | None = None
|
|
76
74
|
|
|
77
75
|
|
|
@@ -4,10 +4,9 @@ from abc import ABC, abstractmethod
|
|
|
4
4
|
from fntypes.result import Result
|
|
5
5
|
|
|
6
6
|
from telegrinder.api import ABCAPI
|
|
7
|
-
from telegrinder.bot.cute_types import BaseCute
|
|
8
7
|
from telegrinder.bot.dispatch.context import Context
|
|
9
8
|
|
|
10
|
-
EventT = typing.TypeVar("EventT"
|
|
9
|
+
EventT = typing.TypeVar("EventT")
|
|
11
10
|
Handler = typing.Callable[typing.Concatenate[EventT, ...], typing.Awaitable[typing.Any]]
|
|
12
11
|
|
|
13
12
|
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class CatcherError(TypeError):
|
|
5
|
-
def __init__(self, exc: typing.Any, error: str) -> None:
|
|
1
|
+
class CatcherError(BaseException):
|
|
2
|
+
def __init__(self, exc: BaseException, message: str) -> None:
|
|
6
3
|
self.exc = exc
|
|
7
|
-
self.
|
|
4
|
+
self.message = message
|
|
8
5
|
|
|
9
6
|
|
|
10
7
|
__all__ = ("CatcherError",)
|
|
@@ -57,8 +57,7 @@ class Catcher(typing.Generic[EventT]):
|
|
|
57
57
|
) -> Result[typing.Any, BaseException]:
|
|
58
58
|
if self.match_exception(exception):
|
|
59
59
|
logger.debug(
|
|
60
|
-
"Catcher {!r} caught an exception in handler {!r}, "
|
|
61
|
-
"running catcher...".format(
|
|
60
|
+
"Catcher {!r} caught an exception in handler {!r}, " "running catcher...".format(
|
|
62
61
|
self.func.__name__,
|
|
63
62
|
handler_name,
|
|
64
63
|
)
|
|
@@ -71,7 +70,7 @@ class Catcher(typing.Generic[EventT]):
|
|
|
71
70
|
)
|
|
72
71
|
logger.debug("Failed to match exception {!r}!", exception.__class__.__name__)
|
|
73
72
|
return Error(exception)
|
|
74
|
-
|
|
73
|
+
|
|
75
74
|
def match_exception(self, exception: BaseException) -> bool:
|
|
76
75
|
for exc in self.exceptions:
|
|
77
76
|
if isinstance(exc, type) and type(exception) is exc:
|
|
@@ -84,10 +83,11 @@ class Catcher(typing.Generic[EventT]):
|
|
|
84
83
|
class ErrorHandler(ABCErrorHandler[EventT]):
|
|
85
84
|
def __init__(self, catcher: Catcher[EventT] | None = None, /) -> None:
|
|
86
85
|
self.catcher = catcher
|
|
87
|
-
|
|
86
|
+
|
|
88
87
|
def __repr__(self) -> str:
|
|
89
88
|
return (
|
|
90
|
-
"<
|
|
89
|
+
"<{}: exceptions_handled=[{}], catcher={!r}>".format(
|
|
90
|
+
self.__class__.__name__,
|
|
91
91
|
", ".join(
|
|
92
92
|
e.__name__ if isinstance(e, type) else repr(e)
|
|
93
93
|
for e in self.catcher.exceptions
|
|
@@ -95,7 +95,7 @@ class ErrorHandler(ABCErrorHandler[EventT]):
|
|
|
95
95
|
self.catcher,
|
|
96
96
|
)
|
|
97
97
|
if self.catcher is not None
|
|
98
|
-
else "<
|
|
98
|
+
else "<{}: No catcher>".format(self.__class__.__name__)
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
def __call__(
|
|
@@ -106,7 +106,8 @@ class ErrorHandler(ABCErrorHandler[EventT]):
|
|
|
106
106
|
ignore_errors: bool = False,
|
|
107
107
|
):
|
|
108
108
|
"""Register the catcher.
|
|
109
|
-
|
|
109
|
+
|
|
110
|
+
:param logging: Logging the result of the catcher at the level 'DEBUG'.
|
|
110
111
|
:param raise_exception: Raise an exception if the catcher hasn't started.
|
|
111
112
|
:param ignore_errors: Ignore errors that may occur.
|
|
112
113
|
"""
|
|
@@ -121,8 +122,9 @@ class ErrorHandler(ABCErrorHandler[EventT]):
|
|
|
121
122
|
ignore_errors=ignore_errors,
|
|
122
123
|
)
|
|
123
124
|
return func
|
|
125
|
+
|
|
124
126
|
return decorator
|
|
125
|
-
|
|
127
|
+
|
|
126
128
|
async def process(
|
|
127
129
|
self,
|
|
128
130
|
handler: Handler[EventT],
|
|
@@ -135,23 +137,25 @@ class ErrorHandler(ABCErrorHandler[EventT]):
|
|
|
135
137
|
try:
|
|
136
138
|
return await self.catcher(handler, event, api, ctx)
|
|
137
139
|
except BaseException as exc:
|
|
138
|
-
return Error(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
return Error(
|
|
141
|
+
CatcherError(
|
|
142
|
+
exc,
|
|
143
|
+
"Exception {} was occurred during the running catcher {!r}.".format(
|
|
144
|
+
repr(exc), self.catcher.func.__name__
|
|
145
|
+
),
|
|
142
146
|
)
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
def process_catcher_error(self, error: CatcherError) -> Result[None,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def process_catcher_error(self, error: CatcherError) -> Result[None, BaseException]:
|
|
146
150
|
assert self.catcher is not None
|
|
147
|
-
|
|
151
|
+
|
|
148
152
|
if self.catcher.raise_exception:
|
|
149
153
|
raise error.exc from None
|
|
150
|
-
if not self.catcher.ignore_errors:
|
|
151
|
-
return Error(error.error)
|
|
152
154
|
if self.catcher.logging:
|
|
153
|
-
logger.error(error.
|
|
154
|
-
|
|
155
|
+
logger.error(error.message)
|
|
156
|
+
if not self.catcher.ignore_errors:
|
|
157
|
+
return Error(error.exc)
|
|
158
|
+
|
|
155
159
|
return Ok(None)
|
|
156
160
|
|
|
157
161
|
async def run(
|
|
@@ -160,16 +164,22 @@ class ErrorHandler(ABCErrorHandler[EventT]):
|
|
|
160
164
|
event: EventT,
|
|
161
165
|
api: ABCAPI,
|
|
162
166
|
ctx: Context,
|
|
163
|
-
) -> Result[typing.Any,
|
|
167
|
+
) -> Result[typing.Any, BaseException]:
|
|
164
168
|
if not self.catcher:
|
|
165
169
|
return Ok(await handler(event, **magic_bundle(handler, ctx))) # type: ignore
|
|
166
|
-
|
|
170
|
+
|
|
167
171
|
match await self.process(handler, event, api, ctx):
|
|
168
|
-
case Ok(
|
|
172
|
+
case Ok(value) as ok:
|
|
173
|
+
if self.catcher.logging:
|
|
174
|
+
logger.debug(
|
|
175
|
+
"Catcher {!r} returned a value: {!r}",
|
|
176
|
+
self.catcher.func.__name__,
|
|
177
|
+
value,
|
|
178
|
+
)
|
|
169
179
|
return ok
|
|
170
180
|
case Error(exc) as err:
|
|
171
181
|
if isinstance(exc, CatcherError):
|
|
172
|
-
return self.process_catcher_error(exc)
|
|
182
|
+
return self.process_catcher_error(exc)
|
|
173
183
|
if self.catcher.ignore_errors:
|
|
174
184
|
return Ok(None)
|
|
175
185
|
return err
|