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/node/composer.py
CHANGED
|
@@ -1,101 +1,108 @@
|
|
|
1
|
+
import dataclasses
|
|
1
2
|
import typing
|
|
2
3
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
4
|
+
from fntypes import Error, Ok, Result
|
|
5
|
+
from fntypes.error import UnwrapError
|
|
6
|
+
|
|
7
|
+
from telegrinder.api import API
|
|
8
|
+
from telegrinder.bot.cute_types.update import Update, UpdateCute
|
|
5
9
|
from telegrinder.bot.dispatch.context import Context
|
|
6
|
-
from telegrinder.
|
|
7
|
-
from telegrinder.node.
|
|
8
|
-
|
|
10
|
+
from telegrinder.modules import logger
|
|
11
|
+
from telegrinder.node.base import (
|
|
12
|
+
ComposeError,
|
|
13
|
+
Node,
|
|
14
|
+
NodeScope,
|
|
15
|
+
get_node_calc_lst,
|
|
16
|
+
get_nodes,
|
|
17
|
+
)
|
|
18
|
+
from telegrinder.tools.magic import magic_bundle
|
|
9
19
|
|
|
10
|
-
CONTEXT_STORE_NODES_KEY = "
|
|
20
|
+
CONTEXT_STORE_NODES_KEY = "_node_ctx"
|
|
21
|
+
GLOBAL_VALUE_KEY = "_value"
|
|
11
22
|
|
|
12
23
|
|
|
13
24
|
async def compose_node(
|
|
14
25
|
_node: type[Node],
|
|
15
|
-
|
|
16
|
-
ctx: Context,
|
|
26
|
+
linked: dict[type, typing.Any],
|
|
17
27
|
) -> "NodeSession":
|
|
18
28
|
node = _node.as_node()
|
|
19
|
-
|
|
20
|
-
node_ctx: dict[type[Node], "NodeSession"] = ctx.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
21
|
-
|
|
22
|
-
for name, subnode in node.get_sub_nodes().items():
|
|
23
|
-
if subnode in node_ctx:
|
|
24
|
-
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
|
-
else:
|
|
32
|
-
context.sessions[name] = await compose_node(subnode, update, ctx)
|
|
33
|
-
|
|
34
|
-
if getattr(subnode, "scope", None) is NodeScope.PER_EVENT:
|
|
35
|
-
node_ctx[subnode] = context.sessions[name]
|
|
36
|
-
|
|
37
|
-
generator: typing.AsyncGenerator | None
|
|
29
|
+
kwargs = magic_bundle(node.compose, linked, typebundle=True)
|
|
38
30
|
|
|
39
31
|
if node.is_generator():
|
|
40
|
-
generator = typing.cast(typing.AsyncGenerator, node.compose(**
|
|
32
|
+
generator = typing.cast(typing.AsyncGenerator[typing.Any, None], node.compose(**kwargs))
|
|
41
33
|
value = await generator.asend(None)
|
|
42
34
|
else:
|
|
43
35
|
generator = None
|
|
44
|
-
value = await node.compose(**
|
|
36
|
+
value = await typing.cast(typing.Awaitable[typing.Any], node.compose(**kwargs))
|
|
45
37
|
|
|
46
|
-
return NodeSession(_node, value,
|
|
38
|
+
return NodeSession(_node, value, {}, generator)
|
|
47
39
|
|
|
48
40
|
|
|
49
41
|
async def compose_nodes(
|
|
50
|
-
|
|
51
|
-
update: UpdateCute,
|
|
42
|
+
nodes: dict[str, type[Node]],
|
|
52
43
|
ctx: Context,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
44
|
+
data: dict[type, typing.Any] | None = None,
|
|
45
|
+
) -> Result["NodeCollection", ComposeError]:
|
|
46
|
+
logger.debug("Composing nodes: {!r}...", nodes)
|
|
47
|
+
|
|
48
|
+
parent_nodes: dict[type[Node], NodeSession] = {}
|
|
49
|
+
event_nodes: dict[type[Node], "NodeSession"] = ctx.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
50
|
+
data = {Context: ctx} | (data or {})
|
|
51
|
+
|
|
52
|
+
# Create flattened list of ordered nodes to be calculated
|
|
53
|
+
# TODO: optimize flattened list calculation via caching key = tuple of node types
|
|
54
|
+
calculation_nodes: list[list[type[Node]]] = []
|
|
55
|
+
for node_t in nodes.values():
|
|
56
|
+
calculation_nodes.append(get_node_calc_lst(node_t))
|
|
57
|
+
|
|
58
|
+
for linked_nodes in calculation_nodes:
|
|
59
|
+
local_nodes: dict[type[Node], "NodeSession"] = {}
|
|
60
|
+
for node_t in linked_nodes:
|
|
61
|
+
scope = getattr(node_t, "scope", None)
|
|
62
|
+
|
|
63
|
+
if scope is NodeScope.PER_EVENT and node_t in event_nodes:
|
|
64
|
+
local_nodes[node_t] = event_nodes[node_t]
|
|
62
65
|
continue
|
|
63
|
-
elif scope is NodeScope.GLOBAL and hasattr(node_t,
|
|
64
|
-
|
|
66
|
+
elif scope is NodeScope.GLOBAL and hasattr(node_t, GLOBAL_VALUE_KEY):
|
|
67
|
+
local_nodes[node_t] = getattr(node_t, GLOBAL_VALUE_KEY)
|
|
65
68
|
continue
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
subnodes = {
|
|
71
|
+
k: session.value
|
|
72
|
+
for k, session in
|
|
73
|
+
(local_nodes | event_nodes).items()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
local_nodes[node_t] = await compose_node(node_t, subnodes | data)
|
|
78
|
+
except (ComposeError, UnwrapError) as exc:
|
|
79
|
+
for t, local_node in local_nodes.items():
|
|
80
|
+
if t.scope is NodeScope.PER_CALL:
|
|
81
|
+
await local_node.close()
|
|
82
|
+
return Error(ComposeError(f"Cannot compose {node_t}. Error: {exc}"))
|
|
72
83
|
|
|
73
84
|
if scope is NodeScope.PER_EVENT:
|
|
74
|
-
|
|
85
|
+
event_nodes[node_t] = local_nodes[node_t]
|
|
75
86
|
elif scope is NodeScope.GLOBAL:
|
|
76
|
-
setattr(node_t,
|
|
87
|
+
setattr(node_t, GLOBAL_VALUE_KEY, local_nodes[node_t])
|
|
77
88
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return NodeCollection(nodes)
|
|
89
|
+
# Last node is the parent node
|
|
90
|
+
parent_node_t = linked_nodes[-1]
|
|
91
|
+
parent_nodes[parent_node_t] = local_nodes[parent_node_t]
|
|
82
92
|
|
|
93
|
+
node_sessions = {k: parent_nodes[t] for k, t in nodes.items()}
|
|
94
|
+
return Ok(NodeCollection(node_sessions))
|
|
83
95
|
|
|
96
|
+
|
|
97
|
+
@dataclasses.dataclass(slots=True, repr=False)
|
|
84
98
|
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
|
|
99
|
+
node_type: type[Node] | None
|
|
100
|
+
value: typing.Any
|
|
101
|
+
subnodes: dict[str, typing.Self]
|
|
102
|
+
generator: typing.AsyncGenerator[typing.Any, typing.Any | None] | None = None
|
|
96
103
|
|
|
97
104
|
def __repr__(self) -> str:
|
|
98
|
-
return f"<{self.__class__.__name__}: {self.value}" + ("ACTIVE>" if self.generator else ">")
|
|
105
|
+
return f"<{self.__class__.__name__}: {self.value!r}" + (" ACTIVE>" if self.generator else ">")
|
|
99
106
|
|
|
100
107
|
async def close(
|
|
101
108
|
self,
|
|
@@ -111,18 +118,22 @@ class NodeSession:
|
|
|
111
118
|
if self.generator is None:
|
|
112
119
|
return
|
|
113
120
|
try:
|
|
121
|
+
logger.debug("Closing session for node {!r}...", self.node_type)
|
|
114
122
|
await self.generator.asend(with_value)
|
|
115
123
|
except StopAsyncIteration:
|
|
116
124
|
self.generator = None
|
|
117
125
|
|
|
118
126
|
|
|
119
127
|
class NodeCollection:
|
|
128
|
+
__slots__ = ("sessions",)
|
|
129
|
+
|
|
120
130
|
def __init__(self, sessions: dict[str, NodeSession]) -> None:
|
|
121
131
|
self.sessions = sessions
|
|
122
132
|
|
|
123
133
|
def __repr__(self) -> str:
|
|
124
|
-
return "<{}: sessions={}>".format(self.__class__.__name__, self.sessions)
|
|
134
|
+
return "<{}: sessions={!r}>".format(self.__class__.__name__, self.sessions)
|
|
125
135
|
|
|
136
|
+
@property
|
|
126
137
|
def values(self) -> dict[str, typing.Any]:
|
|
127
138
|
return {name: session.value for name, session in self.sessions.items()}
|
|
128
139
|
|
|
@@ -135,26 +146,40 @@ class NodeCollection:
|
|
|
135
146
|
await session.close(with_value, scopes=scopes)
|
|
136
147
|
|
|
137
148
|
|
|
149
|
+
@dataclasses.dataclass(slots=True, repr=False)
|
|
138
150
|
class Composition:
|
|
139
|
-
|
|
151
|
+
func: typing.Callable[..., typing.Any]
|
|
152
|
+
is_blocking: bool
|
|
153
|
+
nodes: dict[str, type[Node]] = dataclasses.field(init=False)
|
|
140
154
|
|
|
141
|
-
def
|
|
142
|
-
self.
|
|
143
|
-
self.nodes = get_annotations(func)
|
|
144
|
-
self.is_blocking = is_blocking
|
|
155
|
+
def __post_init__(self) -> None:
|
|
156
|
+
self.nodes = get_nodes(self.func)
|
|
145
157
|
|
|
146
158
|
def __repr__(self) -> str:
|
|
147
|
-
return "<{}: for function={!r} with nodes={}>".format(
|
|
159
|
+
return "<{}: for function={!r} with nodes={!r}>".format(
|
|
148
160
|
("blocking " if self.is_blocking else "") + self.__class__.__name__,
|
|
149
|
-
self.func.
|
|
161
|
+
self.func.__qualname__,
|
|
150
162
|
self.nodes,
|
|
151
163
|
)
|
|
152
164
|
|
|
153
|
-
async def compose_nodes(
|
|
154
|
-
|
|
165
|
+
async def compose_nodes(
|
|
166
|
+
self,
|
|
167
|
+
update: UpdateCute,
|
|
168
|
+
context: Context,
|
|
169
|
+
) -> NodeCollection | None:
|
|
170
|
+
match await compose_nodes(
|
|
171
|
+
nodes=self.nodes,
|
|
172
|
+
ctx=context,
|
|
173
|
+
data={Update: update, API: update.api},
|
|
174
|
+
):
|
|
175
|
+
case Ok(col):
|
|
176
|
+
return col
|
|
177
|
+
case Error(err):
|
|
178
|
+
logger.debug(f"Composition failed with error: {err}")
|
|
179
|
+
return None
|
|
155
180
|
|
|
156
181
|
async def __call__(self, **kwargs: typing.Any) -> typing.Any:
|
|
157
182
|
return await self.func(**magic_bundle(self.func, kwargs, start_idx=0, bundle_ctx=False)) # type: ignore
|
|
158
183
|
|
|
159
184
|
|
|
160
|
-
__all__ = ("
|
|
185
|
+
__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 Node
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class ContainerNode(Node):
|
|
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
|
|
14
|
+
def get_subnodes(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,68 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
import msgspec
|
|
4
|
+
|
|
5
|
+
from telegrinder.api import API
|
|
6
|
+
from telegrinder.bot.cute_types import BaseCute
|
|
7
|
+
from telegrinder.bot.dispatch.context import Context
|
|
8
|
+
from telegrinder.msgspec_utils import DataclassInstance
|
|
9
|
+
from telegrinder.node.base import ComposeError, DataNode, Node
|
|
10
|
+
from telegrinder.node.update import UpdateNode
|
|
11
|
+
|
|
12
|
+
if typing.TYPE_CHECKING:
|
|
13
|
+
Dataclass = typing.TypeVar("Dataclass", bound="DataclassType")
|
|
14
|
+
|
|
15
|
+
DataclassType: typing.TypeAlias = DataclassInstance | DataNode | msgspec.Struct | dict[str, typing.Any]
|
|
16
|
+
|
|
17
|
+
EVENT_NODE_KEY = "_event_node"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
from telegrinder.msgspec_utils import decoder
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _EventNode(Node):
|
|
24
|
+
dataclass: type["DataclassType"]
|
|
25
|
+
|
|
26
|
+
def __new__(cls, dataclass: type["DataclassType"], /) -> type[typing.Self]:
|
|
27
|
+
namespace = dict(**cls.__dict__)
|
|
28
|
+
namespace.pop("__new__", None)
|
|
29
|
+
new_cls = type("EventNode", (cls,), {"dataclass": dataclass, **namespace})
|
|
30
|
+
return new_cls # type: ignore
|
|
31
|
+
|
|
32
|
+
def __class_getitem__(cls, dataclass: type["DataclassType"], /) -> typing.Self:
|
|
33
|
+
return cls(dataclass)
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
async def compose(cls, raw_update: UpdateNode, ctx: Context, api: API) -> "DataclassType":
|
|
37
|
+
dataclass_type = typing.get_origin(cls.dataclass) or cls.dataclass
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
if issubclass(dataclass_type, dict):
|
|
41
|
+
dataclass = cls.dataclass(**raw_update.incoming_update.to_full_dict())
|
|
42
|
+
|
|
43
|
+
elif issubclass(dataclass_type, BaseCute):
|
|
44
|
+
dataclass = dataclass_type.from_update(raw_update.incoming_update, bound_api=api)
|
|
45
|
+
|
|
46
|
+
elif issubclass(dataclass_type, (msgspec.Struct, DataclassInstance)): # type: ignore
|
|
47
|
+
# FIXME: must be used with encode_name
|
|
48
|
+
dataclass = decoder.convert(
|
|
49
|
+
raw_update.incoming_update.to_full_dict(),
|
|
50
|
+
type=cls.dataclass,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
else:
|
|
54
|
+
dataclass = cls.dataclass(**raw_update.incoming_update.to_dict())
|
|
55
|
+
|
|
56
|
+
ctx[EVENT_NODE_KEY] = cls
|
|
57
|
+
return dataclass
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
raise ComposeError(f"Cannot parse update into {cls.dataclass.__name__!r}, error: {exc}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if typing.TYPE_CHECKING:
|
|
63
|
+
EventNode: typing.TypeAlias = typing.Annotated["Dataclass", ...]
|
|
64
|
+
else:
|
|
65
|
+
class EventNode(_EventNode):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
__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,40 +2,45 @@ import inspect
|
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
4
|
from telegrinder.bot.dispatch.context import Context
|
|
5
|
+
from telegrinder.modules import logger
|
|
6
|
+
from telegrinder.node.base import ComposeError, Node
|
|
7
|
+
from telegrinder.node.composer import CONTEXT_STORE_NODES_KEY, Composition, NodeSession
|
|
8
|
+
from telegrinder.node.scope import NodeScope
|
|
9
|
+
from telegrinder.node.update import UpdateNode
|
|
5
10
|
from telegrinder.tools.magic import get_impls, impl
|
|
6
11
|
|
|
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
12
|
|
|
12
|
-
|
|
13
|
-
class Polymorphic:
|
|
13
|
+
class Polymorphic(Node):
|
|
14
14
|
@classmethod
|
|
15
15
|
async def compose(cls, update: UpdateNode, context: Context) -> typing.Any:
|
|
16
|
+
logger.debug(f"Composing polimorphic node {cls.__name__}")
|
|
16
17
|
scope = getattr(cls, "scope", None)
|
|
17
18
|
node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
18
19
|
|
|
19
20
|
for i, impl in enumerate(get_impls(cls)):
|
|
21
|
+
logger.debug("Checking impl {}", impl.__name__)
|
|
20
22
|
composition = Composition(impl, True)
|
|
21
23
|
node_collection = await composition.compose_nodes(update, context)
|
|
22
24
|
if node_collection is None:
|
|
25
|
+
logger.debug("Impl {} composition failed", impl.__name__)
|
|
23
26
|
continue
|
|
24
27
|
|
|
25
28
|
# To determine whether this is a right morph, all subnodes must be resolved
|
|
26
29
|
if scope is NodeScope.PER_EVENT and (cls, i) in node_ctx:
|
|
27
|
-
|
|
30
|
+
logger.debug("Morph is already cached as per_event node, using its value. Impl {} succeeded", impl.__name__)
|
|
31
|
+
res: NodeSession = node_ctx[(cls, i)]
|
|
28
32
|
await node_collection.close_all()
|
|
29
|
-
return
|
|
33
|
+
return res.value
|
|
30
34
|
|
|
31
|
-
result = composition.func(cls, **node_collection.values
|
|
35
|
+
result = composition.func(cls, **node_collection.values)
|
|
32
36
|
if inspect.isawaitable(result):
|
|
33
37
|
result = await result
|
|
34
38
|
|
|
35
39
|
if scope is NodeScope.PER_EVENT:
|
|
36
|
-
node_ctx[(cls, i)] = NodeSession(cls, result, {})
|
|
40
|
+
node_ctx[(cls, i)] = NodeSession(cls, result, {})
|
|
37
41
|
|
|
38
42
|
await node_collection.close_all(with_value=result)
|
|
43
|
+
logger.debug("Impl {} succeeded with value {}", impl.__name__, result)
|
|
39
44
|
return result
|
|
40
45
|
|
|
41
46
|
raise ComposeError("No implementation found.")
|
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:
|
|
@@ -49,7 +63,7 @@ class RuleChain(dict[str, typing.Any]):
|
|
|
49
63
|
return cls
|
|
50
64
|
|
|
51
65
|
@classmethod
|
|
52
|
-
def
|
|
66
|
+
def get_subnodes(cls) -> dict:
|
|
53
67
|
return {"update": UpdateNode}
|
|
54
68
|
|
|
55
69
|
@classmethod
|
telegrinder/node/scope.py
CHANGED
|
@@ -33,4 +33,12 @@ def global_node(node: T) -> T:
|
|
|
33
33
|
return node
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
__all__ = (
|
|
36
|
+
__all__ = (
|
|
37
|
+
"NodeScope",
|
|
38
|
+
"PER_EVENT",
|
|
39
|
+
"PER_CALL",
|
|
40
|
+
"per_call",
|
|
41
|
+
"per_event",
|
|
42
|
+
"global_node",
|
|
43
|
+
"GLOBAL",
|
|
44
|
+
)
|
telegrinder/node/source.py
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
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.bot.cute_types import ChatJoinRequestCute
|
|
8
|
+
from telegrinder.node.base import ComposeError, DataNode, ScalarNode
|
|
9
|
+
from telegrinder.node.callback_query import CallbackQueryNode
|
|
10
|
+
from telegrinder.node.event import EventNode
|
|
11
|
+
from telegrinder.node.message import MessageNode
|
|
12
|
+
from telegrinder.node.polymorphic import Polymorphic, impl
|
|
13
|
+
from telegrinder.types.objects import Chat, Message, User
|
|
8
14
|
|
|
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
15
|
|
|
14
|
-
|
|
15
|
-
@dataclasses.dataclass(kw_only=True)
|
|
16
|
+
@dataclasses.dataclass(kw_only=True, slots=True)
|
|
16
17
|
class Source(Polymorphic, DataNode):
|
|
17
18
|
api: API
|
|
18
19
|
chat: Chat
|
|
@@ -24,7 +25,7 @@ class Source(Polymorphic, DataNode):
|
|
|
24
25
|
return cls(
|
|
25
26
|
api=message.ctx_api,
|
|
26
27
|
chat=message.chat,
|
|
27
|
-
from_user=message.from_user,
|
|
28
|
+
from_user=message.from_.expect(ComposeError("MessageNode has no from_user")),
|
|
28
29
|
thread_id=message.message_thread_id,
|
|
29
30
|
)
|
|
30
31
|
|
|
@@ -32,11 +33,20 @@ class Source(Polymorphic, DataNode):
|
|
|
32
33
|
async def compose_callback_query(cls, callback_query: CallbackQueryNode) -> typing.Self:
|
|
33
34
|
return cls(
|
|
34
35
|
api=callback_query.ctx_api,
|
|
35
|
-
chat=callback_query.chat.expect(ComposeError),
|
|
36
|
+
chat=callback_query.chat.expect(ComposeError("CallbackQueryNode has no chat")),
|
|
36
37
|
from_user=callback_query.from_user,
|
|
37
38
|
thread_id=callback_query.message_thread_id,
|
|
38
39
|
)
|
|
39
40
|
|
|
41
|
+
@impl
|
|
42
|
+
async def compose_chat_join_request(cls, chat_join_request: EventNode[ChatJoinRequestCute]) -> typing.Self:
|
|
43
|
+
return cls(
|
|
44
|
+
api=chat_join_request.ctx_api,
|
|
45
|
+
chat=chat_join_request.chat,
|
|
46
|
+
from_user=chat_join_request.from_user,
|
|
47
|
+
thread_id=Nothing(),
|
|
48
|
+
)
|
|
49
|
+
|
|
40
50
|
async def send(self, text: str) -> Message:
|
|
41
51
|
result = await self.api.send_message(
|
|
42
52
|
chat_id=self.chat.id,
|
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
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
from telegrinder.api import API
|
|
1
2
|
from telegrinder.bot.cute_types import UpdateCute
|
|
2
|
-
|
|
3
|
-
from .
|
|
3
|
+
from telegrinder.node.base import ScalarNode
|
|
4
|
+
from telegrinder.types import Update
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class UpdateNode(ScalarNode, UpdateCute):
|
|
7
8
|
@classmethod
|
|
8
|
-
async def compose(cls, update:
|
|
9
|
-
|
|
9
|
+
async def compose(cls, update: Update, api: API) -> UpdateCute:
|
|
10
|
+
if isinstance(update, UpdateCute):
|
|
11
|
+
return update
|
|
12
|
+
return UpdateCute.from_update(update, api)
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
__all__ = ("UpdateNode",)
|
telegrinder/py.typed
ADDED
|
File without changes
|