telegrinder 0.1.dev168__py3-none-any.whl → 0.1.dev170__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 +9 -3
- telegrinder/bot/__init__.py +7 -5
- telegrinder/bot/cute_types/base.py +12 -14
- telegrinder/bot/cute_types/callback_query.py +54 -43
- telegrinder/bot/cute_types/chat_join_request.py +8 -7
- telegrinder/bot/cute_types/chat_member_updated.py +23 -17
- telegrinder/bot/cute_types/inline_query.py +1 -1
- telegrinder/bot/cute_types/message.py +331 -183
- telegrinder/bot/cute_types/update.py +4 -8
- telegrinder/bot/cute_types/utils.py +1 -5
- telegrinder/bot/dispatch/__init__.py +2 -3
- telegrinder/bot/dispatch/abc.py +4 -0
- telegrinder/bot/dispatch/context.py +9 -4
- telegrinder/bot/dispatch/dispatch.py +35 -33
- telegrinder/bot/dispatch/handler/func.py +34 -13
- telegrinder/bot/dispatch/handler/message_reply.py +6 -3
- telegrinder/bot/dispatch/middleware/abc.py +4 -4
- telegrinder/bot/dispatch/process.py +40 -13
- telegrinder/bot/dispatch/return_manager/abc.py +12 -12
- telegrinder/bot/dispatch/return_manager/callback_query.py +1 -3
- telegrinder/bot/dispatch/return_manager/inline_query.py +1 -3
- telegrinder/bot/dispatch/view/abc.py +37 -45
- telegrinder/bot/dispatch/view/box.py +66 -50
- telegrinder/bot/dispatch/view/message.py +1 -5
- telegrinder/bot/dispatch/view/raw.py +6 -6
- telegrinder/bot/dispatch/waiter_machine/__init__.py +2 -1
- telegrinder/bot/dispatch/waiter_machine/machine.py +77 -35
- telegrinder/bot/dispatch/waiter_machine/middleware.py +31 -7
- telegrinder/bot/dispatch/waiter_machine/short_state.py +17 -8
- telegrinder/bot/polling/polling.py +4 -4
- telegrinder/bot/rules/__init__.py +9 -6
- telegrinder/bot/rules/abc.py +99 -22
- telegrinder/bot/rules/adapter/__init__.py +4 -1
- telegrinder/bot/rules/adapter/abc.py +11 -6
- telegrinder/bot/rules/adapter/errors.py +1 -2
- telegrinder/bot/rules/adapter/event.py +14 -9
- telegrinder/bot/rules/adapter/node.py +42 -0
- telegrinder/bot/rules/callback_data.py +4 -4
- telegrinder/bot/rules/chat_join.py +3 -2
- telegrinder/bot/rules/command.py +26 -14
- telegrinder/bot/rules/enum_text.py +5 -5
- telegrinder/bot/rules/func.py +6 -6
- telegrinder/bot/rules/fuzzy.py +5 -7
- telegrinder/bot/rules/inline.py +4 -5
- telegrinder/bot/rules/integer.py +10 -8
- telegrinder/bot/rules/is_from.py +63 -91
- telegrinder/bot/rules/markup.py +5 -5
- telegrinder/bot/rules/mention.py +4 -4
- telegrinder/bot/rules/message.py +1 -1
- telegrinder/bot/rules/node.py +27 -0
- telegrinder/bot/rules/regex.py +5 -5
- telegrinder/bot/rules/rule_enum.py +4 -4
- telegrinder/bot/rules/start.py +5 -5
- telegrinder/bot/rules/text.py +9 -13
- telegrinder/bot/rules/update.py +4 -4
- telegrinder/bot/scenario/__init__.py +3 -3
- telegrinder/bot/scenario/choice.py +2 -3
- telegrinder/model.py +51 -16
- telegrinder/modules.py +14 -6
- telegrinder/msgspec_utils.py +67 -23
- telegrinder/node/__init__.py +26 -8
- telegrinder/node/attachment.py +13 -9
- telegrinder/node/base.py +27 -14
- telegrinder/node/callback_query.py +18 -0
- telegrinder/node/command.py +29 -0
- telegrinder/node/composer.py +119 -30
- telegrinder/node/me.py +14 -0
- telegrinder/node/message.py +2 -4
- telegrinder/node/polymorphic.py +44 -0
- telegrinder/node/rule.py +26 -22
- telegrinder/node/scope.py +36 -0
- telegrinder/node/source.py +37 -10
- telegrinder/node/text.py +11 -5
- telegrinder/node/tools/__init__.py +2 -2
- telegrinder/node/tools/generator.py +6 -6
- telegrinder/tools/__init__.py +8 -13
- telegrinder/tools/buttons.py +23 -17
- telegrinder/tools/error_handler/error_handler.py +11 -14
- telegrinder/tools/formatting/__init__.py +0 -6
- telegrinder/tools/formatting/html.py +10 -12
- telegrinder/tools/formatting/links.py +0 -5
- telegrinder/tools/formatting/spec_html_formats.py +0 -11
- telegrinder/tools/global_context/abc.py +1 -3
- telegrinder/tools/global_context/global_context.py +6 -16
- telegrinder/tools/i18n/simple.py +1 -3
- telegrinder/tools/kb_set/yaml.py +1 -2
- telegrinder/tools/keyboard.py +7 -8
- telegrinder/tools/limited_dict.py +1 -1
- telegrinder/tools/loop_wrapper/loop_wrapper.py +6 -5
- telegrinder/tools/magic.py +27 -5
- telegrinder/types/__init__.py +20 -0
- telegrinder/types/enums.py +37 -31
- telegrinder/types/methods.py +608 -327
- telegrinder/types/objects.py +1139 -716
- {telegrinder-0.1.dev168.dist-info → telegrinder-0.1.dev170.dist-info}/LICENSE +1 -1
- {telegrinder-0.1.dev168.dist-info → telegrinder-0.1.dev170.dist-info}/METADATA +6 -5
- telegrinder-0.1.dev170.dist-info/RECORD +143 -0
- telegrinder/bot/dispatch/composition.py +0 -88
- telegrinder-0.1.dev168.dist-info/RECORD +0 -137
- {telegrinder-0.1.dev168.dist-info → telegrinder-0.1.dev170.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from fntypes import Nothing, Option, Some
|
|
4
|
+
|
|
5
|
+
from .base import DataNode
|
|
6
|
+
from .text import Text
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def single_split(s: str, separator: str) -> tuple[str, str]:
|
|
10
|
+
left, *right = s.split(separator, 1)
|
|
11
|
+
return left, (right[0] if right else "")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def cut_mention(text: str) -> tuple[str, Option[str]]:
|
|
15
|
+
left, right = single_split(text, "@")
|
|
16
|
+
return left, Some(right) if right else Nothing()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CommandInfo(DataNode):
|
|
21
|
+
name: str
|
|
22
|
+
arguments: str
|
|
23
|
+
mention: Option[str] = field(default_factory=Nothing)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
async def compose(cls, text: Text):
|
|
27
|
+
name, arguments = single_split(text, separator=" ")
|
|
28
|
+
name, mention = cut_mention(name)
|
|
29
|
+
return cls(name, arguments, mention)
|
telegrinder/node/composer.py
CHANGED
|
@@ -1,23 +1,112 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
|
+
from telegrinder.api.api import API
|
|
3
4
|
from telegrinder.bot.cute_types import UpdateCute
|
|
4
|
-
from telegrinder.
|
|
5
|
+
from telegrinder.bot.dispatch.context import Context
|
|
6
|
+
from telegrinder.node.base import ComposeError, Node
|
|
7
|
+
from telegrinder.node.scope import NodeScope
|
|
8
|
+
from telegrinder.tools.magic import get_annotations, magic_bundle
|
|
9
|
+
|
|
10
|
+
CONTEXT_STORE_NODES_KEY = "node_ctx"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def compose_node(
|
|
14
|
+
_node: type[Node],
|
|
15
|
+
update: UpdateCute,
|
|
16
|
+
ctx: Context,
|
|
17
|
+
) -> "NodeSession":
|
|
18
|
+
node = _node.as_node()
|
|
19
|
+
context = NodeCollection({})
|
|
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
|
|
38
|
+
|
|
39
|
+
if node.is_generator():
|
|
40
|
+
generator = typing.cast(typing.AsyncGenerator, node.compose(**context.values()))
|
|
41
|
+
value = await generator.asend(None)
|
|
42
|
+
else:
|
|
43
|
+
generator = None
|
|
44
|
+
value = await node.compose(**context.values()) # type: ignore
|
|
45
|
+
|
|
46
|
+
return NodeSession(_node, value, context.sessions, generator)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def compose_nodes(
|
|
50
|
+
node_types: dict[str, type[Node]],
|
|
51
|
+
update: UpdateCute,
|
|
52
|
+
ctx: Context,
|
|
53
|
+
) -> typing.Optional["NodeCollection"]:
|
|
54
|
+
nodes: dict[str, NodeSession] = {}
|
|
55
|
+
node_ctx: dict[type[Node], "NodeSession"] = ctx.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
56
|
+
|
|
57
|
+
for name, node_t in node_types.items():
|
|
58
|
+
scope = getattr(node_t, "scope", None)
|
|
59
|
+
try:
|
|
60
|
+
if scope is NodeScope.PER_EVENT and node_t in node_ctx:
|
|
61
|
+
nodes[name] = node_ctx[node_t]
|
|
62
|
+
continue
|
|
63
|
+
elif scope is NodeScope.GLOBAL and hasattr(node_t, "_value"):
|
|
64
|
+
nodes[name] = getattr(node_t, "_value")
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
nodes[name] = await compose_node(
|
|
68
|
+
node_t,
|
|
69
|
+
update,
|
|
70
|
+
ctx,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if scope is NodeScope.PER_EVENT:
|
|
74
|
+
node_ctx[node_t] = nodes[name]
|
|
75
|
+
elif scope is NodeScope.GLOBAL:
|
|
76
|
+
setattr(node_t, "_value", nodes[name])
|
|
77
|
+
|
|
78
|
+
except ComposeError:
|
|
79
|
+
await NodeCollection(nodes).close_all()
|
|
80
|
+
return None
|
|
81
|
+
return NodeCollection(nodes)
|
|
5
82
|
|
|
6
83
|
|
|
7
84
|
class NodeSession:
|
|
8
85
|
def __init__(
|
|
9
86
|
self,
|
|
87
|
+
node_type: type[Node] | None,
|
|
10
88
|
value: typing.Any,
|
|
11
89
|
subnodes: dict[str, typing.Self],
|
|
12
90
|
generator: typing.AsyncGenerator[typing.Any, None] | None = None,
|
|
13
91
|
):
|
|
92
|
+
self.node_type = node_type
|
|
14
93
|
self.value = value
|
|
15
94
|
self.subnodes = subnodes
|
|
16
95
|
self.generator = generator
|
|
17
96
|
|
|
18
|
-
|
|
97
|
+
def __repr__(self) -> str:
|
|
98
|
+
return f"<{self.__class__.__name__}: {self.value}" + ("ACTIVE>" if self.generator else ">")
|
|
99
|
+
|
|
100
|
+
async def close(
|
|
101
|
+
self,
|
|
102
|
+
with_value: typing.Any | None = None,
|
|
103
|
+
scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
|
|
104
|
+
) -> None:
|
|
105
|
+
if self.node_type and getattr(self.node_type, "scope", None) not in scopes:
|
|
106
|
+
return
|
|
107
|
+
|
|
19
108
|
for subnode in self.subnodes.values():
|
|
20
|
-
await subnode.close()
|
|
109
|
+
await subnode.close(scopes=scopes)
|
|
21
110
|
|
|
22
111
|
if self.generator is None:
|
|
23
112
|
return
|
|
@@ -26,46 +115,46 @@ class NodeSession:
|
|
|
26
115
|
except StopAsyncIteration:
|
|
27
116
|
self.generator = None
|
|
28
117
|
|
|
29
|
-
def __repr__(self) -> str:
|
|
30
|
-
return f"<{self.__class__.__name__}: {self.value}" + ("ACTIVE>" if self.generator else ">")
|
|
31
|
-
|
|
32
118
|
|
|
33
119
|
class NodeCollection:
|
|
34
120
|
def __init__(self, sessions: dict[str, NodeSession]) -> None:
|
|
35
121
|
self.sessions = sessions
|
|
36
122
|
|
|
123
|
+
def __repr__(self) -> str:
|
|
124
|
+
return "<{}: sessions={}>".format(self.__class__.__name__, self.sessions)
|
|
125
|
+
|
|
37
126
|
def values(self) -> dict[str, typing.Any]:
|
|
38
127
|
return {name: session.value for name, session in self.sessions.items()}
|
|
39
128
|
|
|
40
|
-
async def close_all(
|
|
129
|
+
async def close_all(
|
|
130
|
+
self,
|
|
131
|
+
with_value: typing.Any | None = None,
|
|
132
|
+
scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
|
|
133
|
+
) -> None:
|
|
41
134
|
for session in self.sessions.values():
|
|
42
|
-
await session.close(with_value)
|
|
135
|
+
await session.close(with_value, scopes=scopes)
|
|
43
136
|
|
|
44
137
|
|
|
45
|
-
|
|
46
|
-
|
|
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)
|
|
138
|
+
class Composition:
|
|
139
|
+
nodes: dict[str, type[Node]]
|
|
58
140
|
|
|
59
|
-
|
|
141
|
+
def __init__(self, func: typing.Callable[..., typing.Any], is_blocking: bool) -> None:
|
|
142
|
+
self.func = func
|
|
143
|
+
self.nodes = get_annotations(func)
|
|
144
|
+
self.is_blocking = is_blocking
|
|
60
145
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
146
|
+
def __repr__(self) -> str:
|
|
147
|
+
return "<{}: for function={!r} with nodes={}>".format(
|
|
148
|
+
("blocking " if self.is_blocking else "") + self.__class__.__name__,
|
|
149
|
+
self.func.__name__,
|
|
150
|
+
self.nodes,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
async def compose_nodes(self, update: UpdateCute, context: Context) -> NodeCollection | None:
|
|
154
|
+
return await compose_nodes(self.nodes, update, context)
|
|
67
155
|
|
|
68
|
-
|
|
156
|
+
async def __call__(self, **kwargs: typing.Any) -> typing.Any:
|
|
157
|
+
return await self.func(**magic_bundle(self.func, kwargs, start_idx=0, bundle_ctx=False)) # type: ignore
|
|
69
158
|
|
|
70
159
|
|
|
71
|
-
__all__ = ("NodeCollection", "NodeSession", "compose_node")
|
|
160
|
+
__all__ = ("NodeCollection", "NodeSession", "Composition", "compose_node", "compose_nodes")
|
telegrinder/node/me.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from telegrinder.api.api import API
|
|
2
|
+
from telegrinder.types import User
|
|
3
|
+
|
|
4
|
+
from .base import ComposeError, ScalarNode
|
|
5
|
+
from .scope import GLOBAL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Me(ScalarNode, User):
|
|
9
|
+
scope = GLOBAL
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
async def compose(cls, api: API) -> User:
|
|
13
|
+
me = await api.get_me()
|
|
14
|
+
return me.expect(ComposeError("Can't complete get_me request"))
|
telegrinder/node/message.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import typing
|
|
2
|
-
|
|
3
1
|
from telegrinder.bot.cute_types import MessageCute
|
|
4
2
|
|
|
5
3
|
from .base import ComposeError, ScalarNode
|
|
@@ -8,10 +6,10 @@ from .update import UpdateNode
|
|
|
8
6
|
|
|
9
7
|
class MessageNode(ScalarNode, MessageCute):
|
|
10
8
|
@classmethod
|
|
11
|
-
async def compose(cls, update: UpdateNode) ->
|
|
9
|
+
async def compose(cls, update: UpdateNode) -> "MessageNode":
|
|
12
10
|
if not update.message:
|
|
13
11
|
raise ComposeError
|
|
14
|
-
return
|
|
12
|
+
return MessageNode(
|
|
15
13
|
**update.message.unwrap().to_dict(),
|
|
16
14
|
api=update.api,
|
|
17
15
|
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from telegrinder.bot.dispatch.context import Context
|
|
5
|
+
from telegrinder.tools.magic import get_impls, impl
|
|
6
|
+
|
|
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
|
+
|
|
13
|
+
class Polymorphic:
|
|
14
|
+
@classmethod
|
|
15
|
+
async def compose(cls, update: UpdateNode, context: Context) -> typing.Any:
|
|
16
|
+
scope = getattr(cls, "scope", None)
|
|
17
|
+
node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
18
|
+
|
|
19
|
+
for i, impl in enumerate(get_impls(cls)):
|
|
20
|
+
composition = Composition(impl, True)
|
|
21
|
+
node_collection = await composition.compose_nodes(update, context)
|
|
22
|
+
if node_collection is None:
|
|
23
|
+
continue
|
|
24
|
+
|
|
25
|
+
# To determine whether this is a right morph, all subnodes must be resolved
|
|
26
|
+
if scope is NodeScope.PER_EVENT and (cls, i) in node_ctx:
|
|
27
|
+
result: NodeSession = node_ctx[(cls, i)]
|
|
28
|
+
await node_collection.close_all()
|
|
29
|
+
return result.value
|
|
30
|
+
|
|
31
|
+
result = composition.func(cls, **node_collection.values())
|
|
32
|
+
if inspect.isawaitable(result):
|
|
33
|
+
result = await result
|
|
34
|
+
|
|
35
|
+
if scope is NodeScope.PER_EVENT:
|
|
36
|
+
node_ctx[(cls, i)] = NodeSession(cls, result, {}) # type: ignore
|
|
37
|
+
|
|
38
|
+
await node_collection.close_all(with_value=result)
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
raise ComposeError("No implementation found.")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__all__ = ("Polymorphic", "impl")
|
telegrinder/node/rule.py
CHANGED
|
@@ -2,18 +2,39 @@ import dataclasses
|
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
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
5
|
from telegrinder.node.base import ComposeError, Node
|
|
8
6
|
from telegrinder.node.update import UpdateNode
|
|
9
7
|
|
|
8
|
+
if typing.TYPE_CHECKING:
|
|
9
|
+
from telegrinder.bot.rules.abc import ABCRule
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
class RuleChain(dict[str, typing.Any]):
|
|
12
13
|
dataclass = dict
|
|
13
|
-
rules: tuple[ABCRule, ...] = ()
|
|
14
|
+
rules: tuple["ABCRule", ...] = ()
|
|
15
|
+
|
|
16
|
+
def __init_subclass__(cls) -> None:
|
|
17
|
+
if cls.__name__ == "_RuleNode":
|
|
18
|
+
return
|
|
19
|
+
cls.dataclass = cls.generate_node_dataclass(cls)
|
|
20
|
+
|
|
21
|
+
def __new__(cls, *rules: "ABCRule") -> type[Node]:
|
|
22
|
+
return type("_RuleNode", (cls,), {"dataclass": dict, "rules": rules}) # type: ignore
|
|
23
|
+
|
|
24
|
+
def __class_getitem__(cls, items: typing.Union[tuple["ABCRule", ...], "ABCRule"]) -> typing.Self:
|
|
25
|
+
if not isinstance(items, tuple):
|
|
26
|
+
items = (items,)
|
|
27
|
+
assert all(isinstance(rule, "ABCRule") for rule in items), "All items must be instances of 'ABCRule'."
|
|
28
|
+
return cls(*items)
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def generate_node_dataclass(cls_: type["RuleChain"]): # noqa: ANN205
|
|
32
|
+
return dataclasses.dataclass(type(cls_.__name__, (object,), dict(cls_.__dict__)))
|
|
14
33
|
|
|
15
34
|
@classmethod
|
|
16
35
|
async def compose(cls, update: UpdateNode):
|
|
36
|
+
from telegrinder.bot.dispatch.process import check_rule
|
|
37
|
+
|
|
17
38
|
ctx = Context()
|
|
18
39
|
for rule in cls.rules:
|
|
19
40
|
if not await check_rule(update.api, rule, update, ctx):
|
|
@@ -35,22 +56,5 @@ class RuleContext(dict):
|
|
|
35
56
|
def is_generator(cls) -> typing.Literal[False]:
|
|
36
57
|
return False
|
|
37
58
|
|
|
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
59
|
|
|
56
|
-
__all__ = ("
|
|
60
|
+
__all__ = ("RuleChain",)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
if typing.TYPE_CHECKING:
|
|
5
|
+
from .base import Node
|
|
6
|
+
|
|
7
|
+
T = typing.TypeVar("T", bound=type["Node"])
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NodeScope(enum.Enum):
|
|
11
|
+
GLOBAL = enum.auto()
|
|
12
|
+
PER_EVENT = enum.auto()
|
|
13
|
+
PER_CALL = enum.auto()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
PER_EVENT = NodeScope.PER_EVENT
|
|
17
|
+
PER_CALL = NodeScope.PER_CALL
|
|
18
|
+
GLOBAL = NodeScope.GLOBAL
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def per_call(node: T) -> T:
|
|
22
|
+
setattr(node, "scope", PER_CALL)
|
|
23
|
+
return node
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def per_event(node: T) -> T:
|
|
27
|
+
setattr(node, "scope", PER_EVENT)
|
|
28
|
+
return node
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def global_node(node: T) -> T:
|
|
32
|
+
setattr(node, "scope", GLOBAL)
|
|
33
|
+
return node
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ("NodeScope", "PER_EVENT", "PER_CALL", "per_call", "per_event", "global_node", "GLOBAL")
|
telegrinder/node/source.py
CHANGED
|
@@ -1,34 +1,61 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
|
+
from fntypes import Nothing, Option
|
|
5
|
+
|
|
4
6
|
from telegrinder.api import API
|
|
5
|
-
from telegrinder.types import Chat, Message
|
|
7
|
+
from telegrinder.types import Chat, Message, User
|
|
6
8
|
|
|
7
|
-
from .base import DataNode
|
|
9
|
+
from .base import ComposeError, DataNode, ScalarNode
|
|
10
|
+
from .callback_query import CallbackQueryNode
|
|
8
11
|
from .message import MessageNode
|
|
12
|
+
from .polymorphic import Polymorphic, impl
|
|
9
13
|
|
|
10
14
|
|
|
11
|
-
@dataclasses.dataclass
|
|
12
|
-
class Source(DataNode):
|
|
15
|
+
@dataclasses.dataclass(kw_only=True)
|
|
16
|
+
class Source(Polymorphic, DataNode):
|
|
13
17
|
api: API
|
|
14
18
|
chat: Chat
|
|
15
|
-
|
|
19
|
+
from_user: User
|
|
20
|
+
thread_id: Option[int] = dataclasses.field(default_factory=lambda: Nothing())
|
|
16
21
|
|
|
17
|
-
@
|
|
18
|
-
async def
|
|
22
|
+
@impl
|
|
23
|
+
async def compose_message(cls, message: MessageNode) -> typing.Self:
|
|
19
24
|
return cls(
|
|
20
25
|
api=message.ctx_api,
|
|
21
26
|
chat=message.chat,
|
|
22
|
-
|
|
27
|
+
from_user=message.from_user,
|
|
28
|
+
thread_id=message.message_thread_id,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
@impl
|
|
32
|
+
async def compose_callback_query(cls, callback_query: CallbackQueryNode) -> typing.Self:
|
|
33
|
+
return cls(
|
|
34
|
+
api=callback_query.ctx_api,
|
|
35
|
+
chat=callback_query.chat.expect(ComposeError),
|
|
36
|
+
from_user=callback_query.from_user,
|
|
37
|
+
thread_id=callback_query.message_thread_id,
|
|
23
38
|
)
|
|
24
39
|
|
|
25
40
|
async def send(self, text: str) -> Message:
|
|
26
41
|
result = await self.api.send_message(
|
|
27
42
|
chat_id=self.chat.id,
|
|
28
|
-
message_thread_id=self.thread_id,
|
|
43
|
+
message_thread_id=self.thread_id.unwrap_or_none(),
|
|
29
44
|
text=text,
|
|
30
45
|
)
|
|
31
46
|
return result.unwrap()
|
|
32
47
|
|
|
33
48
|
|
|
34
|
-
|
|
49
|
+
class ChatSource(ScalarNode, Chat):
|
|
50
|
+
@classmethod
|
|
51
|
+
async def compose(cls, source: Source) -> Chat:
|
|
52
|
+
return source.chat
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class UserSource(ScalarNode, User):
|
|
56
|
+
@classmethod
|
|
57
|
+
async def compose(cls, source: Source) -> User:
|
|
58
|
+
return source.from_user
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__all__ = ("Source", "ChatSource", "UserSource")
|
telegrinder/node/text.py
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import typing
|
|
2
|
-
|
|
3
1
|
from .base import ComposeError, ScalarNode
|
|
4
2
|
from .message import MessageNode
|
|
5
3
|
|
|
6
4
|
|
|
7
5
|
class Text(ScalarNode, str):
|
|
8
6
|
@classmethod
|
|
9
|
-
async def compose(cls, message: MessageNode) ->
|
|
7
|
+
async def compose(cls, message: MessageNode) -> str:
|
|
10
8
|
if not message.text:
|
|
11
9
|
raise ComposeError("Message has no text")
|
|
12
|
-
return
|
|
10
|
+
return message.text.unwrap()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TextInteger(ScalarNode, int):
|
|
14
|
+
@classmethod
|
|
15
|
+
async def compose(cls, text: Text) -> int:
|
|
16
|
+
if not text.isdigit():
|
|
17
|
+
raise ComposeError("Text is not digit")
|
|
18
|
+
return int(text)
|
|
13
19
|
|
|
14
20
|
|
|
15
|
-
__all__ = ("Text",)
|
|
21
|
+
__all__ = ("Text", "TextInteger")
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
from .generator import
|
|
1
|
+
from .generator import generate_node
|
|
2
2
|
|
|
3
|
-
__all__ = ("
|
|
3
|
+
__all__ = ("generate_node",)
|
|
@@ -19,11 +19,11 @@ def error_on_none(value: T | None) -> T:
|
|
|
19
19
|
return value
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def
|
|
23
|
-
subnodes: tuple[type[Node], ...],
|
|
24
|
-
func: typing.Callable[...,
|
|
25
|
-
casts: tuple[typing.Callable, ...] = (cast_false_to_none, error_on_none),
|
|
26
|
-
) -> type[
|
|
22
|
+
def generate_node(
|
|
23
|
+
subnodes: tuple[type["Node"], ...],
|
|
24
|
+
func: typing.Callable[..., T],
|
|
25
|
+
casts: tuple[typing.Callable[[typing.Any], typing.Any], ...] = (cast_false_to_none, error_on_none),
|
|
26
|
+
) -> type["Node"]:
|
|
27
27
|
async def compose(**kw: typing.Any) -> typing.Any:
|
|
28
28
|
args = await ContainerNode.compose(**kw)
|
|
29
29
|
result = func(*args)
|
|
@@ -37,4 +37,4 @@ def generate(
|
|
|
37
37
|
return type("_ContainerNode", (container,), {"compose": compose})
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
__all__ = ("
|
|
40
|
+
__all__ = ("generate_node",)
|
telegrinder/tools/__init__.py
CHANGED
|
@@ -14,7 +14,6 @@ from .formatting import (
|
|
|
14
14
|
StartBotLink,
|
|
15
15
|
StartGroupLink,
|
|
16
16
|
TgEmoji,
|
|
17
|
-
UserOpenMessage,
|
|
18
17
|
block_quote,
|
|
19
18
|
bold,
|
|
20
19
|
channel_boost_link,
|
|
@@ -38,8 +37,6 @@ from .formatting import (
|
|
|
38
37
|
strike,
|
|
39
38
|
tg_emoji,
|
|
40
39
|
underline,
|
|
41
|
-
user_open_message,
|
|
42
|
-
user_open_message_link,
|
|
43
40
|
)
|
|
44
41
|
from .global_context import (
|
|
45
42
|
ABCGlobalContext,
|
|
@@ -68,7 +65,7 @@ from .keyboard import (
|
|
|
68
65
|
)
|
|
69
66
|
from .limited_dict import LimitedDict
|
|
70
67
|
from .loop_wrapper import ABCLoopWrapper, DelayedTask, Lifespan, LoopWrapper
|
|
71
|
-
from .magic import magic_bundle, resolve_arg_names
|
|
68
|
+
from .magic import impl, magic_bundle, resolve_arg_names
|
|
72
69
|
from .parse_mode import ParseMode
|
|
73
70
|
|
|
74
71
|
__all__ = (
|
|
@@ -115,12 +112,6 @@ __all__ = (
|
|
|
115
112
|
"StartGroupLink",
|
|
116
113
|
"TelegrinderCtx",
|
|
117
114
|
"TgEmoji",
|
|
118
|
-
"UserOpenMessage",
|
|
119
|
-
"block_quote",
|
|
120
|
-
"bold",
|
|
121
|
-
"channel_boost_link",
|
|
122
|
-
"code_inline",
|
|
123
|
-
"ctx_var",
|
|
124
115
|
"escape",
|
|
125
116
|
"get_channel_boost_link",
|
|
126
117
|
"get_invite_chat_link",
|
|
@@ -134,7 +125,6 @@ __all__ = (
|
|
|
134
125
|
"magic_bundle",
|
|
135
126
|
"mention",
|
|
136
127
|
"pre_code",
|
|
137
|
-
"resolve_arg_names",
|
|
138
128
|
"resolve_domain",
|
|
139
129
|
"spoiler",
|
|
140
130
|
"start_bot_link",
|
|
@@ -142,6 +132,11 @@ __all__ = (
|
|
|
142
132
|
"strike",
|
|
143
133
|
"tg_emoji",
|
|
144
134
|
"underline",
|
|
145
|
-
"
|
|
146
|
-
"
|
|
135
|
+
"bold",
|
|
136
|
+
"channel_boost_link",
|
|
137
|
+
"code_inline",
|
|
138
|
+
"ctx_var",
|
|
139
|
+
"block_quote",
|
|
140
|
+
"impl",
|
|
141
|
+
"resolve_arg_names",
|
|
147
142
|
)
|
telegrinder/tools/buttons.py
CHANGED
|
@@ -47,30 +47,36 @@ class RowButtons(typing.Generic[ButtonT]):
|
|
|
47
47
|
@dataclasses.dataclass
|
|
48
48
|
class Button(BaseButton):
|
|
49
49
|
text: str
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
request_contact: bool = dataclasses.field(default=False, kw_only=True)
|
|
51
|
+
request_location: bool = dataclasses.field(default=False, kw_only=True)
|
|
52
|
+
request_chat: dict[str, typing.Any] | KeyboardButtonRequestChat | None = dataclasses.field(
|
|
53
|
+
default=None, kw_only=True
|
|
54
|
+
)
|
|
55
|
+
request_user: dict[str, typing.Any] | KeyboardButtonRequestUsers | None = dataclasses.field(
|
|
56
|
+
default=None, kw_only=True
|
|
57
|
+
)
|
|
58
|
+
request_poll: dict[str, typing.Any] | KeyboardButtonPollType | None = dataclasses.field(
|
|
59
|
+
default=None, kw_only=True
|
|
60
|
+
)
|
|
61
|
+
web_app: dict[str, typing.Any] | WebAppInfo | None = dataclasses.field(default=None, kw_only=True)
|
|
57
62
|
|
|
58
63
|
|
|
59
64
|
@dataclasses.dataclass
|
|
60
65
|
class InlineButton(BaseButton):
|
|
61
66
|
text: str
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
url: str | None = dataclasses.field(default=None, kw_only=True)
|
|
68
|
+
login_url: dict[str, typing.Any] | LoginUrl | None = dataclasses.field(default=None, kw_only=True)
|
|
69
|
+
pay: bool | None = dataclasses.field(default=None, kw_only=True)
|
|
70
|
+
callback_data: str | dict[str, typing.Any] | DataclassInstance | msgspec.Struct | None = (
|
|
71
|
+
dataclasses.field(default=None, kw_only=True)
|
|
72
|
+
)
|
|
73
|
+
callback_game: dict[str, typing.Any] | CallbackGame | None = dataclasses.field(default=None, kw_only=True)
|
|
74
|
+
switch_inline_query: str | None = dataclasses.field(default=None, kw_only=True)
|
|
75
|
+
switch_inline_query_current_chat: str | None = dataclasses.field(default=None, kw_only=True)
|
|
70
76
|
switch_inline_query_chosen_chat: dict[str, typing.Any] | SwitchInlineQueryChosenChat | None = (
|
|
71
|
-
None
|
|
77
|
+
dataclasses.field(default=None, kw_only=True)
|
|
72
78
|
)
|
|
73
|
-
web_app: dict[str, typing.Any] | WebAppInfo | None = None
|
|
79
|
+
web_app: dict[str, typing.Any] | WebAppInfo | None = dataclasses.field(default=None, kw_only=True)
|
|
74
80
|
|
|
75
81
|
|
|
76
82
|
__all__ = (
|