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,6 +1,8 @@
1
+ import dataclasses
1
2
  import typing
2
3
 
3
4
  import fntypes.option
5
+ import fntypes.result
4
6
  import msgspec
5
7
  from fntypes.co import Error, Ok, Result, Variative
6
8
 
@@ -8,10 +10,12 @@ if typing.TYPE_CHECKING:
8
10
  from datetime import datetime
9
11
 
10
12
  from fntypes.option import Option
13
+ from fntypes.result import Result
11
14
  else:
12
15
  from datetime import datetime as dt
13
16
 
14
17
  Value = typing.TypeVar("Value")
18
+ Err = typing.TypeVar("Err")
15
19
 
16
20
  datetime = type("datetime", (dt,), {})
17
21
 
@@ -19,13 +23,18 @@ else:
19
23
  def __instancecheck__(cls, __instance: typing.Any) -> bool:
20
24
  return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
21
25
 
26
+ class ResultMeta(type):
27
+ def __instancecheck__(cls, __instance: typing.Any) -> bool:
28
+ return isinstance(__instance, fntypes.result.Ok | fntypes.result.Error)
29
+
22
30
  class Option(typing.Generic[Value], metaclass=OptionMeta):
23
31
  pass
24
32
 
33
+ class Result(typing.Generic[Value, Err], metaclass=ResultMeta):
34
+ pass
35
+
25
36
 
26
37
  T = typing.TypeVar("T")
27
- Type = typing.TypeVar("Type", bound=type | typing.Any)
28
- Ts = typing.TypeVarTuple("Ts")
29
38
 
30
39
  DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any], typing.Any]
31
40
  EncHook: typing.TypeAlias = typing.Callable[[T], typing.Any]
@@ -41,18 +50,62 @@ def repr_type(t: type) -> str:
41
50
  return getattr(t, "__name__", repr(get_origin(t)))
42
51
 
43
52
 
44
- def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, msgspec.ValidationError]:
53
+ def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, str]:
45
54
  try:
46
55
  return Ok(decoder.convert(obj, type=t, strict=True))
47
- except msgspec.ValidationError as exc:
48
- return Error(exc)
56
+ except msgspec.ValidationError:
57
+ return Error(
58
+ "Expected object of type `{}`, got `{}`.".format(
59
+ repr_type(t),
60
+ repr_type(type(obj)),
61
+ )
62
+ )
63
+
64
+
65
+ def msgspec_to_builtins(
66
+ obj: typing.Any,
67
+ *,
68
+ str_keys: bool = False,
69
+ builtin_types: typing.Iterable[type[typing.Any]] | None = None,
70
+ order: typing.Literal["deterministic", "sorted"] | None = None,
71
+ ) -> typing.Any:
72
+ return encoder.to_builtins(**locals())
49
73
 
50
74
 
51
75
  def option_dec_hook(tp: type[Option[typing.Any]], obj: typing.Any) -> Option[typing.Any]:
52
- if obj is None:
53
- return Nothing
76
+ orig_type = get_origin(tp)
54
77
  (value_type,) = typing.get_args(tp) or (typing.Any,)
55
- return msgspec_convert({"value": obj}, fntypes.option.Some[value_type]).unwrap()
78
+
79
+ if obj is None and orig_type in (fntypes.option.Nothing, Option):
80
+ return fntypes.option.Nothing()
81
+ return fntypes.option.Some(msgspec_convert(obj, value_type).unwrap())
82
+
83
+
84
+ def result_dec_hook(
85
+ tp: type[Result[typing.Any, typing.Any]], obj: typing.Any
86
+ ) -> Result[typing.Any, typing.Any]:
87
+ if not isinstance(obj, dict):
88
+ raise TypeError(f"Cannot parse to Result object of type `{repr_type(type(obj))}`.")
89
+
90
+ orig_type = get_origin(tp)
91
+ (first_type, second_type) = (
92
+ typing.get_args(tp) + (typing.Any,) if len(typing.get_args(tp)) == 1 else typing.get_args(tp)
93
+ ) or (typing.Any, typing.Any)
94
+
95
+ if orig_type is Ok and "ok" in obj:
96
+ return Ok(msgspec_convert(obj["ok"], first_type).unwrap())
97
+
98
+ if orig_type is Error and "error" in obj:
99
+ return Error(msgspec_convert(obj["error"], first_type).unwrap())
100
+
101
+ if orig_type is Result:
102
+ match obj:
103
+ case {"ok": ok}:
104
+ return Ok(msgspec_convert(ok, first_type).unwrap())
105
+ case {"error": error}:
106
+ return Error(msgspec_convert(error, second_type).unwrap())
107
+
108
+ raise msgspec.ValidationError(f"Cannot parse object `{obj!r}` to Result.")
56
109
 
