telegrinder 0.1.dev19__py3-none-any.whl → 0.1.dev158__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 +129 -22
- telegrinder/api/__init__.py +11 -2
- telegrinder/api/abc.py +25 -9
- telegrinder/api/api.py +29 -24
- telegrinder/api/error.py +14 -4
- telegrinder/api/response.py +11 -7
- telegrinder/bot/__init__.py +68 -7
- telegrinder/bot/bot.py +30 -24
- telegrinder/bot/cute_types/__init__.py +11 -1
- telegrinder/bot/cute_types/base.py +47 -0
- telegrinder/bot/cute_types/callback_query.py +64 -14
- telegrinder/bot/cute_types/inline_query.py +22 -16
- telegrinder/bot/cute_types/message.py +163 -43
- telegrinder/bot/cute_types/update.py +23 -0
- telegrinder/bot/dispatch/__init__.py +56 -3
- telegrinder/bot/dispatch/abc.py +9 -7
- telegrinder/bot/dispatch/composition.py +74 -0
- telegrinder/bot/dispatch/context.py +71 -0
- telegrinder/bot/dispatch/dispatch.py +86 -49
- telegrinder/bot/dispatch/handler/__init__.py +3 -0
- telegrinder/bot/dispatch/handler/abc.py +11 -5
- telegrinder/bot/dispatch/handler/func.py +41 -32
- telegrinder/bot/dispatch/handler/message_reply.py +46 -0
- telegrinder/bot/dispatch/middleware/__init__.py +2 -0
- telegrinder/bot/dispatch/middleware/abc.py +10 -4
- telegrinder/bot/dispatch/process.py +53 -49
- telegrinder/bot/dispatch/return_manager/__init__.py +19 -0
- telegrinder/bot/dispatch/return_manager/abc.py +95 -0
- telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
- telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
- telegrinder/bot/dispatch/return_manager/message.py +25 -0
- telegrinder/bot/dispatch/view/__init__.py +14 -2
- telegrinder/bot/dispatch/view/abc.py +121 -2
- telegrinder/bot/dispatch/view/box.py +38 -0
- telegrinder/bot/dispatch/view/callback_query.py +13 -38
- telegrinder/bot/dispatch/view/inline_query.py +11 -38
- telegrinder/bot/dispatch/view/message.py +11 -46
- telegrinder/bot/dispatch/waiter_machine/__init__.py +9 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +116 -0
- telegrinder/bot/dispatch/waiter_machine/middleware.py +76 -0
- telegrinder/bot/dispatch/waiter_machine/short_state.py +37 -0
- telegrinder/bot/polling/__init__.py +2 -0
- telegrinder/bot/polling/abc.py +11 -4
- telegrinder/bot/polling/polling.py +89 -40
- telegrinder/bot/rules/__init__.py +92 -5
- telegrinder/bot/rules/abc.py +81 -63
- telegrinder/bot/rules/adapter/__init__.py +11 -0
- telegrinder/bot/rules/adapter/abc.py +21 -0
- telegrinder/bot/rules/adapter/errors.py +5 -0
- telegrinder/bot/rules/adapter/event.py +43 -0
- telegrinder/bot/rules/adapter/raw_update.py +24 -0
- telegrinder/bot/rules/callback_data.py +159 -38
- telegrinder/bot/rules/command.py +116 -0
- telegrinder/bot/rules/enum_text.py +28 -0
- telegrinder/bot/rules/func.py +17 -17
- telegrinder/bot/rules/fuzzy.py +13 -10
- telegrinder/bot/rules/inline.py +61 -0
- telegrinder/bot/rules/integer.py +12 -7
- telegrinder/bot/rules/is_from.py +148 -7
- telegrinder/bot/rules/markup.py +21 -18
- telegrinder/bot/rules/mention.py +17 -0
- telegrinder/bot/rules/message_entities.py +33 -0
- telegrinder/bot/rules/regex.py +27 -19
- telegrinder/bot/rules/rule_enum.py +74 -0
- telegrinder/bot/rules/start.py +42 -0
- telegrinder/bot/rules/text.py +23 -14
- telegrinder/bot/scenario/__init__.py +2 -0
- telegrinder/bot/scenario/abc.py +12 -5
- telegrinder/bot/scenario/checkbox.py +48 -30
- telegrinder/bot/scenario/choice.py +16 -10
- telegrinder/client/__init__.py +2 -0
- telegrinder/client/abc.py +8 -21
- telegrinder/client/aiohttp.py +30 -21
- telegrinder/model.py +68 -37
- telegrinder/modules.py +189 -21
- telegrinder/msgspec_json.py +14 -0
- telegrinder/msgspec_utils.py +207 -0
- telegrinder/node/__init__.py +31 -0
- telegrinder/node/attachment.py +71 -0
- telegrinder/node/base.py +93 -0
- telegrinder/node/composer.py +71 -0
- telegrinder/node/container.py +22 -0
- telegrinder/node/message.py +18 -0
- telegrinder/node/rule.py +56 -0
- telegrinder/node/source.py +31 -0
- telegrinder/node/text.py +13 -0
- telegrinder/node/tools/__init__.py +3 -0
- telegrinder/node/tools/generator.py +40 -0
- telegrinder/node/update.py +12 -0
- telegrinder/rules.py +1 -1
- telegrinder/tools/__init__.py +165 -4
- telegrinder/tools/buttons.py +75 -51
- telegrinder/tools/error_handler/__init__.py +8 -0
- telegrinder/tools/error_handler/abc.py +30 -0
- telegrinder/tools/error_handler/error_handler.py +156 -0
- telegrinder/tools/formatting/__init__.py +81 -3
- telegrinder/tools/formatting/html.py +283 -37
- telegrinder/tools/formatting/links.py +32 -0
- telegrinder/tools/formatting/spec_html_formats.py +121 -0
- telegrinder/tools/global_context/__init__.py +12 -0
- telegrinder/tools/global_context/abc.py +66 -0
- telegrinder/tools/global_context/global_context.py +451 -0
- telegrinder/tools/global_context/telegrinder_ctx.py +25 -0
- telegrinder/tools/i18n/__init__.py +12 -0
- telegrinder/tools/i18n/base.py +31 -0
- telegrinder/tools/i18n/middleware/__init__.py +3 -0
- telegrinder/tools/i18n/middleware/base.py +26 -0
- telegrinder/tools/i18n/simple.py +48 -0
- telegrinder/tools/inline_query.py +684 -0
- telegrinder/tools/kb_set/__init__.py +2 -0
- telegrinder/tools/kb_set/base.py +3 -0
- telegrinder/tools/kb_set/yaml.py +28 -17
- telegrinder/tools/keyboard.py +84 -62
- telegrinder/tools/loop_wrapper/__init__.py +4 -0
- telegrinder/tools/loop_wrapper/abc.py +18 -0
- telegrinder/tools/loop_wrapper/loop_wrapper.py +132 -0
- telegrinder/tools/magic.py +48 -23
- telegrinder/tools/parse_mode.py +1 -2
- telegrinder/types/__init__.py +1 -0
- telegrinder/types/enums.py +651 -0
- telegrinder/types/methods.py +3933 -1128
- telegrinder/types/objects.py +4755 -1633
- {telegrinder-0.1.dev19.dist-info → telegrinder-0.1.dev158.dist-info}/LICENSE +2 -1
- telegrinder-0.1.dev158.dist-info/METADATA +108 -0
- telegrinder-0.1.dev158.dist-info/RECORD +126 -0
- {telegrinder-0.1.dev19.dist-info → telegrinder-0.1.dev158.dist-info}/WHEEL +1 -1
- telegrinder/bot/dispatch/waiter.py +0 -37
- telegrinder/result.py +0 -38
- telegrinder/tools/formatting/abc.py +0 -52
- telegrinder/tools/formatting/markdown.py +0 -57
- telegrinder/typegen/__init__.py +0 -1
- telegrinder/typegen/__main__.py +0 -3
- telegrinder/typegen/nicification.py +0 -20
- telegrinder/typegen/schema_generator.py +0 -259
- telegrinder-0.1.dev19.dist-info/METADATA +0 -22
- telegrinder-0.1.dev19.dist-info/RECORD +0 -74
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import types # noqa: TCH003
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import fntypes.option
|
|
5
|
+
import msgspec
|
|
6
|
+
from fntypes.co import Error, Ok, Result, Variative
|
|
7
|
+
|
|
8
|
+
T = typing.TypeVar("T")
|
|
9
|
+
Ts = typing.TypeVarTuple("Ts")
|
|
10
|
+
|
|
11
|
+
if typing.TYPE_CHECKING:
|
|
12
|
+
from fntypes.option import Option
|
|
13
|
+
else:
|
|
14
|
+
|
|
15
|
+
Value = typing.TypeVar("Value")
|
|
16
|
+
|
|
17
|
+
class OptionMeta(type):
|
|
18
|
+
def __instancecheck__(cls, __instance: typing.Any) -> bool:
|
|
19
|
+
return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Option(typing.Generic[Value], metaclass=OptionMeta):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
DecHook: typing.TypeAlias = typing.Callable[[type[T], object], object]
|
|
26
|
+
EncHook: typing.TypeAlias = typing.Callable[[T], object]
|
|
27
|
+
|
|
28
|
+
Nothing: typing.Final[fntypes.option.Nothing] = fntypes.option.Nothing()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_origin(t: type[T]) -> type[T]:
|
|
32
|
+
return typing.cast(T, typing.get_origin(t)) or t
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def repr_type(t: type) -> str:
|
|
36
|
+
return getattr(t, "__name__", repr(get_origin(t)))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, msgspec.ValidationError]:
|
|
40
|
+
try:
|
|
41
|
+
return Ok(decoder.convert(obj, type=t, strict=True))
|
|
42
|
+
except msgspec.ValidationError as exc:
|
|
43
|
+
return Error(exc)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def option_dec_hook(tp: type["Option[typing.Any]"], obj: typing.Any) -> typing.Any:
|
|
47
|
+
if obj is None:
|
|
48
|
+
return Nothing
|
|
49
|
+
generic_args = typing.get_args(tp)
|
|
50
|
+
value_type: typing.Any | type[typing.Any] = typing.Any if not generic_args else generic_args[0]
|
|
51
|
+
return msgspec_convert({"value": obj}, fntypes.option.Some[value_type]).unwrap()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
|
|
55
|
+
union_types = typing.get_args(tp)
|
|
56
|
+
|
|
57
|
+
if isinstance(obj, dict):
|
|
58
|
+
struct_fields_match_sums: dict[type[msgspec.Struct], int] = {
|
|
59
|
+
m: sum(1 for k in obj if k in m.__struct_fields__)
|
|
60
|
+
for m in union_types
|
|
61
|
+
if issubclass(get_origin(m), msgspec.Struct)
|
|
62
|
+
}
|
|
63
|
+
union_types = tuple(t for t in union_types if t not in struct_fields_match_sums)
|
|
64
|
+
reverse = False
|
|
65
|
+
|
|
66
|
+
if len(set(struct_fields_match_sums.values())) != len(struct_fields_match_sums.values()):
|
|
67
|
+
struct_fields_match_sums = {m: len(m.__struct_fields__) for m in struct_fields_match_sums}
|
|
68
|
+
reverse = True
|
|
69
|
+
|
|
70
|
+
union_types = (
|
|
71
|
+
*sorted(struct_fields_match_sums, key=lambda k: struct_fields_match_sums[k], reverse=reverse),
|
|
72
|
+
*union_types,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
for t in union_types:
|
|
76
|
+
match msgspec_convert(obj, t):
|
|
77
|
+
case Ok(value):
|
|
78
|
+
return tp(value)
|
|
79
|
+
|
|
80
|
+
raise TypeError(
|
|
81
|
+
"Object of type `{}` does not belong to types `{}`".format(
|
|
82
|
+
repr_type(type(obj)),
|
|
83
|
+
" | ".join(map(repr_type, union_types)),
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def option_enc_hook(obj: "Option[typing.Any]") -> typing.Any | None:
|
|
89
|
+
return obj.value if isinstance(obj, fntypes.option.Some) else None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def variative_enc_hook(obj: Variative) -> typing.Any:
|
|
93
|
+
return typing.cast(typing.Any, obj.v)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Decoder:
|
|
97
|
+
def __init__(self) -> None:
|
|
98
|
+
self.dec_hooks: dict[type | types.UnionType, DecHook[typing.Any]] = {
|
|
99
|
+
Option: option_dec_hook,
|
|
100
|
+
Variative: variative_dec_hook,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def add_dec_hook(self, tp: type[T]):
|
|
104
|
+
def decorator(func: DecHook[T]) -> DecHook[T]:
|
|
105
|
+
return self.dec_hooks.setdefault(get_origin(tp), func)
|
|
106
|
+
|
|
107
|
+
return decorator
|
|
108
|
+
|
|
109
|
+
def dec_hook(self, tp: type[typing.Any], obj: object) -> object:
|
|
110
|
+
origin_type = t if isinstance((t := get_origin(tp)), type) else type(t)
|
|
111
|
+
if origin_type not in self.dec_hooks:
|
|
112
|
+
raise TypeError(
|
|
113
|
+
f"Unknown type `{repr_type(origin_type)}`. "
|
|
114
|
+
"You can implement decode hook for this type."
|
|
115
|
+
)
|
|
116
|
+
return self.dec_hooks[origin_type](tp, obj)
|
|
117
|
+
|
|
118
|
+
def convert(
|
|
119
|
+
self,
|
|
120
|
+
obj: object,
|
|
121
|
+
*,
|
|
122
|
+
type: type[T] = dict,
|
|
123
|
+
strict: bool = True,
|
|
124
|
+
from_attributes: bool = False,
|
|
125
|
+
builtin_types: typing.Iterable[type] | None = None,
|
|
126
|
+
str_keys: bool = False,
|
|
127
|
+
) -> T:
|
|
128
|
+
return msgspec.convert(
|
|
129
|
+
obj,
|
|
130
|
+
type,
|
|
131
|
+
strict=strict,
|
|
132
|
+
from_attributes=from_attributes,
|
|
133
|
+
dec_hook=self.dec_hook,
|
|
134
|
+
builtin_types=builtin_types,
|
|
135
|
+
str_keys=str_keys,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def decode(
|
|
139
|
+
self,
|
|
140
|
+
buf: str | bytes,
|
|
141
|
+
*,
|
|
142
|
+
type: type[T] = dict,
|
|
143
|
+
strict: bool = True,
|
|
144
|
+
) -> T:
|
|
145
|
+
return msgspec.json.decode(
|
|
146
|
+
buf,
|
|
147
|
+
type=type,
|
|
148
|
+
strict=strict,
|
|
149
|
+
dec_hook=self.dec_hook,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class Encoder:
|
|
154
|
+
def __init__(self) -> None:
|
|
155
|
+
self.enc_hooks: dict[type, EncHook] = {
|
|
156
|
+
fntypes.option.Some: option_enc_hook,
|
|
157
|
+
fntypes.option.Nothing: option_enc_hook,
|
|
158
|
+
Variative: variative_enc_hook,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
def add_dec_hook(self, tp: type[T]):
|
|
162
|
+
def decorator(func: EncHook[T]) -> EncHook[T]:
|
|
163
|
+
return self.enc_hooks.setdefault(get_origin(tp), func)
|
|
164
|
+
|
|
165
|
+
return decorator
|
|
166
|
+
|
|
167
|
+
def enc_hook(self, obj: object) -> object:
|
|
168
|
+
origin_type = get_origin(type(obj))
|
|
169
|
+
if origin_type not in self.enc_hooks:
|
|
170
|
+
raise NotImplementedError(
|
|
171
|
+
"Not implemented encode hook for "
|
|
172
|
+
f"object of type `{repr_type(origin_type)}`."
|
|
173
|
+
)
|
|
174
|
+
return self.enc_hooks[origin_type](obj)
|
|
175
|
+
|
|
176
|
+
@typing.overload
|
|
177
|
+
def encode(self, obj: typing.Any) -> str:
|
|
178
|
+
...
|
|
179
|
+
|
|
180
|
+
@typing.overload
|
|
181
|
+
def encode(self, obj: typing.Any, *, as_str: bool = False) -> bytes:
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
def encode(self, obj: typing.Any, *, as_str: bool = True) -> str | bytes:
|
|
185
|
+
buf = msgspec.json.encode(obj, enc_hook=self.enc_hook)
|
|
186
|
+
return buf.decode() if as_str else buf
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
decoder: typing.Final[Decoder] = Decoder()
|
|
190
|
+
encoder: typing.Final[Encoder] = Encoder()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
__all__ = (
|
|
194
|
+
"Decoder",
|
|
195
|
+
"Encoder",
|
|
196
|
+
"Option",
|
|
197
|
+
"Nothing",
|
|
198
|
+
"get_origin",
|
|
199
|
+
"repr_type",
|
|
200
|
+
"msgspec_convert",
|
|
201
|
+
"option_dec_hook",
|
|
202
|
+
"option_enc_hook",
|
|
203
|
+
"variative_dec_hook",
|
|
204
|
+
"variative_enc_hook",
|
|
205
|
+
"decoder",
|
|
206
|
+
"encoder",
|
|
207
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .attachment import Attachment, Audio, Photo, Video
|
|
2
|
+
from .base import ComposeError, DataNode, Node, ScalarNode
|
|
3
|
+
from .composer import NodeCollection, NodeSession, compose_node
|
|
4
|
+
from .container import ContainerNode
|
|
5
|
+
from .message import MessageNode
|
|
6
|
+
from .rule import RuleContext
|
|
7
|
+
from .source import Source
|
|
8
|
+
from .text import Text
|
|
9
|
+
from .tools import generate
|
|
10
|
+
from .update import UpdateNode
|
|
11
|
+
|
|
12
|
+
__all__ = (
|
|
13
|
+
"Node",
|
|
14
|
+
"DataNode",
|
|
15
|
+
"ScalarNode",
|
|
16
|
+
"Attachment",
|
|
17
|
+
"Photo",
|
|
18
|
+
"Video",
|
|
19
|
+
"Text",
|
|
20
|
+
"Audio",
|
|
21
|
+
"UpdateNode",
|
|
22
|
+
"compose_node",
|
|
23
|
+
"ComposeError",
|
|
24
|
+
"MessageNode",
|
|
25
|
+
"Source",
|
|
26
|
+
"NodeSession",
|
|
27
|
+
"NodeCollection",
|
|
28
|
+
"ContainerNode",
|
|
29
|
+
"generate",
|
|
30
|
+
"RuleContext",
|
|
31
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from fntypes.option import Nothing
|
|
5
|
+
|
|
6
|
+
import telegrinder.types
|
|
7
|
+
from telegrinder.msgspec_utils import Option
|
|
8
|
+
|
|
9
|
+
from .base import ComposeError, DataNode, ScalarNode
|
|
10
|
+
from .message import MessageNode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclasses.dataclass
|
|
14
|
+
class Attachment(DataNode):
|
|
15
|
+
attachment_type: typing.Literal["audio", "document", "photo", "poll", "video"]
|
|
16
|
+
_: dataclasses.KW_ONLY
|
|
17
|
+
audio: Option[telegrinder.types.Audio] = dataclasses.field(default_factory=lambda: Nothing())
|
|
18
|
+
document: Option[telegrinder.types.Document] = dataclasses.field(default_factory=lambda: Nothing())
|
|
19
|
+
photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(default_factory=lambda: Nothing())
|
|
20
|
+
poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing())
|
|
21
|
+
video: Option[telegrinder.types.Video] = dataclasses.field(default_factory=lambda: Nothing())
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
async def compose(cls, message: MessageNode) -> "Attachment":
|
|
25
|
+
for attachment_type in ("audio", "document", "photo", "poll", "video"):
|
|
26
|
+
if (attachment := getattr(message, attachment_type, None)) is not None:
|
|
27
|
+
return cls(attachment_type, **{attachment_type: attachment})
|
|
28
|
+
return cls.compose_error("No attachment found in message")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclasses.dataclass
|
|
32
|
+
class Photo(DataNode):
|
|
33
|
+
sizes: list[telegrinder.types.PhotoSize]
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
async def compose(cls, attachment: Attachment) -> typing.Self:
|
|
37
|
+
return cls(attachment.photo.expect(ComposeError("Attachment is not an photo")))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Video(ScalarNode, telegrinder.types.Video):
|
|
41
|
+
@classmethod
|
|
42
|
+
async def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
|
|
43
|
+
return attachment.video.expect(ComposeError("Attachment is not an video"))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Audio(ScalarNode, telegrinder.types.Audio):
|
|
47
|
+
@classmethod
|
|
48
|
+
async def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
|
|
49
|
+
return attachment.audio.expect(ComposeError("Attachment is not an audio"))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Document(ScalarNode, telegrinder.types.Document):
|
|
53
|
+
@classmethod
|
|
54
|
+
async def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
|
|
55
|
+
return attachment.document.expect(ComposeError("Attachment is not an document"))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Poll(ScalarNode, telegrinder.types.Poll):
|
|
59
|
+
@classmethod
|
|
60
|
+
async def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
|
|
61
|
+
return attachment.poll.expect(ComposeError("Attachment is not an poll"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
__all__ = (
|
|
65
|
+
"Attachment",
|
|
66
|
+
"Audio",
|
|
67
|
+
"Document",
|
|
68
|
+
"Photo",
|
|
69
|
+
"Poll",
|
|
70
|
+
"Video",
|
|
71
|
+
)
|
telegrinder/node/base.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import inspect
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
ComposeResult: typing.TypeAlias = typing.Coroutine[typing.Any, typing.Any, typing.Any] | typing.AsyncGenerator[typing.Any, None]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ComposeError(BaseException):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Node(abc.ABC):
|
|
13
|
+
node: str = "node"
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
@abc.abstractmethod
|
|
17
|
+
def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def compose_error(cls, error: str | None = None) -> typing.NoReturn:
|
|
22
|
+
raise ComposeError(error)
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def get_sub_nodes(cls) -> dict[str, type[typing.Self]]:
|
|
26
|
+
parameters = inspect.signature(cls.compose).parameters
|
|
27
|
+
|
|
28
|
+
sub_nodes = {}
|
|
29
|
+
for name, param in parameters.items():
|
|
30
|
+
if param.annotation is inspect._empty:
|
|
31
|
+
continue
|
|
32
|
+
node = param.annotation
|
|
33
|
+
sub_nodes[name] = node
|
|
34
|
+
return sub_nodes
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def as_node(cls) -> type[typing.Self]:
|
|
38
|
+
return cls
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def is_generator(cls) -> bool:
|
|
42
|
+
return inspect.isasyncgenfunction(cls.compose)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DataNode(Node, abc.ABC):
|
|
46
|
+
node = "data"
|
|
47
|
+
|
|
48
|
+
@typing.dataclass_transform()
|
|
49
|
+
@classmethod
|
|
50
|
+
@abc.abstractmethod
|
|
51
|
+
async def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ScalarNodeProto(Node, abc.ABC):
|
|
56
|
+
@classmethod
|
|
57
|
+
@abc.abstractmethod
|
|
58
|
+
async def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
SCALAR_NODE = type("ScalarNode", (), {"node": "scalar"})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if typing.TYPE_CHECKING:
|
|
66
|
+
|
|
67
|
+
class ScalarNode(ScalarNodeProto, abc.ABC):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
else:
|
|
71
|
+
|
|
72
|
+
def create_node(cls, bases, dct):
|
|
73
|
+
dct.update(cls.__dict__)
|
|
74
|
+
return type(cls.__name__, bases, dct)
|
|
75
|
+
|
|
76
|
+
def create_class(name, bases, dct):
|
|
77
|
+
return type(
|
|
78
|
+
"Scalar",
|
|
79
|
+
(SCALAR_NODE,),
|
|
80
|
+
{"as_node": classmethod(lambda cls: create_node(cls, bases, dct))},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
class ScalarNode(ScalarNodeProto, abc.ABC, metaclass=create_class):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
__all__ = (
|
|
88
|
+
"ScalarNode",
|
|
89
|
+
"SCALAR_NODE",
|
|
90
|
+
"DataNode",
|
|
91
|
+
"Node",
|
|
92
|
+
"ComposeError",
|
|
93
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from telegrinder.bot.cute_types import UpdateCute
|
|
4
|
+
from telegrinder.node import Node
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class NodeSession:
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
value: typing.Any,
|
|
11
|
+
subnodes: dict[str, typing.Self],
|
|
12
|
+
generator: typing.AsyncGenerator[typing.Any, None] | None = None,
|
|
13
|
+
):
|
|
14
|
+
self.value = value
|
|
15
|
+
self.subnodes = subnodes
|
|
16
|
+
self.generator = generator
|
|
17
|
+
|
|
18
|
+
async def close(self, with_value: typing.Any | None = None) -> None:
|
|
19
|
+
for subnode in self.subnodes.values():
|
|
20
|
+
await subnode.close()
|
|
21
|
+
|
|
22
|
+
if self.generator is None:
|
|
23
|
+
return
|
|
24
|
+
try:
|
|
25
|
+
await self.generator.asend(with_value)
|
|
26
|
+
except StopAsyncIteration:
|
|
27
|
+
self.generator = None
|
|
28
|
+
|
|
29
|
+
def __repr__(self) -> str:
|
|
30
|
+
return f"<NodeSession {self.value}" + ("ACTIVE>" if self.generator else ">")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NodeCollection:
|
|
34
|
+
def __init__(self, sessions: dict[str, NodeSession]) -> None:
|
|
35
|
+
self.sessions = sessions
|
|
36
|
+
|
|
37
|
+
def values(self) -> dict[str, typing.Any]:
|
|
38
|
+
return {name: session.value for name, session in self.sessions.items()}
|
|
39
|
+
|
|
40
|
+
async def close_all(self, with_value: typing.Any | None = None) -> None:
|
|
41
|
+
for session in self.sessions.values():
|
|
42
|
+
await session.close(with_value)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def compose_node(
|
|
46
|
+
node: type[Node],
|
|
47
|
+
update: UpdateCute,
|
|
48
|
+
ready_context: dict[str, NodeSession] | None = None,
|
|
49
|
+
) -> NodeSession:
|
|
50
|
+
_node = node.as_node()
|
|
51
|
+
context = NodeCollection(ready_context.copy() if ready_context else {})
|
|
52
|
+
|
|
53
|
+
for name, subnode in _node.get_sub_nodes().items():
|
|
54
|
+
if subnode is UpdateCute:
|
|
55
|
+
context.sessions[name] = NodeSession(update, {})
|
|
56
|
+
else:
|
|
57
|
+
context.sessions[name] = await compose_node(subnode, update)
|
|
58
|
+
|
|
59
|
+
generator: typing.AsyncGenerator | None
|
|
60
|
+
|
|
61
|
+
if _node.is_generator():
|
|
62
|
+
generator = typing.cast(typing.AsyncGenerator, _node.compose(**context.values()))
|
|
63
|
+
value = await generator.asend(None)
|
|
64
|
+
else:
|
|
65
|
+
generator = None
|
|
66
|
+
value = await _node.compose(**context.values()) # type: ignore
|
|
67
|
+
|
|
68
|
+
return NodeSession(value, context.sessions, generator)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
__all__ = ("NodeCollection", "NodeSession", "compose_node")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from .base import Node
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ContainerNode(Node):
|
|
7
|
+
linked_nodes: typing.ClassVar[list[type[Node]]]
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
async def compose(cls, **kw) -> tuple["Node", ...]:
|
|
11
|
+
return tuple(t[1] for t in sorted(kw.items(), key=lambda t: t[0]))
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def get_sub_nodes(cls) -> dict[str, type["Node"]]:
|
|
15
|
+
return {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def link_nodes(cls, linked_nodes: list[type[Node]]) -> type["ContainerNode"]:
|
|
19
|
+
return type("_ContainerNode", (cls,), {"linked_nodes": linked_nodes})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__all__ = ("ContainerNode",)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from telegrinder.bot.cute_types import MessageCute
|
|
4
|
+
|
|
5
|
+
from .base import ComposeError, ScalarNode
|
|
6
|
+
from .update import UpdateNode
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MessageNode(ScalarNode, MessageCute):
|
|
10
|
+
@classmethod
|
|
11
|
+
async def compose(cls, update: UpdateNode) -> typing.Self:
|
|
12
|
+
return cls(
|
|
13
|
+
**update.message.expect(ComposeError).to_dict(),
|
|
14
|
+
api=update.api,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ("MessageNode",)
|
telegrinder/node/rule.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from telegrinder.bot.dispatch.context import Context
|
|
5
|
+
from telegrinder.bot.dispatch.process import check_rule
|
|
6
|
+
from telegrinder.bot.rules.abc import ABCRule
|
|
7
|
+
from telegrinder.node.base import ComposeError, Node
|
|
8
|
+
from telegrinder.node.update import UpdateNode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RuleContext(dict):
|
|
12
|
+
dataclass = dict
|
|
13
|
+
rules: tuple[ABCRule, ...] = ()
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
async def compose(cls, update: UpdateNode):
|
|
17
|
+
ctx = Context()
|
|
18
|
+
for rule in cls.rules:
|
|
19
|
+
if not await check_rule(update.api, rule, update, ctx):
|
|
20
|
+
raise ComposeError
|
|
21
|
+
try:
|
|
22
|
+
return cls.dataclass(**ctx) # type: ignore
|
|
23
|
+
except Exception as exc:
|
|
24
|
+
raise ComposeError(f"Dataclass validation error: {exc}")
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def as_node(cls) -> type[typing.Self]:
|
|
28
|
+
return cls
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def get_sub_nodes(cls) -> dict:
|
|
32
|
+
return {"update": UpdateNode}
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def is_generator(cls) -> typing.Literal[False]:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
def __new__(cls, *rules: ABCRule) -> type[Node]:
|
|
39
|
+
return type("_RuleNode", (cls,), {"dataclass": dict, "rules": rules}) # type: ignore
|
|
40
|
+
|
|
41
|
+
def __class_getitem__(cls, item: tuple[ABCRule, ...]) -> typing.Self:
|
|
42
|
+
if not isinstance(item, tuple):
|
|
43
|
+
item = (item,)
|
|
44
|
+
return cls(*item)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def generate_dataclass(cls_: type["RuleContext"]): # noqa: ANN205
|
|
48
|
+
return dataclasses.dataclass(type(cls_.__name__, (object,), dict(cls_.__dict__)))
|
|
49
|
+
|
|
50
|
+
def __init_subclass__(cls) -> None:
|
|
51
|
+
if cls.__name__ == "_RuleNode":
|
|
52
|
+
return
|
|
53
|
+
cls.dataclass = cls.generate_dataclass(cls)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = ("RuleContext",)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from telegrinder.api import API
|
|
5
|
+
from telegrinder.msgspec_utils import Nothing, Option
|
|
6
|
+
from telegrinder.types import Chat, Message
|
|
7
|
+
|
|
8
|
+
from .base import DataNode
|
|
9
|
+
from .message import MessageNode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclasses.dataclass
|
|
13
|
+
class Source(DataNode):
|
|
14
|
+
api: API
|
|
15
|
+
chat: Chat
|
|
16
|
+
thread_id: Option[int] = dataclasses.field(default_factory=lambda: Nothing)
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
async def compose(cls, message: MessageNode) -> typing.Self:
|
|
20
|
+
return cls(
|
|
21
|
+
api=message.ctx_api,
|
|
22
|
+
chat=message.chat,
|
|
23
|
+
thread_id=message.message_thread_id,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
async def send(self, text: str) -> Message:
|
|
27
|
+
result = await self.api.send_message(self.chat.id, message_thread_id=self.thread_id, text=text)
|
|
28
|
+
return result.unwrap()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = ("Source",)
|
telegrinder/node/text.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from .base import ComposeError, ScalarNode
|
|
4
|
+
from .message import MessageNode
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Text(ScalarNode, str):
|
|
8
|
+
@classmethod
|
|
9
|
+
async def compose(cls, message: MessageNode) -> typing.Self:
|
|
10
|
+
return cls(message.text.expect(ComposeError("Message has no text")))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = ("Text",)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from telegrinder.node.base import ComposeError, Node
|
|
5
|
+
from telegrinder.node.container import ContainerNode
|
|
6
|
+
|
|
7
|
+
T = typing.TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def cast_false_to_none(value: T) -> T | None:
|
|
11
|
+
if value is False:
|
|
12
|
+
return None
|
|
13
|
+
return value
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def error_on_none(value: T | None) -> T:
|
|
17
|
+
if value is None:
|
|
18
|
+
raise ComposeError
|
|
19
|
+
return value
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def generate(
|
|
23
|
+
subnodes: tuple[type[Node], ...],
|
|
24
|
+
func: typing.Callable[..., typing.Any],
|
|
25
|
+
casts: tuple[typing.Callable, ...] = (cast_false_to_none, error_on_none),
|
|
26
|
+
) -> type[ContainerNode]:
|
|
27
|
+
async def compose(**kw: typing.Any) -> typing.Any:
|
|
28
|
+
args = await ContainerNode.compose(**kw)
|
|
29
|
+
result = func(*args)
|
|
30
|
+
if inspect.isawaitable(result):
|
|
31
|
+
result = await result
|
|
32
|
+
for cast in casts:
|
|
33
|
+
result = cast(result)
|
|
34
|
+
return result
|
|
35
|
+
|
|
36
|
+
container = ContainerNode.link_nodes(list(subnodes))
|
|
37
|
+
return type("_ContainerNode", (container,), {"compose": compose})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ("generate",)
|
telegrinder/rules.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
from .bot.rules import *
|
|
1
|
+
from .bot.rules import * # noqa: F403
|