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.

Files changed (116) hide show
  1. telegrinder/__init__.py +2 -2
  2. telegrinder/api/__init__.py +1 -2
  3. telegrinder/api/api.py +15 -6
  4. telegrinder/api/error.py +2 -1
  5. telegrinder/api/token.py +36 -0
  6. telegrinder/bot/__init__.py +12 -6
  7. telegrinder/bot/bot.py +18 -6
  8. telegrinder/bot/cute_types/__init__.py +7 -7
  9. telegrinder/bot/cute_types/base.py +122 -20
  10. telegrinder/bot/cute_types/callback_query.py +10 -6
  11. telegrinder/bot/cute_types/chat_join_request.py +4 -5
  12. telegrinder/bot/cute_types/chat_member_updated.py +4 -6
  13. telegrinder/bot/cute_types/inline_query.py +3 -4
  14. telegrinder/bot/cute_types/message.py +32 -21
  15. telegrinder/bot/cute_types/update.py +51 -4
  16. telegrinder/bot/cute_types/utils.py +3 -466
  17. telegrinder/bot/dispatch/__init__.py +10 -11
  18. telegrinder/bot/dispatch/abc.py +8 -5
  19. telegrinder/bot/dispatch/context.py +17 -8
  20. telegrinder/bot/dispatch/dispatch.py +71 -48
  21. telegrinder/bot/dispatch/handler/__init__.py +3 -3
  22. telegrinder/bot/dispatch/handler/abc.py +4 -4
  23. telegrinder/bot/dispatch/handler/func.py +46 -22
  24. telegrinder/bot/dispatch/handler/message_reply.py +6 -7
  25. telegrinder/bot/dispatch/middleware/__init__.py +1 -1
  26. telegrinder/bot/dispatch/middleware/abc.py +2 -2
  27. telegrinder/bot/dispatch/process.py +38 -19
  28. telegrinder/bot/dispatch/return_manager/__init__.py +4 -4
  29. telegrinder/bot/dispatch/return_manager/abc.py +3 -3
  30. telegrinder/bot/dispatch/return_manager/callback_query.py +1 -2
  31. telegrinder/bot/dispatch/return_manager/inline_query.py +1 -2
  32. telegrinder/bot/dispatch/return_manager/message.py +1 -2
  33. telegrinder/bot/dispatch/view/__init__.py +8 -8
  34. telegrinder/bot/dispatch/view/abc.py +18 -16
  35. telegrinder/bot/dispatch/view/box.py +75 -64
  36. telegrinder/bot/dispatch/view/callback_query.py +1 -2
  37. telegrinder/bot/dispatch/view/chat_join_request.py +1 -2
  38. telegrinder/bot/dispatch/view/chat_member.py +16 -2
  39. telegrinder/bot/dispatch/view/inline_query.py +1 -2
  40. telegrinder/bot/dispatch/view/message.py +12 -5
  41. telegrinder/bot/dispatch/view/raw.py +9 -8
  42. telegrinder/bot/dispatch/waiter_machine/__init__.py +3 -3
  43. telegrinder/bot/dispatch/waiter_machine/machine.py +12 -8
  44. telegrinder/bot/dispatch/waiter_machine/middleware.py +1 -1
  45. telegrinder/bot/dispatch/waiter_machine/short_state.py +4 -3
  46. telegrinder/bot/polling/abc.py +1 -1
  47. telegrinder/bot/polling/polling.py +6 -6
  48. telegrinder/bot/rules/__init__.py +20 -20
  49. telegrinder/bot/rules/abc.py +57 -43
  50. telegrinder/bot/rules/adapter/__init__.py +5 -5
  51. telegrinder/bot/rules/adapter/abc.py +6 -3
  52. telegrinder/bot/rules/adapter/errors.py +2 -1
  53. telegrinder/bot/rules/adapter/event.py +28 -13
  54. telegrinder/bot/rules/adapter/node.py +28 -22
  55. telegrinder/bot/rules/adapter/raw_update.py +13 -5
  56. telegrinder/bot/rules/callback_data.py +4 -4
  57. telegrinder/bot/rules/chat_join.py +4 -4
  58. telegrinder/bot/rules/command.py +5 -7
  59. telegrinder/bot/rules/func.py +2 -2
  60. telegrinder/bot/rules/fuzzy.py +1 -1
  61. telegrinder/bot/rules/inline.py +3 -3
  62. telegrinder/bot/rules/integer.py +1 -2
  63. telegrinder/bot/rules/markup.py +5 -3
  64. telegrinder/bot/rules/message_entities.py +2 -2
  65. telegrinder/bot/rules/node.py +2 -2
  66. telegrinder/bot/rules/regex.py +1 -1
  67. telegrinder/bot/rules/rule_enum.py +1 -1
  68. telegrinder/bot/rules/text.py +1 -2
  69. telegrinder/bot/rules/update.py +1 -2
  70. telegrinder/bot/scenario/abc.py +2 -2
  71. telegrinder/bot/scenario/checkbox.py +3 -4
  72. telegrinder/bot/scenario/choice.py +1 -2
  73. telegrinder/model.py +89 -45
  74. telegrinder/modules.py +3 -3
  75. telegrinder/msgspec_utils.py +85 -57
  76. telegrinder/node/__init__.py +17 -10
  77. telegrinder/node/attachment.py +19 -16
  78. telegrinder/node/base.py +46 -22
  79. telegrinder/node/callback_query.py +5 -9
  80. telegrinder/node/command.py +6 -2
  81. telegrinder/node/composer.py +102 -77
  82. telegrinder/node/container.py +3 -3
  83. telegrinder/node/event.py +68 -0
  84. telegrinder/node/me.py +3 -0
  85. telegrinder/node/message.py +6 -10
  86. telegrinder/node/polymorphic.py +15 -10
  87. telegrinder/node/rule.py +20 -6
  88. telegrinder/node/scope.py +9 -1
  89. telegrinder/node/source.py +21 -11
  90. telegrinder/node/text.py +4 -4
  91. telegrinder/node/update.py +7 -4
  92. telegrinder/py.typed +0 -0
  93. telegrinder/rules.py +59 -0
  94. telegrinder/tools/__init__.py +2 -2
  95. telegrinder/tools/buttons.py +5 -10
  96. telegrinder/tools/error_handler/abc.py +2 -2
  97. telegrinder/tools/error_handler/error.py +2 -0
  98. telegrinder/tools/error_handler/error_handler.py +6 -6
  99. telegrinder/tools/formatting/spec_html_formats.py +10 -10
  100. telegrinder/tools/global_context/__init__.py +2 -2
  101. telegrinder/tools/global_context/global_context.py +3 -3
  102. telegrinder/tools/global_context/telegrinder_ctx.py +4 -4
  103. telegrinder/tools/keyboard.py +3 -3
  104. telegrinder/tools/loop_wrapper/loop_wrapper.py +47 -13
  105. telegrinder/tools/magic.py +96 -18
  106. telegrinder/types/__init__.py +1 -0
  107. telegrinder/types/enums.py +2 -0
  108. telegrinder/types/methods.py +91 -15
  109. telegrinder/types/objects.py +49 -24
  110. telegrinder/verification_utils.py +1 -3
  111. {telegrinder-0.1.dev170.dist-info → telegrinder-0.2.0.dist-info}/METADATA +2 -2
  112. telegrinder-0.2.0.dist-info/RECORD +145 -0
  113. telegrinder/api/abc.py +0 -73
  114. telegrinder-0.1.dev170.dist-info/RECORD +0 -143
  115. {telegrinder-0.1.dev170.dist-info → telegrinder-0.2.0.dist-info}/LICENSE +0 -0
  116. {telegrinder-0.1.dev170.dist-info → telegrinder-0.2.0.dist-info}/WHEEL +0 -0