57
110
 
58
111
  def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
@@ -93,6 +146,11 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
93
146
  )
94
147
 
95
148
 
149
+ @typing.runtime_checkable
150
+ class DataclassInstance(typing.Protocol):
151
+ __dataclass_fields__: typing.ClassVar[dict[str, dataclasses.Field[typing.Any]]]
152
+
153
+
96
154
  class Decoder:
97
155
  """Class `Decoder` for `msgspec` module with decode hook
98
156
  for objects with the specified type.
@@ -111,7 +169,6 @@ class Decoder:
111
169
  decoder.dec_hooks[dt] = lambda t, timestamp: t.fromtimestamp(timestamp)
112
170
 
113
171
  decoder.dec_hook(dt, 1713354732) #> datetime.datetime(2024, 4, 17, 14, 52, 12)
114
- decoder.dec_hook(int, "123") #> TypeError: Unknown type `int`. You can implement decode hook for this type.
115
172
 
116
173
  decoder.convert("123", type=int, strict=False) #> 123
117
174
  decoder.convert(1, type=Digit) #> <Digit.ONE: 1>
@@ -122,9 +179,14 @@ class Decoder:
122
179
 
123
180
  def __init__(self) -> None:
124
181
  self.dec_hooks: dict[typing.Any, DecHook[typing.Any]] = {
182
+ Result: result_dec_hook,
125
183
  Option: option_dec_hook,
126
184
  Variative: variative_dec_hook,
127
185
  datetime: lambda t, obj: t.fromtimestamp(obj),
186
+ fntypes.result.Error: result_dec_hook,
187
+ fntypes.result.Ok: result_dec_hook,
188
+ fntypes.option.Some: option_dec_hook,
189
+ fntypes.option.Nothing: option_dec_hook,
128
190
  }
129
191
 
130
192
  def __repr__(self) -> str:
@@ -143,7 +205,7 @@ class Decoder:
143
205
  origin_type = t if isinstance((t := get_origin(tp)), type) else type(t)
144
206
  if origin_type not in self.dec_hooks:
145
207
  raise TypeError(
146
- f"Unknown type `{repr_type(origin_type)}`. " "You can implement decode hook for this type."
208
+ f"Unknown type `{repr_type(origin_type)}`. You can implement decode hook for this type."
147
209
  )
148
210
  return self.dec_hooks[origin_type](tp, obj)
149
211
 
@@ -201,8 +263,6 @@ class Encoder:
201
263
  encoder.enc_hooks[dt] = lambda d: int(d.timestamp())
202
264
 
203
265
  encoder.enc_hook(dt.now()) #> 1713354732
204
- encoder.enc_hook(123) #> NotImplementedError: Not implemented encode hook for object of type `int`.
205
-
206
266
  encoder.encode({'digit': Digit.ONE}) #> '{"digit":1}'
207
267
  ```
208
268
  """
@@ -211,6 +271,10 @@ class Encoder:
211
271
  self.enc_hooks: dict[typing.Any, EncHook[typing.Any]] = {
212
272
  fntypes.option.Some: lambda opt: opt.value,
213
273
  fntypes.option.Nothing: lambda _: None,
274
+ fntypes.result.Ok: lambda ok: {"ok": ok.value},
275
+ fntypes.result.Error: lambda err: {
276
+ "error": (str(err.error) if isinstance(err.error, BaseException) else err.error)
277
+ },
214
278
  Variative: lambda variative: variative.v,
215
279
  datetime: lambda date: int(date.timestamp()),
216
280
  }
@@ -232,7 +296,7 @@ class Encoder:
232
296
  origin_type = get_origin(obj.__class__)
233
297
  if origin_type not in self.enc_hooks:
234
298
  raise NotImplementedError(
235
- "Not implemented encode hook for " f"object of type `{repr_type(origin_type)}`."
299
+ f"Not implemented encode hook for object of type `{repr_type(origin_type)}`."
236
300
  )
237
301
  return self.enc_hooks[origin_type](obj)
238
302
 
@@ -249,6 +313,22 @@ class Encoder:
249
313
  buf = msgspec.json.encode(obj, enc_hook=self.enc_hook)
250
314
  return buf.decode() if as_str else buf
251
315
 
