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.

Files changed (79) hide show
  1. telegrinder/api/abc.py +7 -1
  2. telegrinder/api/api.py +12 -3
  3. telegrinder/api/error.py +2 -1
  4. telegrinder/bot/bot.py +6 -1
  5. telegrinder/bot/cute_types/base.py +144 -17
  6. telegrinder/bot/cute_types/callback_query.py +6 -1
  7. telegrinder/bot/cute_types/chat_member_updated.py +1 -2
  8. telegrinder/bot/cute_types/message.py +23 -11
  9. telegrinder/bot/cute_types/update.py +48 -0
  10. telegrinder/bot/cute_types/utils.py +2 -465
  11. telegrinder/bot/dispatch/__init__.py +2 -3
  12. telegrinder/bot/dispatch/abc.py +6 -3
  13. telegrinder/bot/dispatch/context.py +6 -6
  14. telegrinder/bot/dispatch/dispatch.py +61 -23
  15. telegrinder/bot/dispatch/handler/abc.py +2 -2
  16. telegrinder/bot/dispatch/handler/func.py +36 -17
  17. telegrinder/bot/dispatch/handler/message_reply.py +2 -2
  18. telegrinder/bot/dispatch/middleware/abc.py +2 -2
  19. telegrinder/bot/dispatch/process.py +10 -10
  20. telegrinder/bot/dispatch/return_manager/abc.py +3 -3
  21. telegrinder/bot/dispatch/view/abc.py +12 -15
  22. telegrinder/bot/dispatch/view/box.py +73 -62
  23. telegrinder/bot/dispatch/view/message.py +11 -3
  24. telegrinder/bot/dispatch/view/raw.py +3 -0
  25. telegrinder/bot/dispatch/waiter_machine/machine.py +2 -2
  26. telegrinder/bot/dispatch/waiter_machine/middleware.py +1 -1
  27. telegrinder/bot/dispatch/waiter_machine/short_state.py +2 -1
  28. telegrinder/bot/polling/polling.py +3 -3
  29. telegrinder/bot/rules/abc.py +11 -7
  30. telegrinder/bot/rules/adapter/event.py +7 -4
  31. telegrinder/bot/rules/adapter/node.py +1 -1
  32. telegrinder/bot/rules/command.py +5 -7
  33. telegrinder/bot/rules/func.py +1 -1
  34. telegrinder/bot/rules/fuzzy.py +1 -1
  35. telegrinder/bot/rules/integer.py +1 -2
  36. telegrinder/bot/rules/markup.py +3 -3
  37. telegrinder/bot/rules/message_entities.py +1 -1
  38. telegrinder/bot/rules/node.py +2 -2
  39. telegrinder/bot/rules/regex.py +1 -1
  40. telegrinder/bot/rules/rule_enum.py +1 -1
  41. telegrinder/bot/scenario/checkbox.py +2 -2
  42. telegrinder/model.py +87 -47
  43. telegrinder/modules.py +3 -3
  44. telegrinder/msgspec_utils.py +94 -13
  45. telegrinder/node/__init__.py +20 -11
  46. telegrinder/node/attachment.py +19 -16
  47. telegrinder/node/base.py +120 -24
  48. telegrinder/node/callback_query.py +5 -9
  49. telegrinder/node/command.py +6 -2
  50. telegrinder/node/composer.py +82 -54
  51. telegrinder/node/container.py +4 -4
  52. telegrinder/node/event.py +59 -0
  53. telegrinder/node/me.py +3 -0
  54. telegrinder/node/message.py +6 -10
  55. telegrinder/node/polymorphic.py +11 -12
  56. telegrinder/node/rule.py +27 -5
  57. telegrinder/node/source.py +10 -11
  58. telegrinder/node/text.py +4 -4
  59. telegrinder/node/update.py +1 -2
  60. telegrinder/py.typed +0 -0
  61. telegrinder/tools/__init__.py +2 -2
  62. telegrinder/tools/buttons.py +5 -10
  63. telegrinder/tools/error_handler/error.py +2 -0
  64. telegrinder/tools/error_handler/error_handler.py +1 -1
  65. telegrinder/tools/formatting/spec_html_formats.py +10 -10
  66. telegrinder/tools/global_context/__init__.py +2 -2
  67. telegrinder/tools/global_context/global_context.py +2 -2
  68. telegrinder/tools/global_context/telegrinder_ctx.py +4 -4
  69. telegrinder/tools/keyboard.py +2 -2
  70. telegrinder/tools/loop_wrapper/loop_wrapper.py +39 -5
  71. telegrinder/tools/magic.py +48 -15
  72. telegrinder/types/enums.py +1 -0
  73. telegrinder/types/methods.py +14 -5
  74. telegrinder/types/objects.py +3 -0
  75. {telegrinder-0.1.dev169.dist-info → telegrinder-0.1.dev171.dist-info}/METADATA +2 -2
  76. telegrinder-0.1.dev171.dist-info/RECORD +145 -0
  77. telegrinder-0.1.dev169.dist-info/RECORD +0 -143
  78. {telegrinder-0.1.dev169.dist-info → telegrinder-0.1.dev171.dist-info}/LICENSE +0 -0
  79. {telegrinder-0.1.dev169.dist-info → telegrinder-0.1.dev171.dist-info}/WHEEL +0 -0
