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.

Files changed (100) hide show
  1. telegrinder/__init__.py +9 -3
  2. telegrinder/bot/__init__.py +7 -5
  3. telegrinder/bot/cute_types/base.py +12 -14
  4. telegrinder/bot/cute_types/callback_query.py +54 -43
  5. telegrinder/bot/cute_types/chat_join_request.py +8 -7
  6. telegrinder/bot/cute_types/chat_member_updated.py +23 -17
  7. telegrinder/bot/cute_types/inline_query.py +1 -1
  8. telegrinder/bot/cute_types/message.py +331 -183
  9. telegrinder/bot/cute_types/update.py +4 -8
  10. telegrinder/bot/cute_types/utils.py +1 -5
  11. telegrinder/bot/dispatch/__init__.py +2 -3
  12. telegrinder/bot/dispatch/abc.py +4 -0
  13. telegrinder/bot/dispatch/context.py +9 -4
  14. telegrinder/bot/dispatch/dispatch.py +35 -33
  15. telegrinder/bot/dispatch/handler/func.py +34 -13
  16. telegrinder/bot/dispatch/handler/message_reply.py +6 -3
  17. telegrinder/bot/dispatch/middleware/abc.py +4 -4
  18. telegrinder/bot/dispatch/process.py +40 -13
  19. telegrinder/bot/dispatch/return_manager/abc.py +12 -12
  20. telegrinder/bot/dispatch/return_manager/callback_query.py +1 -3
  21. telegrinder/bot/dispatch/return_manager/inline_query.py +1 -3
  22. telegrinder/bot/dispatch/view/abc.py +37 -45
  23. telegrinder/bot/dispatch/view/box.py +66 -50
  24. telegrinder/bot/dispatch/view/message.py +1 -5
  25. telegrinder/bot/dispatch/view/raw.py +6 -6
  26. telegrinder/bot/dispatch/waiter_machine/__init__.py +2 -1
  27. telegrinder/bot/dispatch/waiter_machine/machine.py +77 -35
  28. telegrinder/bot/dispatch/waiter_machine/middleware.py +31 -7
  29. telegrinder/bot/dispatch/waiter_machine/short_state.py +17 -8
  30. telegrinder/bot/polling/polling.py +4 -4
  31. telegrinder/bot/rules/__init__.py +9 -6
  32. telegrinder/bot/rules/abc.py +99 -22
  33. telegrinder/bot/rules/adapter/__init__.py +4 -1
  34. telegrinder/bot/rules/adapter/abc.py +11 -6
  35. telegrinder/bot/rules/adapter/errors.py +1 -2
  36. telegrinder/bot/rules/adapter/event.py +14 -9
  37. telegrinder/bot/rules/adapter/node.py +42 -0
  38. telegrinder/bot/rules/callback_data.py +4 -4
  39. telegrinder/bot/rules/chat_join.py +3 -2
  40. telegrinder/bot/rules/command.py +26 -14
  41. telegrinder/bot/rules/enum_text.py +5 -5
  42. telegrinder/bot/rules/func.py +6 -6
  43. telegrinder/bot/rules/fuzzy.py +5 -7
  44. telegrinder/bot/rules/inline.py +4 -5
  45. telegrinder/bot/rules/integer.py +10 -8
  46. telegrinder/bot/rules/is_from.py +63 -91
  47. telegrinder/bot/rules/markup.py +5 -5
  48. telegrinder/bot/rules/mention.py +4 -4
  49. telegrinder/bot/rules/message.py +1 -1
  50. telegrinder/bot/rules/node.py +27 -0
  51. telegrinder/bot/rules/regex.py +5 -5
  52. telegrinder/bot/rules/rule_enum.py +4 -4
  53. telegrinder/bot/rules/start.py +5 -5
  54. telegrinder/bot/rules/text.py +9 -13
  55. telegrinder/bot/rules/update.py +4 -4
  56. telegrinder/bot/scenario/__init__.py +3 -3
  57. telegrinder/bot/scenario/choice.py +2 -3
  58. telegrinder/model.py +51 -16
  59. telegrinder/modules.py +14 -6
  60. telegrinder/msgspec_utils.py +67 -23
  61. telegrinder/node/__init__.py +26 -8
  62. telegrinder/node/attachment.py +13 -9
  63. telegrinder/node/base.py +27 -14
  64. telegrinder/node/callback_query.py +18 -0
  65. telegrinder/node/command.py +29 -0
  66. telegrinder/node/composer.py +119 -30
  67. telegrinder/node/me.py +14 -0
  68. telegrinder/node/message.py +2 -4
  69. telegrinder/node/polymorphic.py +44 -0
  70. telegrinder/node/rule.py +26 -22
  71. telegrinder/node/scope.py +36 -0
  72. telegrinder/node/source.py +37 -10
  73. telegrinder/node/text.py +11 -5
  74. telegrinder/node/tools/__init__.py +2 -2
  75. telegrinder/node/tools/generator.py +6 -6
  76. telegrinder/tools/__init__.py +8 -13
  77. telegrinder/tools/buttons.py +23 -17
  78. telegrinder/tools/error_handler/error_handler.py +11 -14
  79. telegrinder/tools/formatting/__init__.py +0 -6
  80. telegrinder/tools/formatting/html.py +10 -12
  81. telegrinder/tools/formatting/links.py +0 -5
  82. telegrinder/tools/formatting/spec_html_formats.py +0 -11
  83. telegrinder/tools/global_context/abc.py +1 -3
  84. telegrinder/tools/global_context/global_context.py +6 -16
  85. telegrinder/tools/i18n/simple.py +1 -3
  86. telegrinder/tools/kb_set/yaml.py +1 -2
  87. telegrinder/tools/keyboard.py +7 -8
  88. telegrinder/tools/limited_dict.py +1 -1
  89. telegrinder/tools/loop_wrapper/loop_wrapper.py +6 -5
  90. telegrinder/tools/magic.py +27 -5
  91. telegrinder/types/__init__.py +20 -0
  92. telegrinder/types/enums.py +37 -31
  93. telegrinder/types/methods.py +608 -327
  94. telegrinder/types/objects.py +1139 -716
  95. {telegrinder-0.1.dev168.dist-info → telegrinder-0.1.dev170.dist-info}/LICENSE +1 -1
  96. {telegrinder-0.1.dev168.dist-info → telegrinder-0.1.dev170.dist-info}/METADATA +6 -5
  97. telegrinder-0.1.dev170.dist-info/RECORD +143 -0
  98. telegrinder/bot/dispatch/composition.py +0 -88
  99. telegrinder-0.1.dev168.dist-info/RECORD +0 -137
  100. {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)