316
+ def to_builtins(
317
+ self,
318
+ obj: typing.Any,
319
+ *,
320
+ str_keys: bool = False,
321
+ builtin_types: typing.Iterable[type[typing.Any]] | None = None,
322
+ order: typing.Literal["deterministic", "sorted"] | None = None,
323
+ ) -> typing.Any:
324
+ return msgspec.to_builtins(
325
+ obj,
326
+ str_keys=str_keys,
327
+ builtin_types=builtin_types,
328
+ enc_hook=self.enc_hook,
329
+ order=order,
330
+ )
331
+
252
332
 
253
333
  decoder: typing.Final[Decoder] = Decoder()
254
334
  encoder: typing.Final[Encoder] = Encoder()
@@ -264,6 +344,7 @@ __all__ = (
264
344
  "encoder",
265
345
  "get_origin",
266
346
  "msgspec_convert",
347
+ "msgspec_to_builtins",
267
348
  "option_dec_hook",
268
349
  "repr_type",
269
350
  "variative_dec_hook",
@@ -1,10 +1,13 @@
1
1
  from .attachment import Attachment, Audio, Photo, Video
2
- from .base import ComposeError, DataNode, Node, ScalarNode, is_node
2
+ from .base import BaseNode, ComposeError, DataNode, Node, ScalarNode, is_node, node_impl
3
+ from .callback_query import CallbackQueryNode
3
4
  from .command import CommandInfo
4
5
  from .composer import Composition, NodeCollection, NodeSession, compose_node, compose_nodes
5
6
  from .container import ContainerNode
7
+ from .event import EventNode
6
8
  from .me import Me
7
9
  from .message import MessageNode
10
+ from .polymorphic import Polymorphic, impl
8
11
  from .rule import RuleChain
9
12
  from .scope import GLOBAL, PER_CALL, PER_EVENT, NodeScope, global_node, per_call, per_event
10
13
  from .source import ChatSource, Source, UserSource
@@ -15,35 +18,41 @@ from .update import UpdateNode
15
18
  __all__ = (
16
19
  "Attachment",
17
20
  "Audio",
21
+ "BaseNode",
22
+ "CallbackQueryNode",
18
23
  "ChatSource",
24
+ "CommandInfo",
19
25
  "ComposeError",
26
+ "Composition",
20
27
  "ContainerNode",
21
28
  "DataNode",
29
+ "EventNode",
30
+ "GLOBAL",
31
+ "Me",
22
32
  "MessageNode",
23
33
  "Node",
24
34
  "NodeCollection",
35
+ "NodeScope",
25
36
  "NodeSession",
37
+ "PER_CALL",
38
+ "PER_EVENT",
26
39
  "Photo",
40
+ "Polymorphic",
27
41
  "RuleChain",
28
42
  "ScalarNode",
29
43
  "Source",
30
44
  "Text",
31
45
  "TextInteger",
32
- "UserSource",
33
46
  "UpdateNode",
47
+ "UserSource",
34
48
  "Video",
35
49
  "compose_node",
50
+ "compose_nodes",
36
51
  "generate_node",
37
- "Composition",
52
+ "global_node",
53
+ "impl",
38
54
  "is_node",
39
- "compose_nodes",
40
- "NodeScope",
41
- "PER_CALL",
42
- "PER_EVENT",
55
+ "node_impl",
43
56
  "per_call",
44
57
  "per_event",
45
- "CommandInfo",
46
- "GLOBAL",
47
- "global_node",
48
- "Me",
49
58
  )
@@ -1,30 +1,33 @@
1
1
  import dataclasses
2
2
  import typing
3
3
 
4
- from fntypes import Option, Some
4
+ from fntypes.co import Option, Some
5
5
  from fntypes.option import Nothing
6
6
 
7
7
  import telegrinder.types
8
+ from telegrinder.node.base import ComposeError, DataNode, ScalarNode
9
+ from telegrinder.node.message import MessageNode
8
10
 
9
- from .base import ComposeError, DataNode, ScalarNode
10
- from .message import MessageNode
11
11
 
12
-
13
- @dataclasses.dataclass
12
+ @dataclasses.dataclass(slots=True)
14
13
  class Attachment(DataNode):
15
14
  attachment_type: typing.Literal["audio", "document", "photo", "poll", "video"]
16
15
  audio: Option[telegrinder.types.Audio] = dataclasses.field(
17
- default_factory=lambda: Nothing(), kw_only=True
16
+ default_factory=lambda: Nothing(),
17
+ kw_only=True,
18
18
  )
19
19
  document: Option[telegrinder.types.Document] = dataclasses.field(
20
- default_factory=lambda: Nothing(), kw_only=True
20
+ default_factory=lambda: Nothing(),
21
+ kw_only=True,
21
22
  )
22
23
  photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
23
- default_factory=lambda: Nothing(), kw_only=True
24
+ default_factory=lambda: Nothing(),
25
+ kw_only=True,
24
26
  )