@@ -1,11 +1,20 @@
1
+ import dataclasses
1
2
  import typing
2
3
 
3
- from telegrinder.api.api import API
4
- from telegrinder.bot.cute_types import UpdateCute
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.node.base import ComposeError, Node
7
- from telegrinder.node.scope import NodeScope
8
- from telegrinder.tools.magic import get_annotations, magic_bundle
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
- generator: typing.AsyncGenerator | None
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()) # type: ignore
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
- ) -> typing.Optional["NodeCollection"]:
54
- nodes: dict[str, NodeSession] = {}
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
- for name, node_t in node_types.items():
58
- scope = getattr(node_t, "scope", None)
59
- try:
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
- nodes[name] = node_ctx[node_t]
72
+ node_sessions[name] = node_ctx[node_t]
62
73
  continue
63
74
  elif scope is NodeScope.GLOBAL and hasattr(node_t, "_value"):
64
- nodes[name] = getattr(node_t, "_value")
75
+ node_sessions[name] = getattr(node_t, "_value")
65
76
  continue
66
77
 
67
- nodes[name] = await compose_node(
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] = nodes[name]
81
+ node_ctx[node_t] = node_sessions[name]
75
82
  elif scope is NodeScope.GLOBAL:
76
- setattr(node_t, "_value", nodes[name])
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
- except ComposeError:
79
- await NodeCollection(nodes).close_all()
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
- return NodeCollection(nodes)
101
+
102
+ return NodeCollection(node_sessions)
82
103
 
83
104
 
105
+ @dataclasses.dataclass(slots=True, repr=False)
84
106
  class NodeSession:
85
- def __init__(
86
- self,
87
- node_type: type[Node] | None,
88
- value: typing.Any,
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
- nodes: dict[str, type[Node]]
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 __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
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.__name__,
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(self.nodes, update, context)
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__ = ("NodeCollection", "NodeSession", "Composition", "compose_node", "compose_nodes")
188
+ __all__ = ("Composition", "NodeCollection", "NodeSession", "compose_node", "compose_nodes")
@@ -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(Node):
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["Node", ...]:
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["Node"]]:
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
@@ -12,3 +12,6 @@ class Me(ScalarNode, User):
12
12
  async def compose(cls, api: API) -> User:
13
13
  me = await api.get_me()
14
14
  return me.expect(ComposeError("Can't complete get_me request"))
15
+
16
+
17
+ __all__ = ("Me",)
@@ -1,18 +1,14 @@
1
- from telegrinder.bot.cute_types import MessageCute
2
-
3
- from .base import ComposeError, ScalarNode
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) -> "MessageNode":
8
+ async def compose(cls, update: UpdateNode) -> MessageCute:
10
9
  if not update.message:
11
- raise ComposeError
12
- return MessageNode(
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",)
@@ -2,38 +2,37 @@ import inspect
2
2
  import typing
3
3
 
4
4
  from telegrinder.bot.dispatch.context import Context
5
- from telegrinder.tools.magic import get_impls, impl
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(get_impls(cls)):
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
- result: NodeSession = node_ctx[(cls, i)]
26
+ res: NodeSession = node_ctx[(cls, i)]
28
27
  await node_collection.close_all()
29
- return result.value
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, {}) # type: ignore
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: typing.Union[tuple["ABCRule", ...], "ABCRule"]) -> typing.Self:
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
- from telegrinder.bot.dispatch.process import check_rule
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
@@ -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.types import Chat, Message, User
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
 
@@ -1,6 +1,5 @@
1
1
  from telegrinder.bot.cute_types import UpdateCute
2
-
3
- from .base import ScalarNode
2
+ from telegrinder.node.base import ScalarNode
4
3
 
5
4
 
6
5
  class UpdateNode(ScalarNode, UpdateCute):
telegrinder/py.typed ADDED
File without changes
@@ -43,7 +43,7 @@ from .global_context import (
43
43
  CtxVar,
44
44
  GlobalContext,
45
45
  GlobalCtxVar,
46
- TelegrinderCtx,
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
- "TelegrinderCtx",
113
+ "TelegrinderContext",
114
114
  "TgEmoji",
115
115
  "escape",
116
116
  "get_channel_boost_link",
@@ -3,25 +3,20 @@ import typing
3
3
 
4
4
  import msgspec
5
5
 
6
- from telegrinder.model import encoder
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)
@@ -1,4 +1,6 @@
1
1
  class CatcherError(BaseException):
2
+ __slots__ = ("exc", "message")
3
+
2
4
  def __init__(self, exc: BaseException, message: str) -> None:
3
5
  self.exc = exc
4
6
  self.message = message
@@ -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(