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/node/composer.py
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
+
import dataclasses
|
|
1
2
|
import typing
|
|
2
3
|
|
|
3
|
-
from
|
|
4
|
-
|
|
4
|
+
from fntypes.error import UnwrapError
|
|
5
|
+
|
|
6
|
+
from telegrinder.bot.cute_types.update import UpdateCute
|
|
5
7
|
from telegrinder.bot.dispatch.context import Context
|
|
6
|
-
from telegrinder.
|
|
7
|
-
from telegrinder.node.
|
|
8
|
-
|
|
8
|
+
from telegrinder.modules import logger
|
|
9
|
+
from telegrinder.node.base import (
|
|
10
|
+
BaseNode,
|
|
11
|
+
ComposeError,
|
|
12
|
+
Node,
|
|
13
|
+
NodeScope,
|
|
14
|
+
get_compose_annotations,
|
|
15
|
+
get_nodes,
|
|
16
|
+
)
|
|
17
|
+
from telegrinder.tools.magic import magic_bundle
|
|
9
18
|
|
|
10
19
|
CONTEXT_STORE_NODES_KEY = "node_ctx"
|
|
11
20
|
|
|
@@ -22,80 +31,86 @@ async def compose_node(
|
|
|
22
31
|
for name, subnode in node.get_sub_nodes().items():
|
|
23
32
|
if subnode in node_ctx:
|
|
24
33
|
context.sessions[name] = node_ctx[subnode]
|
|
25
|
-
elif subnode is UpdateCute:
|
|
26
|
-
context.sessions[name] = NodeSession(None, update, {})
|
|
27
|
-
elif subnode is API:
|
|
28
|
-
context.sessions[name] = NodeSession(None, update.ctx_api, {})
|
|
29
|
-
elif subnode is Context:
|
|
30
|
-
context.sessions[name] = NodeSession(None, ctx, {})
|
|
31
34
|
else:
|
|
32
35
|
context.sessions[name] = await compose_node(subnode, update, ctx)
|
|
33
36
|
|
|
34
37
|
if getattr(subnode, "scope", None) is NodeScope.PER_EVENT:
|
|
35
38
|
node_ctx[subnode] = context.sessions[name]
|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
for name, annotation in node.get_compose_annotations().items():
|
|
41
|
+
context.sessions[name] = NodeSession(
|
|
42
|
+
None,
|
|
43
|
+
await node.compose_annotation(annotation, update, ctx),
|
|
44
|
+
{},
|
|
45
|
+
)
|
|
38
46
|
|
|
39
47
|
if node.is_generator():
|
|
40
|
-
generator = typing.cast(typing.AsyncGenerator, node.compose(**context.values()))
|
|
48
|
+
generator = typing.cast(typing.AsyncGenerator[typing.Any, None], node.compose(**context.values()))
|
|
41
49
|
value = await generator.asend(None)
|
|
42
50
|
else:
|
|
43
51
|
generator = None
|
|
44
|
-
value = await node.compose(**context.values())
|
|
52
|
+
value = await typing.cast(typing.Awaitable[typing.Any], node.compose(**context.values()))
|
|
45
53
|
|
|
46
54
|
return NodeSession(_node, value, context.sessions, generator)
|
|
47
55
|
|
|
48
56
|
|
|
49
57
|
async def compose_nodes(
|
|
50
|
-
node_types: dict[str, type[Node]],
|
|
51
58
|
update: UpdateCute,
|
|
52
59
|
ctx: Context,
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
nodes: dict[str, type[Node]],
|
|
61
|
+
node_class: type[Node] | None = None,
|
|
62
|
+
context_annotations: dict[str, typing.Any] | None = None,
|
|
63
|
+
) -> "NodeCollection | None":
|
|
64
|
+
node_sessions: dict[str, NodeSession] = {}
|
|
55
65
|
node_ctx: dict[type[Node], "NodeSession"] = ctx.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
56
66
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
try:
|
|
68
|
+
for name, node_t in nodes.items():
|
|
69
|
+
scope = getattr(node_t, "scope", None)
|
|
70
|
+
|
|
60
71
|
if scope is NodeScope.PER_EVENT and node_t in node_ctx:
|
|
61
|
-
|
|
72
|
+
node_sessions[name] = node_ctx[node_t]
|
|
62
73
|
continue
|
|
63
74
|
elif scope is NodeScope.GLOBAL and hasattr(node_t, "_value"):
|
|
64
|
-
|
|
75
|
+
node_sessions[name] = getattr(node_t, "_value")
|
|
65
76
|
continue
|
|
66
77
|
|
|
67
|
-
|
|
68
|
-
node_t,
|
|
69
|
-
update,
|
|
70
|
-
ctx,
|
|
71
|
-
)
|
|
78
|
+
node_sessions[name] = await compose_node(node_t, update, ctx)
|
|
72
79
|
|
|
73
80
|
if scope is NodeScope.PER_EVENT:
|
|
74
|
-
node_ctx[node_t] =
|
|
81
|
+
node_ctx[node_t] = node_sessions[name]
|
|
75
82
|
elif scope is NodeScope.GLOBAL:
|
|
76
|
-
setattr(node_t, "_value",
|
|
83
|
+
setattr(node_t, "_value", node_sessions[name])
|
|
84
|
+
except (ComposeError, UnwrapError) as exc:
|
|
85
|
+
logger.debug(f"Composing node (name={name!r}, node_class={node_t!r}) failed with error: {str(exc)!r}")
|
|
86
|
+
await NodeCollection(node_sessions).close_all()
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
if context_annotations:
|
|
90
|
+
node_class = node_class or BaseNode
|
|
77
91
|
|
|
78
|
-
|
|
79
|
-
|
|
92
|
+
try:
|
|
93
|
+
for name, annotation in context_annotations.items():
|
|
94
|
+
node_sessions[name] = await node_class.compose_annotation(annotation, update, ctx)
|
|
95
|
+
except (ComposeError, UnwrapError) as exc:
|
|
96
|
+
logger.debug(
|
|
97
|
+
f"Composing context annotation (name={name!r}, annotation={annotation!r}) failed with error: {str(exc)!r}",
|
|
98
|
+
)
|
|
99
|
+
await NodeCollection(node_sessions).close_all()
|
|
80
100
|
return None
|
|
81
|
-
|
|
101
|
+
|
|
102
|
+
return NodeCollection(node_sessions)
|
|
82
103
|
|
|
83
104
|
|
|
105
|
+
@dataclasses.dataclass(slots=True, repr=False)
|
|
84
106
|
class NodeSession:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
subnodes: dict[str, typing.Self],
|
|
90
|
-
generator: typing.AsyncGenerator[typing.Any, None] | None = None,
|
|
91
|
-
):
|
|
92
|
-
self.node_type = node_type
|
|
93
|
-
self.value = value
|
|
94
|
-
self.subnodes = subnodes
|
|
95
|
-
self.generator = generator
|
|
107
|
+
node_type: type[Node] | None
|
|
108
|
+
value: typing.Any
|
|
109
|
+
subnodes: dict[str, typing.Self]
|
|
110
|
+
generator: typing.AsyncGenerator[typing.Any, None] | None = None
|
|
96
111
|
|
|
97
112
|
def __repr__(self) -> str:
|
|
98
|
-
return f"<{self.__class__.__name__}: {self.value}" + ("ACTIVE>" if self.generator else ">")
|
|
113
|
+
return f"<{self.__class__.__name__}: {self.value!r}" + (" ACTIVE>" if self.generator else ">")
|
|
99
114
|
|
|
100
115
|
async def close(
|
|
101
116
|
self,
|
|
@@ -117,11 +132,13 @@ class NodeSession:
|
|
|
117
132
|
|
|
118
133
|
|
|
119
134
|
class NodeCollection:
|
|
135
|
+
__slots__ = ("sessions",)
|
|
136
|
+
|
|
120
137
|
def __init__(self, sessions: dict[str, NodeSession]) -> None:
|
|
121
138
|
self.sessions = sessions
|
|
122
139
|
|
|
123
140
|
def __repr__(self) -> str:
|
|
124
|
-
return "<{}: sessions={}>".format(self.__class__.__name__, self.sessions)
|
|
141
|
+
return "<{}: sessions={!r}>".format(self.__class__.__name__, self.sessions)
|
|
125
142
|
|
|
126
143
|
def values(self) -> dict[str, typing.Any]:
|
|
127
144
|
return {name: session.value for name, session in self.sessions.items()}
|
|
@@ -135,26 +152,37 @@ class NodeCollection:
|
|
|
135
152
|
await session.close(with_value, scopes=scopes)
|
|
136
153
|
|
|
137
154
|
|
|
155
|
+
@dataclasses.dataclass(slots=True, repr=False)
|
|
138
156
|
class Composition:
|
|
139
|
-
|
|
157
|
+
func: typing.Callable[..., typing.Any]
|
|
158
|
+
is_blocking: bool
|
|
159
|
+
node_class: type[Node] = dataclasses.field(default_factory=lambda: BaseNode)
|
|
160
|
+
nodes: dict[str, type[Node]] = dataclasses.field(init=False)
|
|
161
|
+
context_annotations: dict[str, typing.Any] = dataclasses.field(init=False)
|
|
140
162
|
|
|
141
|
-
def
|
|
142
|
-
self.
|
|
143
|
-
self.
|
|
144
|
-
self.is_blocking = is_blocking
|
|
163
|
+
def __post_init__(self) -> None:
|
|
164
|
+
self.nodes = get_nodes(self.func)
|
|
165
|
+
self.context_annotations = get_compose_annotations(self.func)
|
|
145
166
|
|
|
146
167
|
def __repr__(self) -> str:
|
|
147
|
-
return "<{}: for function={!r} with nodes={}>".format(
|
|
168
|
+
return "<{}: for function={!r} with nodes={!r}, context_annotations={!r}>".format(
|
|
148
169
|
("blocking " if self.is_blocking else "") + self.__class__.__name__,
|
|
149
|
-
self.func.
|
|
170
|
+
self.func.__qualname__,
|
|
150
171
|
self.nodes,
|
|
172
|
+
self.context_annotations,
|
|
151
173
|
)
|
|
152
174
|
|
|
153
175
|
async def compose_nodes(self, update: UpdateCute, context: Context) -> NodeCollection | None:
|
|
154
|
-
return await compose_nodes(
|
|
176
|
+
return await compose_nodes(
|
|
177
|
+
update=update,
|
|
178
|
+
ctx=context,
|
|
179
|
+
nodes=self.nodes,
|
|
180
|
+
node_class=self.node_class,
|
|
181
|
+
context_annotations=self.context_annotations,
|
|
182
|
+
)
|
|
155
183
|
|
|
156
184
|
async def __call__(self, **kwargs: typing.Any) -> typing.Any:
|
|
157
185
|
return await self.func(**magic_bundle(self.func, kwargs, start_idx=0, bundle_ctx=False)) # type: ignore
|
|
158
186
|
|
|
159
187
|
|
|
160
|
-
__all__ = ("
|
|
188
|
+
__all__ = ("Composition", "NodeCollection", "NodeSession", "compose_node", "compose_nodes")
|
telegrinder/node/container.py
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
|
-
from .base import Node
|
|
3
|
+
from telegrinder.node.base import BaseNode, Node
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
class ContainerNode(
|
|
6
|
+
class ContainerNode(BaseNode):
|
|
7
7
|
linked_nodes: typing.ClassVar[list[type[Node]]]
|
|
8
8
|
|
|
9
9
|
@classmethod
|
|
10
|
-
async def compose(cls, **kw) -> tuple[
|
|
10
|
+
async def compose(cls, **kw) -> tuple[Node, ...]:
|
|
11
11
|
return tuple(t[1] for t in sorted(kw.items(), key=lambda t: t[0]))
|
|
12
12
|
|
|
13
13
|
@classmethod
|
|
14
|
-
def get_sub_nodes(cls) -> dict[str, type[
|
|
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
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
import msgspec
|
|
4
|
+
|
|
5
|
+
from telegrinder.bot.dispatch.context import Context
|
|
6
|
+
from telegrinder.msgspec_utils import DataclassInstance
|
|
7
|
+
from telegrinder.node.base import BaseNode, ComposeError, DataNode
|
|
8
|
+
from telegrinder.node.update import UpdateNode
|
|
9
|
+
|
|
10
|
+
if typing.TYPE_CHECKING:
|
|
11
|
+
Dataclass = typing.TypeVar("Dataclass", bound="DataclassType")
|
|
12
|
+
|
|
13
|
+
DataclassType: typing.TypeAlias = DataclassInstance | DataNode | msgspec.Struct | dict[str, typing.Any]
|
|
14
|
+
|
|
15
|
+
EVENT_NODE_KEY = "_event_node"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if typing.TYPE_CHECKING:
|
|
19
|
+
EventNode: typing.TypeAlias = typing.Annotated["Dataclass", ...]
|
|
20
|
+
|
|
21
|
+
else:
|
|
22
|
+
from telegrinder.msgspec_utils import decoder
|
|
23
|
+
|
|
24
|
+
class EventNode(BaseNode):
|
|
25
|
+
dataclass: type["DataclassType"]
|
|
26
|
+
|
|
27
|
+
def __new__(cls, dataclass: type["DataclassType"], /) -> type[typing.Self]:
|
|
28
|
+
namespace = dict(**cls.__dict__)
|
|
29
|
+
namespace.pop("__new__", None)
|
|
30
|
+
new_cls = type("EventNode", (cls,), {"dataclass": dataclass, **namespace})
|
|
31
|
+
return new_cls # type: ignore
|
|
32
|
+
|
|
33
|
+
def __class_getitem__(cls, dataclass: type["DataclassType"], /) -> type[typing.Self]:
|
|
34
|
+
return cls(dataclass)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
async def compose(cls, raw_update: UpdateNode, ctx: Context) -> "DataclassType":
|
|
38
|
+
dataclass_type = typing.get_origin(cls.dataclass) or cls.dataclass
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
if issubclass(dataclass_type, dict):
|
|
42
|
+
dataclass = cls.dataclass(**raw_update.incoming_update.to_full_dict())
|
|
43
|
+
|
|
44
|
+
elif issubclass(dataclass_type, msgspec.Struct | DataclassInstance):
|
|
45
|
+
dataclass = decoder.convert(
|
|
46
|
+
raw_update.incoming_update.to_full_dict(),
|
|
47
|
+
type=cls.dataclass,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
else:
|
|
51
|
+
dataclass = cls.dataclass(**raw_update.incoming_update.to_dict())
|
|
52
|
+
|
|
53
|
+
ctx[EVENT_NODE_KEY] = cls
|
|
54
|
+
return dataclass
|
|
55
|
+
except Exception:
|
|
56
|
+
raise ComposeError(f"Cannot parse update to {cls.dataclass.__name__!r}.")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__all__ = ("EventNode",)
|
telegrinder/node/me.py
CHANGED
telegrinder/node/message.py
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
from telegrinder.bot.cute_types import MessageCute
|
|
2
|
-
|
|
3
|
-
from .
|
|
4
|
-
from .update import UpdateNode
|
|
1
|
+
from telegrinder.bot.cute_types.message import MessageCute
|
|
2
|
+
from telegrinder.node.base import ComposeError, ScalarNode
|
|
3
|
+
from telegrinder.node.update import UpdateNode
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
class MessageNode(ScalarNode, MessageCute):
|
|
8
7
|
@classmethod
|
|
9
|
-
async def compose(cls, update: UpdateNode) ->
|
|
8
|
+
async def compose(cls, update: UpdateNode) -> MessageCute:
|
|
10
9
|
if not update.message:
|
|
11
|
-
raise ComposeError
|
|
12
|
-
return
|
|
13
|
-
**update.message.unwrap().to_dict(),
|
|
14
|
-
api=update.api,
|
|
15
|
-
)
|
|
10
|
+
raise ComposeError("Update is not a message.")
|
|
11
|
+
return update.message.unwrap()
|
|
16
12
|
|
|
17
13
|
|
|
18
14
|
__all__ = ("MessageNode",)
|
telegrinder/node/polymorphic.py
CHANGED
|
@@ -2,38 +2,37 @@ import inspect
|
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
4
|
from telegrinder.bot.dispatch.context import Context
|
|
5
|
-
from telegrinder.
|
|
5
|
+
from telegrinder.node.base import BaseNode, ComposeError
|
|
6
|
+
from telegrinder.node.composer import CONTEXT_STORE_NODES_KEY, Composition, NodeSession
|
|
7
|
+
from telegrinder.node.scope import NodeScope
|
|
8
|
+
from telegrinder.node.update import UpdateNode
|
|
9
|
+
from telegrinder.tools.magic import IMPL_MARK, get_impls_by_key, impl
|
|
6
10
|
|
|
7
|
-
from .base import ComposeError
|
|
8
|
-
from .composer import CONTEXT_STORE_NODES_KEY, Composition, NodeSession
|
|
9
|
-
from .scope import NodeScope
|
|
10
|
-
from .update import UpdateNode
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
class Polymorphic:
|
|
12
|
+
class Polymorphic(BaseNode):
|
|
14
13
|
@classmethod
|
|
15
14
|
async def compose(cls, update: UpdateNode, context: Context) -> typing.Any:
|
|
16
15
|
scope = getattr(cls, "scope", None)
|
|
17
16
|
node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
18
17
|
|
|
19
|
-
for i, impl in enumerate(
|
|
20
|
-
composition = Composition(impl, True)
|
|
18
|
+
for i, impl in enumerate(get_impls_by_key(cls, IMPL_MARK).values()):
|
|
19
|
+
composition = Composition(impl, True, node_class=cls)
|
|
21
20
|
node_collection = await composition.compose_nodes(update, context)
|
|
22
21
|
if node_collection is None:
|
|
23
22
|
continue
|
|
24
23
|
|
|
25
24
|
# To determine whether this is a right morph, all subnodes must be resolved
|
|
26
25
|
if scope is NodeScope.PER_EVENT and (cls, i) in node_ctx:
|
|
27
|
-
|
|
26
|
+
res: NodeSession = node_ctx[(cls, i)]
|
|
28
27
|
await node_collection.close_all()
|
|
29
|
-
return
|
|
28
|
+
return res.value
|
|
30
29
|
|
|
31
30
|
result = composition.func(cls, **node_collection.values())
|
|
32
31
|
if inspect.isawaitable(result):
|
|
33
32
|
result = await result
|
|
34
33
|
|
|
35
34
|
if scope is NodeScope.PER_EVENT:
|
|
36
|
-
node_ctx[(cls, i)] = NodeSession(cls, result, {})
|
|
35
|
+
node_ctx[(cls, i)] = NodeSession(cls, result, {})
|
|
37
36
|
|
|
38
37
|
await node_collection.close_all(with_value=result)
|
|
39
38
|
return result
|
telegrinder/node/rule.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import dataclasses
|
|
2
|
+
import importlib
|
|
2
3
|
import typing
|
|
3
4
|
|
|
4
5
|
from telegrinder.bot.dispatch.context import Context
|
|
@@ -6,6 +7,7 @@ from telegrinder.node.base import ComposeError, Node
|
|
|
6
7
|
from telegrinder.node.update import UpdateNode
|
|
7
8
|
|
|
8
9
|
if typing.TYPE_CHECKING:
|
|
10
|
+
from telegrinder.bot.dispatch.process import check_rule
|
|
9
11
|
from telegrinder.bot.rules.abc import ABCRule
|
|
10
12
|
|
|
11
13
|
|
|
@@ -13,7 +15,9 @@ class RuleChain(dict[str, typing.Any]):
|
|
|
13
15
|
dataclass = dict
|
|
14
16
|
rules: tuple["ABCRule", ...] = ()
|
|
15
17
|
|
|
16
|
-
def __init_subclass__(cls) -> None:
|
|
18
|
+
def __init_subclass__(cls, *args: typing.Any, **kwargs: typing.Any) -> None:
|
|
19
|
+
super().__init_subclass__(*args, **kwargs)
|
|
20
|
+
|
|
17
21
|
if cls.__name__ == "_RuleNode":
|
|
18
22
|
return
|
|
19
23
|
cls.dataclass = cls.generate_node_dataclass(cls)
|
|
@@ -21,7 +25,7 @@ class RuleChain(dict[str, typing.Any]):
|
|
|
21
25
|
def __new__(cls, *rules: "ABCRule") -> type[Node]:
|
|
22
26
|
return type("_RuleNode", (cls,), {"dataclass": dict, "rules": rules}) # type: ignore
|
|
23
27
|
|
|
24
|
-
def __class_getitem__(cls, items:
|
|
28
|
+
def __class_getitem__(cls, items: "ABCRule | tuple[ABCRule, ...]", /) -> typing.Self:
|
|
25
29
|
if not isinstance(items, tuple):
|
|
26
30
|
items = (items,)
|
|
27
31
|
assert all(isinstance(rule, "ABCRule") for rule in items), "All items must be instances of 'ABCRule'."
|
|
@@ -32,13 +36,23 @@ class RuleChain(dict[str, typing.Any]):
|
|
|
32
36
|
return dataclasses.dataclass(type(cls_.__name__, (object,), dict(cls_.__dict__)))
|
|
33
37
|
|
|
34
38
|
@classmethod
|
|
35
|
-
async def compose(cls, update: UpdateNode):
|
|
36
|
-
|
|
39
|
+
async def compose(cls, update: UpdateNode) -> typing.Any:
|
|
40
|
+
globalns = globals()
|
|
41
|
+
if "check_rule" not in globalns:
|
|
42
|
+
globalns.update(
|
|
43
|
+
{
|
|
44
|
+
"check_rule": getattr(
|
|
45
|
+
importlib.import_module("telegrinder.bot.dispatch.process"),
|
|
46
|
+
"check_rule",
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
)
|
|
37
50
|
|
|
38
51
|
ctx = Context()
|
|
39
52
|
for rule in cls.rules:
|
|
40
53
|
if not await check_rule(update.api, rule, update, ctx):
|
|
41
|
-
raise ComposeError
|
|
54
|
+
raise ComposeError(f"Rule {rule!r} failed!")
|
|
55
|
+
|
|
42
56
|
try:
|
|
43
57
|
return cls.dataclass(**ctx) # type: ignore
|
|
44
58
|
except Exception as exc:
|
|
@@ -52,6 +66,14 @@ class RuleChain(dict[str, typing.Any]):
|
|
|
52
66
|
def get_sub_nodes(cls) -> dict:
|
|
53
67
|
return {"update": UpdateNode}
|
|
54
68
|
|
|
69
|
+
@classmethod
|
|
70
|
+
def get_compose_annotations(cls) -> dict[str, typing.Any]:
|
|
71
|
+
return {}
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def get_node_impls(cls) -> dict[str, typing.Callable[..., typing.Any]]:
|
|
75
|
+
return {}
|
|
76
|
+
|
|
55
77
|
@classmethod
|
|
56
78
|
def is_generator(cls) -> typing.Literal[False]:
|
|
57
79
|
return False
|
telegrinder/node/source.py
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
|
-
from fntypes import Nothing, Option
|
|
4
|
+
from fntypes.option import Nothing, Option
|
|
5
5
|
|
|
6
|
-
from telegrinder.api import API
|
|
7
|
-
from telegrinder.
|
|
6
|
+
from telegrinder.api.api import API
|
|
7
|
+
from telegrinder.node.base import ComposeError, DataNode, ScalarNode
|
|
8
|
+
from telegrinder.node.callback_query import CallbackQueryNode
|
|
9
|
+
from telegrinder.node.message import MessageNode
|
|
10
|
+
from telegrinder.node.polymorphic import Polymorphic, impl
|
|
11
|
+
from telegrinder.types.objects import Chat, Message, User
|
|
8
12
|
|
|
9
|
-
from .base import ComposeError, DataNode, ScalarNode
|
|
10
|
-
from .callback_query import CallbackQueryNode
|
|
11
|
-
from .message import MessageNode
|
|
12
|
-
from .polymorphic import Polymorphic, impl
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
@dataclasses.dataclass(kw_only=True)
|
|
14
|
+
@dataclasses.dataclass(kw_only=True, slots=True)
|
|
16
15
|
class Source(Polymorphic, DataNode):
|
|
17
16
|
api: API
|
|
18
17
|
chat: Chat
|
|
@@ -24,7 +23,7 @@ class Source(Polymorphic, DataNode):
|
|
|
24
23
|
return cls(
|
|
25
24
|
api=message.ctx_api,
|
|
26
25
|
chat=message.chat,
|
|
27
|
-
from_user=message.from_user,
|
|
26
|
+
from_user=message.from_.expect(ComposeError("MessageNode has no from_user")),
|
|
28
27
|
thread_id=message.message_thread_id,
|
|
29
28
|
)
|
|
30
29
|
|
|
@@ -32,7 +31,7 @@ class Source(Polymorphic, DataNode):
|
|
|
32
31
|
async def compose_callback_query(cls, callback_query: CallbackQueryNode) -> typing.Self:
|
|
33
32
|
return cls(
|
|
34
33
|
api=callback_query.ctx_api,
|
|
35
|
-
chat=callback_query.chat.expect(ComposeError),
|
|
34
|
+
chat=callback_query.chat.expect(ComposeError("CallbackQueryNode has no chat")),
|
|
36
35
|
from_user=callback_query.from_user,
|
|
37
36
|
thread_id=callback_query.message_thread_id,
|
|
38
37
|
)
|
telegrinder/node/text.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
from .base import ComposeError, ScalarNode
|
|
2
|
-
from .message import MessageNode
|
|
1
|
+
from telegrinder.node.base import ComposeError, ScalarNode
|
|
2
|
+
from telegrinder.node.message import MessageNode
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class Text(ScalarNode, str):
|
|
6
6
|
@classmethod
|
|
7
7
|
async def compose(cls, message: MessageNode) -> str:
|
|
8
8
|
if not message.text:
|
|
9
|
-
raise ComposeError("Message has no text")
|
|
9
|
+
raise ComposeError("Message has no text.")
|
|
10
10
|
return message.text.unwrap()
|
|
11
11
|
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@ class TextInteger(ScalarNode, int):
|
|
|
14
14
|
@classmethod
|
|
15
15
|
async def compose(cls, text: Text) -> int:
|
|
16
16
|
if not text.isdigit():
|
|
17
|
-
raise ComposeError("Text is not digit")
|
|
17
|
+
raise ComposeError("Text is not digit.")
|
|
18
18
|
return int(text)
|
|
19
19
|
|
|
20
20
|
|
telegrinder/node/update.py
CHANGED
telegrinder/py.typed
ADDED
|
File without changes
|
telegrinder/tools/__init__.py
CHANGED
|
@@ -43,7 +43,7 @@ from .global_context import (
|
|
|
43
43
|
CtxVar,
|
|
44
44
|
GlobalContext,
|
|
45
45
|
GlobalCtxVar,
|
|
46
|
-
|
|
46
|
+
TelegrinderContext,
|
|
47
47
|
ctx_var,
|
|
48
48
|
)
|
|
49
49
|
from .i18n import (
|
|
@@ -110,7 +110,7 @@ __all__ = (
|
|
|
110
110
|
"SpecialFormat",
|
|
111
111
|
"StartBotLink",
|
|
112
112
|
"StartGroupLink",
|
|
113
|
-
"
|
|
113
|
+
"TelegrinderContext",
|
|
114
114
|
"TgEmoji",
|
|
115
115
|
"escape",
|
|
116
116
|
"get_channel_boost_link",
|
telegrinder/tools/buttons.py
CHANGED
|
@@ -3,25 +3,20 @@ import typing
|
|
|
3
3
|
|
|
4
4
|
import msgspec
|
|
5
5
|
|
|
6
|
-
from telegrinder.
|
|
7
|
-
from telegrinder.types import (
|
|
6
|
+
from telegrinder.msgspec_utils import DataclassInstance, encoder
|
|
7
|
+
from telegrinder.types.objects import (
|
|
8
8
|
CallbackGame,
|
|
9
9
|
KeyboardButtonPollType,
|
|
10
10
|
KeyboardButtonRequestChat,
|
|
11
11
|
KeyboardButtonRequestUsers,
|
|
12
|
+
LoginUrl,
|
|
12
13
|
SwitchInlineQueryChosenChat,
|
|
13
14
|
WebAppInfo,
|
|
14
15
|
)
|
|
15
|
-
from telegrinder.types.objects import LoginUrl
|
|
16
16
|
|
|
17
17
|
ButtonT = typing.TypeVar("ButtonT", bound="BaseButton")
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
@typing.runtime_checkable
|
|
21
|
-
class DataclassInstance(typing.Protocol):
|
|
22
|
-
__dataclass_fields__: typing.ClassVar[dict[str, dataclasses.Field[typing.Any]]]
|
|
23
|
-
|
|
24
|
-
|
|
25
20
|
@dataclasses.dataclass
|
|
26
21
|
class BaseButton:
|
|
27
22
|
def get_data(self) -> dict[str, typing.Any]:
|
|
@@ -44,7 +39,7 @@ class RowButtons(typing.Generic[ButtonT]):
|
|
|
44
39
|
return [b.get_data() for b in self.buttons]
|
|
45
40
|
|
|
46
41
|
|
|
47
|
-
@dataclasses.dataclass
|
|
42
|
+
@dataclasses.dataclass(slots=True)
|
|
48
43
|
class Button(BaseButton):
|
|
49
44
|
text: str
|
|
50
45
|
request_contact: bool = dataclasses.field(default=False, kw_only=True)
|
|
@@ -61,7 +56,7 @@ class Button(BaseButton):
|
|
|
61
56
|
web_app: dict[str, typing.Any] | WebAppInfo | None = dataclasses.field(default=None, kw_only=True)
|
|
62
57
|
|
|
63
58
|
|
|
64
|
-
@dataclasses.dataclass
|
|
59
|
+
@dataclasses.dataclass(slots=True)
|
|
65
60
|
class InlineButton(BaseButton):
|
|
66
61
|
text: str
|
|
67
62
|
url: str | None = dataclasses.field(default=None, kw_only=True)
|
|
@@ -16,7 +16,7 @@ ExceptionT = typing.TypeVar("ExceptionT", bound=BaseException, contravariant=Tru
|
|
|
16
16
|
FuncCatcher = typing.Callable[typing.Concatenate[ExceptionT, ...], typing.Awaitable[typing.Any]]
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
@dataclasses.dataclass(frozen=True, repr=False)
|
|
19
|
+
@dataclasses.dataclass(frozen=True, repr=False, slots=True)
|
|
20
20
|
class Catcher(typing.Generic[EventT]):
|
|
21
21
|
func: FuncCatcher[BaseException]
|
|
22
22
|
exceptions: list[type[BaseException] | BaseException] = dataclasses.field(
|