25
27
  poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing(), kw_only=True)
26
28
  video: Option[telegrinder.types.Video] = dataclasses.field(
27
- default_factory=lambda: Nothing(), kw_only=True
29
+ default_factory=lambda: Nothing(),
30
+ kw_only=True,
28
31
  )
29
32
 
30
33
  @classmethod
@@ -33,17 +36,17 @@ class Attachment(DataNode):
33
36
  match getattr(message, attachment_type, Nothing()):
34
37
  case Some(attachment):
35
38
  return cls(attachment_type, **{attachment_type: Some(attachment)})
36
- return cls.compose_error("No attachment found in message")
39
+ return cls.compose_error("No attachment found in message.")
37
40
 
38
41
 
39
- @dataclasses.dataclass
42
+ @dataclasses.dataclass(slots=True)
40
43
  class Photo(DataNode):
41
44
  sizes: list[telegrinder.types.PhotoSize]
42
45
 
43
46
  @classmethod
44
47
  async def compose(cls, attachment: Attachment) -> typing.Self:
45
48
  if not attachment.photo:
46
- raise ComposeError("Attachment is not an photo")
49
+ raise ComposeError("Attachment is not a photo.")
47
50
  return cls(attachment.photo.unwrap())
48
51
 
49
52
 
@@ -51,7 +54,7 @@ class Video(ScalarNode, telegrinder.types.Video):
51
54
  @classmethod
52
55
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
53
56
  if not attachment.video:
54
- raise ComposeError("Attachment is not an video")
57
+ raise ComposeError("Attachment is not a video.")
55
58
  return attachment.video.unwrap()
56
59
 
57
60
 
@@ -59,7 +62,7 @@ class Audio(ScalarNode, telegrinder.types.Audio):
59
62
  @classmethod
60
63
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
61
64
  if not attachment.audio:
62
- raise ComposeError("Attachment is not an audio")
65
+ raise ComposeError("Attachment is not an audio.")
63
66
  return attachment.audio.unwrap()
64
67
 
65
68
 
@@ -67,7 +70,7 @@ class Document(ScalarNode, telegrinder.types.Document):
67
70
  @classmethod
68
71
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
69
72
  if not attachment.document:
70
- raise ComposeError("Attachment is not an document")
73
+ raise ComposeError("Attachment is not a document.")
71
74
  return attachment.document.unwrap()
72
75
 
73
76
 
@@ -75,7 +78,7 @@ class Poll(ScalarNode, telegrinder.types.Poll):
75
78
  @classmethod
76
79
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
77
80
  if not attachment.poll:
78
- raise ComposeError("Attachment is not an poll")
81
+ raise ComposeError("Attachment is not a poll.")
79
82
  return attachment.poll.unwrap()
80
83
 
81
84
 
telegrinder/node/base.py CHANGED
@@ -1,19 +1,70 @@
1
1
  import abc
2
2
  import inspect
3
3
  import typing
4
+ from types import AsyncGeneratorType
5
+
6
+ from telegrinder.api.api import API
7
+ from telegrinder.bot.cute_types.update import UpdateCute
8
+ from telegrinder.bot.dispatch.context import Context
9
+ from telegrinder.node.scope import NodeScope
10
+ from telegrinder.tools.magic import (
11
+ NODE_IMPL_MARK,
12
+ cache_magic_value,
13
+ get_annotations,
14
+ get_impls_by_key,
15
+ magic_bundle,
16
+ node_impl,
17
+ )
4
18
 
5
- from telegrinder.tools.magic import get_annotations
19
+ ComposeResult: typing.TypeAlias = typing.Awaitable[typing.Any] | typing.AsyncGenerator[typing.Any, None]
6
20
 
7
- from .scope import NodeScope
8
21
 