@@ -1,101 +1,108 @@
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 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.node.base import ComposeError, Node
7
- from telegrinder.node.scope import NodeScope
8
- from telegrinder.tools.magic import get_annotations, magic_bundle
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 = "node_ctx"
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
- update: UpdateCute,
16
- ctx: Context,
26
+ linked: dict[type, typing.Any],
17
27
  ) -> "NodeSession":
18
28
  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
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(**context.values()))
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(**context.values()) # type: ignore
36
+ value = await typing.cast(typing.Awaitable[typing.Any], node.compose(**kwargs))
45
37
 
46
- return NodeSession(_node, value, context.sessions, generator)
38
+ return NodeSession(_node, value, {}, generator)
47
39
 
48
40
 
49
41
  async def compose_nodes(
50
- node_types: dict[str, type[Node]],
51
- update: UpdateCute,
42
+ nodes: dict[str, type[Node]],
52
43
  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]
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, "_value"):
64
- nodes[name] = getattr(node_t, "_value")
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
- nodes[name] = await compose_node(
68
- node_t,
69
- update,
70
- ctx,
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
- node_ctx[node_t] = nodes[name]
85
+ event_nodes[node_t] = local_nodes[node_t]
75
86
  elif scope is NodeScope.GLOBAL:
76
- setattr(node_t, "_value", nodes[name])
87
+ setattr(node_t, GLOBAL_VALUE_KEY, local_nodes[node_t])
77
88
 
78
- except ComposeError:
79
- await NodeCollection(nodes).close_all()
80
- return None
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
- 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
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
- nodes: dict[str, type[Node]]
151
+ func: typing.Callable[..., typing.Any]
152
+ is_blocking: bool
153
+ nodes: dict[str, type[Node]] = dataclasses.field(init=False)
140
154
 
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
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.__name__,
161
+ self.func.__qualname__,
150
162
  self.nodes,
151
163
  )
152
164
 
153
- async def compose_nodes(self, update: UpdateCute, context: Context) -> NodeCollection | None:
154
- return await compose_nodes(self.nodes, update, context)
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__ = ("NodeCollection", "NodeSession", "Composition", "compose_node", "compose_nodes")
185
+ __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 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["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_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
@@ -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,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
- result: NodeSession = node_ctx[(cls, i)]
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 result.value
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, {}) # type: ignore
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: 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:
@@ -49,7 +63,7 @@ class RuleChain(dict[str, typing.Any]):
49
63
  return cls
50
64
 
51
65
  @classmethod
52
- def get_sub_nodes(cls) -> dict:
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__ = ("NodeScope", "PER_EVENT", "PER_CALL", "per_call", "per_event", "global_node", "GLOBAL")
36
+ __all__ = (
37
+ "NodeScope",
38
+ "PER_EVENT",
39
+ "PER_CALL",
40
+ "per_call",
41
+ "per_event",
42
+ "global_node",
43
+ "GLOBAL",
44
+ )
@@ -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.types import Chat, Message, User
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
 
@@ -1,12 +1,15 @@
1
+ from telegrinder.api import API
1
2
  from telegrinder.bot.cute_types import UpdateCute
2
-
3
- from .base import ScalarNode
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: UpdateCute) -> UpdateCute:
9
- return update
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