@@ -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.node import Node
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
- async def close(self, with_value: typing.Any | None = None) -> None:
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(self, with_value: typing.Any | None = None) -> None:
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
- 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)
138
+ class Composition:
139
+ nodes: dict[str, type[Node]]
58
140
 
59
- generator: typing.AsyncGenerator | None
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
- 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
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
- return NodeSession(value, context.sessions, generator)
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"))
@@ -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) -> typing.Self:
9
+ async def compose(cls, update: UpdateNode) -> "MessageNode":
12
10
  if not update.message:
13
11
  raise ComposeError
14
- return cls(
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
- class RuleContext(dict):
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__ = ("RuleContext",)
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")
@@ -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
- thread_id: int | None = None
19
+ from_user: User
20
+ thread_id: Option[int] = dataclasses.field(default_factory=lambda: Nothing())
16
21
 
17
- @classmethod
18
- async def compose(cls, message: MessageNode) -> typing.Self:
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
- thread_id=message.message_thread_id.unwrap_or_none(),
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
- __all__ = ("Source",)
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) -> typing.Self:
7
+ async def compose(cls, message: MessageNode) -> str:
10
8
  if not message.text:
11
9
  raise ComposeError("Message has no text")
12
- return cls(message.text.unwrap())
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 generate
1
+ from .generator import generate_node
2
2
 
3
- __all__ = ("generate",)
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 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]:
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__ = ("generate",)
40
+ __all__ = ("generate_node",)
@@ -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
- "user_open_message",
146
- "user_open_message_link",
135
+ "bold",
136
+ "channel_boost_link",
137
+ "code_inline",
138
+ "ctx_var",
139
+ "block_quote",
140
+ "impl",
141
+ "resolve_arg_names",
147
142
  )
@@ -47,30 +47,36 @@ class RowButtons(typing.Generic[ButtonT]):
47
47
  @dataclasses.dataclass
48
48
  class Button(BaseButton):
49
49
  text: str
50
- _: dataclasses.KW_ONLY
51
- request_contact: bool = False
52
- request_location: bool = False
53
- request_chat: dict[str, typing.Any] | KeyboardButtonRequestChat | None = None
54
- request_user: dict[str, typing.Any] | KeyboardButtonRequestUsers | None = None
55
- request_poll: dict[str, typing.Any] | KeyboardButtonPollType | None = None
56
- web_app: dict[str, typing.Any] | WebAppInfo | None = None
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
- _: dataclasses.KW_ONLY
63
- url: str | None = None
64
- login_url: dict[str, typing.Any] | LoginUrl | None = None
65
- pay: bool | None = None
66
- callback_data: str | dict[str, typing.Any] | DataclassInstance | msgspec.Struct | None = None
67
- callback_game: dict[str, typing.Any] | CallbackGame | None = None
68
- switch_inline_query: str | None = None
69
- switch_inline_query_current_chat: str | None = None
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__ = (