9
- ComposeResult: typing.TypeAlias = (
10
- typing.Coroutine[typing.Any, typing.Any, typing.Any]
11
- | typing.AsyncGenerator[typing.Any, None]
12
- | typing.Any
13
- )
22
+ def is_node(maybe_node: type[typing.Any]) -> typing.TypeGuard[type["Node"]]:
23
+ maybe_node = typing.get_origin(maybe_node) or maybe_node
24
+ return (
25
+ isinstance(maybe_node, type)
26
+ and issubclass(maybe_node, Node)
27
+ or isinstance(maybe_node, Node)
28
+ or hasattr(maybe_node, "as_node")
29
+ )
30
+
31
+
32
+ @cache_magic_value("__compose_annotations__")
33
+ def get_compose_annotations(function: typing.Callable[..., typing.Any]) -> dict[str, typing.Any]:
34
+ return {k: v for k, v in get_annotations(function).items() if not is_node(v)}
35
+
14
36
 
37
+ @cache_magic_value("__nodes__")
38
+ def get_nodes(function: typing.Callable[..., typing.Any]) -> dict[str, type["Node"]]:
39
+ return {k: v for k, v in get_annotations(function).items() if is_node(v)}
15
40
 
16
- class ComposeError(BaseException): ...
41
+
42
+ @cache_magic_value("__is_generator__")
43
+ def is_generator(function: typing.Callable[..., typing.Any]) -> typing.TypeGuard[AsyncGeneratorType[typing.Any, None]]:
44
+ return inspect.isasyncgenfunction(function)
45
+
46
+
47
+ def get_node_impls(node_cls: type["Node"]) -> dict[str, typing.Any]:
48
+ if not hasattr(node_cls, "__node_impls__"):
49
+ impls = get_impls_by_key(node_cls, NODE_IMPL_MARK)
50
+ if issubclass(node_cls, BaseNode):
51
+ impls |= get_impls_by_key(BaseNode, NODE_IMPL_MARK)
52
+ setattr(node_cls, "__node_impls__", impls)
53
+ return getattr(node_cls, "__node_impls__")
54
+
55
+
56
+ def get_node_impl(
57
+ node: type[typing.Any],
58
+ node_impls: dict[str, typing.Callable[..., typing.Any]],
59
+ ) -> typing.Callable[..., typing.Any] | None:
60
+ for n_impl in node_impls.values():
61
+ if "return" in n_impl.__annotations__ and node is n_impl.__annotations__["return"]:
62
+ return n_impl
63
+ return None
64
+
65
+
66
+ class ComposeError(BaseException):
67
+ pass
17
68
 
18
69
 
19
70
  class Node(abc.ABC):
@@ -25,13 +76,46 @@ class Node(abc.ABC):
25
76
  def compose(cls, *args, **kwargs) -> ComposeResult:
26
77
  pass
27
78
 
79
+ @classmethod
80
+ async def compose_annotation(
81
+ cls,
82
+ annotation: typing.Any,
83
+ update: UpdateCute,
84
+ ctx: Context,
85
+ ) -> typing.Any:
86
+ orig_annotation: type[typing.Any] = typing.get_origin(annotation) or annotation
87
+ n_impl = get_node_impl(orig_annotation, cls.get_node_impls())
88
+ if n_impl is None:
89
+ raise ComposeError(f"Node implementation for {orig_annotation!r} not found.")
90
+
91
+ result = n_impl(
92
+ cls,
93
+ **magic_bundle(
94
+ n_impl,
95
+ {"update": update, "context": ctx},
96
+ start_idx=0,
97
+ bundle_ctx=False,
98
+ ),
99
+ )
100
+ if inspect.isawaitable(result):
101
+ return await result
102
+ return result
103
+
28
104
  @classmethod
29
105
  def compose_error(cls, error: str | None = None) -> typing.NoReturn:
30
106
  raise ComposeError(error)
31
107
 
32
108
  @classmethod
33
- def get_sub_nodes(cls) -> dict[str, type[typing.Self]]:
34
- return get_annotations(cls.compose)
109
+ def get_sub_nodes(cls) -> dict[str, type["Node"]]:
110
+ return get_nodes(cls.compose)
111
+
112
+ @classmethod
113
+ def get_compose_annotations(cls) -> dict[str, typing.Any]:
114
+ return get_compose_annotations(cls.compose)
115
+
116
+ @classmethod
117
+ def get_node_impls(cls) -> dict[str, typing.Callable[..., typing.Any]]:
118
+ return get_node_impls(cls)
35
119
 
36
120
  @classmethod
37
121
  def as_node(cls) -> type[typing.Self]:
@@ -39,10 +123,29 @@ class Node(abc.ABC):
39
123
 
40
124
  @classmethod
41
125
  def is_generator(cls) -> bool:
42
- return inspect.isasyncgenfunction(cls.compose)
126
+ return is_generator(cls.compose)
127
+
128
+
129
+ class BaseNode(Node, abc.ABC):
130
+ @classmethod
131
+ @abc.abstractmethod
132
+ def compose(cls, *args, **kwargs) -> ComposeResult:
133
+ pass
134
+
135
+ @node_impl
136
+ def compose_api(cls, update: UpdateCute) -> API:
137
+ return update.ctx_api
138
+
139
+ @node_impl
140
+ def compose_context(cls, context: Context) -> Context:
141
+ return context
43
142
 
143
+ @node_impl
144
+ def compose_update(cls, update: UpdateCute) -> UpdateCute:
145
+ return update
44
146
 
45
- class DataNode(Node, abc.ABC):
147
+
148
+ class DataNode(BaseNode, abc.ABC):
46
149
  node = "data"
47
150
 
48
151
  @typing.dataclass_transform()
@@ -52,7 +155,7 @@ class DataNode(Node, abc.ABC):
52
155
  pass
53
156
 
54
157
 
55
- class ScalarNodeProto(Node, abc.ABC):
158
+ class ScalarNodeProto(BaseNode, abc.ABC):
56
159
  @classmethod
57
160
  @abc.abstractmethod
58
161
  async def compose(cls, *args, **kwargs) -> ComposeResult:
@@ -68,7 +171,6 @@ if typing.TYPE_CHECKING:
68
171
  pass
69
172
 
70
173
  else:
71
-
72
174
  def create_node(cls, bases, dct):
73
175
  dct.update(cls.__dict__)
74
176
  return type(cls.__name__, bases, dct)
@@ -87,21 +189,15 @@ else:
87
189
  pass
88
190
 
89
191
 
90
- def is_node(maybe_node: type[typing.Any]) -> typing.TypeGuard[type[Node]]:
91
- maybe_node = typing.get_origin(maybe_node) or maybe_node
92
- return (
93
- isinstance(maybe_node, type)
94
- and issubclass(maybe_node, Node)
95
- or isinstance(maybe_node, Node)
96
- or hasattr(maybe_node, "as_node")
97
- )
98
-
99
-
100
192
  __all__ = (
193
+ "BaseNode",
101
194
  "ComposeError",
102
195
  "DataNode",
103
196
  "Node",
104
197
  "SCALAR_NODE",
105
198
  "ScalarNode",
199
+ "ScalarNodeProto",
200
+ "get_compose_annotations",
201
+ "get_nodes",
106
202
  "is_node",
107
203
  )
@@ -1,18 +1,14 @@
1
- from telegrinder.bot.cute_types import CallbackQueryCute
2
-
3
- from .base import ComposeError, ScalarNode
4
- from .update import UpdateNode
1
+ from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
2
+ from telegrinder.node.base import ComposeError, ScalarNode
3
+ from telegrinder.node.update import UpdateNode
5
4
 
6
5
 
7
6
  class CallbackQueryNode(ScalarNode, CallbackQueryCute):
8
7
  @classmethod
9
8
  async def compose(cls, update: UpdateNode) -> CallbackQueryCute:
10
9
  if not update.callback_query:
11
- raise ComposeError
12
- return CallbackQueryCute(
13
- **update.callback_query.unwrap().to_dict(),
14
- api=update.api,
15
- )
10
+ raise ComposeError("Update is not a callback_query.")
11
+ return update.callback_query.unwrap()
16
12
 
17
13
 
18
14
  __all__ = ("CallbackQueryNode",)
@@ -1,3 +1,4 @@
1
+ import typing
1
2
  from dataclasses import dataclass, field
2
3
 
3
4
  from fntypes import Nothing, Option, Some
@@ -16,14 +17,17 @@ def cut_mention(text: str) -> tuple[str, Option[str]]:
16
17
  return left, Some(right) if right else Nothing()
17
18
 
18
19
 
19
- @dataclass
20
+ @dataclass(slots=True)
20
21
  class CommandInfo(DataNode):
21
22
  name: str
22
23
  arguments: str
23
24
  mention: Option[str] = field(default_factory=Nothing)
24
25
 
25
26
  @classmethod
26
- async def compose(cls, text: Text):
27
+ async def compose(cls, text: Text) -> typing.Self:
27
28
  name, arguments = single_split(text, separator=" ")
28
29
  name, mention = cut_mention(name)
29
30
  return cls(name, arguments, mention)
31
+
32
+
33
+ __all__ = ("CommandInfo", "cut_mention", "single_split")