telegrinder 0.3.4.post1__py3-none-any.whl → 0.4.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 (169) hide show
  1. telegrinder/__init__.py +30 -31
  2. telegrinder/api/__init__.py +2 -1
  3. telegrinder/api/api.py +28 -20
  4. telegrinder/api/error.py +8 -4
  5. telegrinder/api/response.py +2 -2
  6. telegrinder/api/token.py +2 -2
  7. telegrinder/bot/__init__.py +6 -0
  8. telegrinder/bot/bot.py +38 -31
  9. telegrinder/bot/cute_types/__init__.py +2 -0
  10. telegrinder/bot/cute_types/base.py +54 -128
  11. telegrinder/bot/cute_types/callback_query.py +76 -61
  12. telegrinder/bot/cute_types/chat_join_request.py +4 -3
  13. telegrinder/bot/cute_types/chat_member_updated.py +28 -31
  14. telegrinder/bot/cute_types/inline_query.py +5 -4
  15. telegrinder/bot/cute_types/message.py +555 -602
  16. telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
  17. telegrinder/bot/cute_types/update.py +20 -12
  18. telegrinder/bot/cute_types/utils.py +3 -36
  19. telegrinder/bot/dispatch/__init__.py +4 -0
  20. telegrinder/bot/dispatch/abc.py +8 -9
  21. telegrinder/bot/dispatch/context.py +5 -7
  22. telegrinder/bot/dispatch/dispatch.py +85 -33
  23. telegrinder/bot/dispatch/handler/abc.py +5 -6
  24. telegrinder/bot/dispatch/handler/audio_reply.py +2 -2
  25. telegrinder/bot/dispatch/handler/base.py +3 -3
  26. telegrinder/bot/dispatch/handler/document_reply.py +2 -2
  27. telegrinder/bot/dispatch/handler/func.py +36 -42
  28. telegrinder/bot/dispatch/handler/media_group_reply.py +5 -4
  29. telegrinder/bot/dispatch/handler/message_reply.py +2 -2
  30. telegrinder/bot/dispatch/handler/photo_reply.py +2 -2
  31. telegrinder/bot/dispatch/handler/sticker_reply.py +2 -2
  32. telegrinder/bot/dispatch/handler/video_reply.py +2 -2
  33. telegrinder/bot/dispatch/middleware/abc.py +83 -8
  34. telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
  35. telegrinder/bot/dispatch/process.py +44 -50
  36. telegrinder/bot/dispatch/return_manager/__init__.py +2 -0
  37. telegrinder/bot/dispatch/return_manager/abc.py +6 -10
  38. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +20 -0
  39. telegrinder/bot/dispatch/view/__init__.py +2 -0
  40. telegrinder/bot/dispatch/view/abc.py +10 -6
  41. telegrinder/bot/dispatch/view/base.py +81 -50
  42. telegrinder/bot/dispatch/view/box.py +20 -9
  43. telegrinder/bot/dispatch/view/callback_query.py +3 -4
  44. telegrinder/bot/dispatch/view/chat_join_request.py +2 -7
  45. telegrinder/bot/dispatch/view/chat_member.py +3 -5
  46. telegrinder/bot/dispatch/view/inline_query.py +3 -4
  47. telegrinder/bot/dispatch/view/message.py +3 -4
  48. telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
  49. telegrinder/bot/dispatch/view/raw.py +42 -40
  50. telegrinder/bot/dispatch/waiter_machine/actions.py +5 -4
  51. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +0 -0
  52. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +0 -0
  53. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +9 -7
  54. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
  55. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +3 -2
  56. telegrinder/bot/dispatch/waiter_machine/machine.py +113 -34
  57. telegrinder/bot/dispatch/waiter_machine/middleware.py +15 -10
  58. telegrinder/bot/dispatch/waiter_machine/short_state.py +7 -18
  59. telegrinder/bot/polling/polling.py +62 -54
  60. telegrinder/bot/rules/__init__.py +24 -1
  61. telegrinder/bot/rules/abc.py +17 -10
  62. telegrinder/bot/rules/callback_data.py +20 -61
  63. telegrinder/bot/rules/chat_join.py +6 -4
  64. telegrinder/bot/rules/command.py +4 -4
  65. telegrinder/bot/rules/enum_text.py +1 -4
  66. telegrinder/bot/rules/func.py +5 -3
  67. telegrinder/bot/rules/fuzzy.py +1 -1
  68. telegrinder/bot/rules/id.py +24 -0
  69. telegrinder/bot/rules/inline.py +6 -4
  70. telegrinder/bot/rules/integer.py +2 -1
  71. telegrinder/bot/rules/logic.py +18 -0
  72. telegrinder/bot/rules/markup.py +5 -6
  73. telegrinder/bot/rules/message.py +2 -4
  74. telegrinder/bot/rules/message_entities.py +1 -3
  75. telegrinder/bot/rules/node.py +15 -9
  76. telegrinder/bot/rules/payload.py +81 -0
  77. telegrinder/bot/rules/payment_invoice.py +29 -0
  78. telegrinder/bot/rules/regex.py +5 -6
  79. telegrinder/bot/rules/state.py +1 -3
  80. telegrinder/bot/rules/text.py +10 -5
  81. telegrinder/bot/rules/update.py +0 -0
  82. telegrinder/bot/scenario/abc.py +2 -4
  83. telegrinder/bot/scenario/checkbox.py +12 -14
  84. telegrinder/bot/scenario/choice.py +6 -9
  85. telegrinder/client/__init__.py +9 -1
  86. telegrinder/client/abc.py +35 -10
  87. telegrinder/client/aiohttp.py +28 -24
  88. telegrinder/client/form_data.py +31 -0
  89. telegrinder/client/sonic.py +212 -0
  90. telegrinder/model.py +38 -145
  91. telegrinder/modules.py +3 -1
  92. telegrinder/msgspec_utils.py +136 -68
  93. telegrinder/node/__init__.py +74 -13
  94. telegrinder/node/attachment.py +92 -16
  95. telegrinder/node/base.py +196 -68
  96. telegrinder/node/callback_query.py +17 -16
  97. telegrinder/node/command.py +3 -2
  98. telegrinder/node/composer.py +40 -75
  99. telegrinder/node/container.py +13 -7
  100. telegrinder/node/either.py +82 -0
  101. telegrinder/node/event.py +20 -31
  102. telegrinder/node/file.py +51 -0
  103. telegrinder/node/me.py +4 -5
  104. telegrinder/node/payload.py +78 -0
  105. telegrinder/node/polymorphic.py +27 -8
  106. telegrinder/node/rule.py +2 -6
  107. telegrinder/node/scope.py +4 -6
  108. telegrinder/node/source.py +37 -21
  109. telegrinder/node/text.py +20 -8
  110. telegrinder/node/tools/generator.py +7 -11
  111. telegrinder/py.typed +0 -0
  112. telegrinder/rules.py +0 -61
  113. telegrinder/tools/__init__.py +97 -38
  114. telegrinder/tools/adapter/__init__.py +19 -0
  115. telegrinder/tools/adapter/abc.py +49 -0
  116. telegrinder/tools/adapter/dataclass.py +56 -0
  117. telegrinder/{bot/rules → tools}/adapter/event.py +8 -10
  118. telegrinder/{bot/rules → tools}/adapter/node.py +8 -10
  119. telegrinder/{bot/rules → tools}/adapter/raw_event.py +2 -2
  120. telegrinder/{bot/rules → tools}/adapter/raw_update.py +2 -2
  121. telegrinder/tools/buttons.py +52 -26
  122. telegrinder/tools/callback_data_serilization/__init__.py +5 -0
  123. telegrinder/tools/callback_data_serilization/abc.py +51 -0
  124. telegrinder/tools/callback_data_serilization/json_ser.py +60 -0
  125. telegrinder/tools/callback_data_serilization/msgpack_ser.py +172 -0
  126. telegrinder/tools/error_handler/abc.py +4 -7
  127. telegrinder/tools/error_handler/error.py +0 -0
  128. telegrinder/tools/error_handler/error_handler.py +34 -48
  129. telegrinder/tools/formatting/__init__.py +57 -37
  130. telegrinder/tools/formatting/deep_links.py +541 -0
  131. telegrinder/tools/formatting/{html.py → html_formatter.py} +51 -79
  132. telegrinder/tools/formatting/spec_html_formats.py +14 -60
  133. telegrinder/tools/functional.py +1 -5
  134. telegrinder/tools/global_context/global_context.py +26 -51
  135. telegrinder/tools/global_context/telegrinder_ctx.py +3 -3
  136. telegrinder/tools/i18n/abc.py +0 -0
  137. telegrinder/tools/i18n/middleware/abc.py +3 -6
  138. telegrinder/tools/input_file_directory.py +30 -0
  139. telegrinder/tools/keyboard.py +9 -9
  140. telegrinder/tools/lifespan.py +105 -0
  141. telegrinder/tools/limited_dict.py +5 -10
  142. telegrinder/tools/loop_wrapper/abc.py +7 -2
  143. telegrinder/tools/loop_wrapper/loop_wrapper.py +40 -95
  144. telegrinder/tools/magic.py +184 -34
  145. telegrinder/tools/state_storage/__init__.py +0 -0
  146. telegrinder/tools/state_storage/abc.py +5 -9
  147. telegrinder/tools/state_storage/memory.py +1 -1
  148. telegrinder/tools/strings.py +13 -0
  149. telegrinder/types/__init__.py +8 -0
  150. telegrinder/types/enums.py +31 -21
  151. telegrinder/types/input_file.py +51 -0
  152. telegrinder/types/methods.py +531 -109
  153. telegrinder/types/objects.py +934 -826
  154. telegrinder/verification_utils.py +0 -2
  155. {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/LICENSE +2 -2
  156. telegrinder-0.4.0.dist-info/METADATA +144 -0
  157. telegrinder-0.4.0.dist-info/RECORD +182 -0
  158. {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/WHEEL +1 -1
  159. telegrinder/bot/rules/adapter/__init__.py +0 -17
  160. telegrinder/bot/rules/adapter/abc.py +0 -31
  161. telegrinder/node/message.py +0 -14
  162. telegrinder/node/update.py +0 -15
  163. telegrinder/tools/formatting/links.py +0 -38
  164. telegrinder/tools/kb_set/__init__.py +0 -4
  165. telegrinder/tools/kb_set/base.py +0 -15
  166. telegrinder/tools/kb_set/yaml.py +0 -63
  167. telegrinder-0.3.4.post1.dist-info/METADATA +0 -110
  168. telegrinder-0.3.4.post1.dist-info/RECORD +0 -165
  169. /telegrinder/{bot/rules → tools}/adapter/errors.py +0 -0
@@ -1,42 +1,74 @@
1
1
  import dataclasses
2
2
  import typing
3
3
 
4
- from fntypes.co import Option, Some
5
- from fntypes.option import Nothing
4
+ from fntypes.option import Nothing, Option, Some
6
5
 
7
6
  import telegrinder.types
8
- from telegrinder.node.base import ComposeError, DataNode, ScalarNode
9
- from telegrinder.node.message import MessageNode
7
+ from telegrinder.bot.cute_types.message import MessageCute
8
+ from telegrinder.node.base import ComposeError, DataNode, scalar_node
9
+
10
+ type AttachmentType = typing.Literal[
11
+ "audio",
12
+ "animation",
13
+ "document",
14
+ "photo",
15
+ "poll",
16
+ "voice",
17
+ "video",
18
+ "video_note",
19
+ "successful_payment",
20
+ ]
10
21
 
11
22
 
12
23
  @dataclasses.dataclass(slots=True)
13
24
  class Attachment(DataNode):
14
- attachment_type: typing.Literal["audio", "document", "photo", "poll", "video"]
25
+ attachment_type: AttachmentType
26
+
27
+ animation: Option[telegrinder.types.Animation] = dataclasses.field(
28
+ default_factory=Nothing,
29
+ kw_only=True,
30
+ )
15
31
  audio: Option[telegrinder.types.Audio] = dataclasses.field(
16
- default_factory=lambda: Nothing(),
32
+ default_factory=Nothing,
17
33
  kw_only=True,
18
34
  )
19
35
  document: Option[telegrinder.types.Document] = dataclasses.field(
20
- default_factory=lambda: Nothing(),
36
+ default_factory=Nothing,
21
37
  kw_only=True,
22
38
  )
23
39
  photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
24
- default_factory=lambda: Nothing(),
40
+ default_factory=Nothing,
25
41
  kw_only=True,
26
42
  )
27
43
  poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing(), kw_only=True)
44
+ voice: Option[telegrinder.types.Voice] = dataclasses.field(
45
+ default_factory=Nothing,
46
+ kw_only=True,
47
+ )
28
48
  video: Option[telegrinder.types.Video] = dataclasses.field(
29
- default_factory=lambda: Nothing(),
49
+ default_factory=Nothing,
50
+ kw_only=True,
51
+ )
52
+ video_note: Option[telegrinder.types.VideoNote] = dataclasses.field(
53
+ default_factory=Nothing,
54
+ kw_only=True,
55
+ )
56
+ successful_payment: Option[telegrinder.types.SuccessfulPayment] = dataclasses.field(
57
+ default_factory=Nothing,
30
58
  kw_only=True,
31
59
  )
32
60
 
33
61
  @classmethod
34
- def compose(cls, message: MessageNode) -> "Attachment":
35
- for attachment_type in ("audio", "document", "photo", "poll", "video"):
62
+ def get_attachment_types(cls) -> tuple[AttachmentType, ...]:
63
+ return typing.get_args(AttachmentType.__value__)
64
+
65
+ @classmethod
66
+ def compose(cls, message: MessageCute) -> typing.Self:
67
+ for attachment_type in cls.get_attachment_types():
36
68
  match getattr(message, attachment_type, Nothing()):
37
69
  case Some(attachment):
38
70
  return cls(attachment_type, **{attachment_type: Some(attachment)})
39
- return cls.compose_error("No attachment found in message.")
71
+ raise ComposeError("No attachment found in message.")
40
72
 
41
73
 
42
74
  @dataclasses.dataclass(slots=True)
@@ -50,7 +82,8 @@ class Photo(DataNode):
50
82
  return cls(attachment.photo.unwrap())
51
83
 
52
84
 
53
- class Video(ScalarNode, telegrinder.types.Video):
85
+ @scalar_node
86
+ class Video:
54
87
  @classmethod
55
88
  def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
56
89
  if not attachment.video:
@@ -58,7 +91,17 @@ class Video(ScalarNode, telegrinder.types.Video):
58
91
  return attachment.video.unwrap()
59
92
 
60
93
 
61
- class Audio(ScalarNode, telegrinder.types.Audio):
94
+ @scalar_node
95
+ class VideoNote:
96
+ @classmethod
97
+ def compose(cls, attachment: Attachment) -> telegrinder.types.VideoNote:
98
+ if not attachment.video_note:
99
+ raise ComposeError("Attachment is not a video note.")
100
+ return attachment.video_note.unwrap()
101
+
102
+
103
+ @scalar_node
104
+ class Audio:
62
105
  @classmethod
63
106
  def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
64
107
  if not attachment.audio:
@@ -66,7 +109,26 @@ class Audio(ScalarNode, telegrinder.types.Audio):
66
109
  return attachment.audio.unwrap()
67
110
 
68
111
 
69
- class Document(ScalarNode, telegrinder.types.Document):
112
+ @scalar_node
113
+ class Animation:
114
+ @classmethod
115
+ def compose(cls, attachment: Attachment) -> telegrinder.types.Animation:
116
+ if not attachment.animation:
117
+ raise ComposeError("Attachment is not an animation.")
118
+ return attachment.animation.unwrap()
119
+
120
+
121
+ @scalar_node
122
+ class Voice:
123
+ @classmethod
124
+ def compose(cls, attachment: Attachment) -> telegrinder.types.Voice:
125
+ if not attachment.voice:
126
+ raise ComposeError("Attachment is not a voice.")
127
+ return attachment.voice.unwrap()
128
+
129
+
130
+ @scalar_node
131
+ class Document:
70
132
  @classmethod
71
133
  def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
72
134
  if not attachment.document:
@@ -74,7 +136,8 @@ class Document(ScalarNode, telegrinder.types.Document):
74
136
  return attachment.document.unwrap()
75
137
 
76
138
 
77
- class Poll(ScalarNode, telegrinder.types.Poll):
139
+ @scalar_node
140
+ class Poll:
78
141
  @classmethod
79
142
  def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
80
143
  if not attachment.poll:
@@ -82,11 +145,24 @@ class Poll(ScalarNode, telegrinder.types.Poll):
82
145
  return attachment.poll.unwrap()
83
146
 
84
147
 
148
+ @scalar_node
149
+ class SuccessfulPayment:
150
+ @classmethod
151
+ def compose(cls, attachment: Attachment) -> telegrinder.types.SuccessfulPayment:
152
+ if not attachment.successful_payment:
153
+ raise ComposeError("Attachment is not a successful payment.")
154
+ return attachment.successful_payment.unwrap()
155
+
156
+
85
157
  __all__ = (
158
+ "Animation",
86
159
  "Attachment",
87
160
  "Audio",
88
161
  "Document",
89
162
  "Photo",
90
163
  "Poll",
164
+ "SuccessfulPayment",
91
165
  "Video",
166
+ "VideoNote",
167
+ "Voice",
92
168
  )
telegrinder/node/base.py CHANGED
@@ -1,57 +1,164 @@
1
+ from __future__ import annotations
2
+
1
3
  import abc
2
4
  import inspect
3
- from types import AsyncGeneratorType
5
+ from collections import deque
6
+ from types import AsyncGeneratorType, CodeType, resolve_bases
4
7
 
5
8
  import typing_extensions as typing
6
9
 
7
10
  from telegrinder.node.scope import NodeScope
8
11
  from telegrinder.tools.magic import cache_magic_value, get_annotations
12
+ from telegrinder.tools.strings import to_pascal_case
13
+
14
+ if typing.TYPE_CHECKING:
15
+ from telegrinder.node.tools.generator import generate_node
16
+ else:
17
+
18
+ def generate_node(*args, **kwargs):
19
+ globalns = globals()
20
+ if "__generate_node" not in globalns:
21
+ import telegrinder.node.tools.generator
22
+
23
+ globals()["__generate_node"] = telegrinder.node.tools.generator.generate_node
24
+
25
+ return globals()["__generate_node"](*args, **kwargs)
26
+
27
+
28
+ type NodeType = Node | NodeProto[typing.Any]
29
+ type IsNode = NodeType | type[NodeType]
9
30
 
10
31
  T = typing.TypeVar("T", default=typing.Any)
11
32
 
12
33
  ComposeResult: typing.TypeAlias = T | typing.Awaitable[T] | typing.AsyncGenerator[T, None]
13
34
 
35
+ UNWRAPPED_NODE_KEY = "__unwrapped_node__"
36
+
37
+
38
+ @typing.overload
39
+ def is_node(maybe_node: type[typing.Any], /) -> typing.TypeIs[type[NodeType]]: ...
40
+
41
+
42
+ @typing.overload
43
+ def is_node(maybe_node: typing.Any, /) -> typing.TypeIs[NodeType]: ...
44
+
45
+
46
+ def is_node(maybe_node: typing.Any, /) -> bool:
47
+ if isinstance(maybe_node, typing.TypeAliasType):
48
+ maybe_node = maybe_node.__value__
49
+ if not isinstance(maybe_node, type):
50
+ maybe_node = typing.get_origin(maybe_node) or maybe_node
14
51
 
15
- def is_node(maybe_node: typing.Any) -> typing.TypeGuard[type["Node"]]:
16
- maybe_node = maybe_node if isinstance(maybe_node, type) else typing.get_origin(maybe_node)
17
52
  return (
18
- isinstance(maybe_node, type)
19
- and issubclass(maybe_node, Node)
20
- or isinstance(maybe_node, Node)
21
- or hasattr(maybe_node, "as_node")
53
+ hasattr(maybe_node, "as_node")
54
+ or isinstance(maybe_node, type)
55
+ and issubclass(maybe_node, (Node, NodeProto))
56
+ or not isinstance(maybe_node, type)
57
+ and isinstance(maybe_node, (Node, NodeProto))
22
58
  )
23
59
 
24
60
 
61
+ @typing.overload
62
+ def as_node(maybe_node: type[typing.Any], /) -> type[NodeType]: ...
63
+
64
+
65
+ @typing.overload
66
+ def as_node(maybe_node: typing.Any, /) -> NodeType: ...
67
+
68
+
69
+ @typing.overload
70
+ def as_node(*maybe_nodes: type[typing.Any]) -> tuple[type[NodeType], ...]: ...
71
+
72
+
73
+ @typing.overload
74
+ def as_node(*maybe_nodes: typing.Any) -> tuple[NodeType, ...]: ...
75
+
76
+
77
+ @typing.overload
78
+ def as_node(*maybe_nodes: type[typing.Any] | typing.Any) -> tuple[IsNode, ...]: ...
79
+
80
+
81
+ def as_node(*maybe_nodes: typing.Any) -> typing.Any | tuple[typing.Any, ...]:
82
+ for maybe_node in maybe_nodes:
83
+ if not is_node(maybe_node):
84
+ is_type = isinstance(maybe_node, type)
85
+ raise LookupError(
86
+ f"{'Type of' if is_type else 'Object of type'} "
87
+ f"{maybe_node.__name__ if is_type else maybe_node.__class__.__name__!r} "
88
+ "cannot be resolved as Node."
89
+ )
90
+ return maybe_nodes[0] if len(maybe_nodes) == 1 else maybe_nodes
91
+
92
+
25
93
  @cache_magic_value("__nodes__")
26
- def get_nodes(function: typing.Callable[..., typing.Any]) -> dict[str, type["Node"]]:
27
- return {k: v for k, v in get_annotations(function).items() if is_node(v)}
94
+ def get_nodes(function: typing.Callable[..., typing.Any], /) -> dict[str, type[NodeType]]:
95
+ return {k: v.as_node() for k, v in get_annotations(function).items() if is_node(v)}
28
96
 
29
97
 
30
98
  @cache_magic_value("__is_generator__")
31
99
  def is_generator(
32
100
  function: typing.Callable[..., typing.Any],
101
+ /,
33
102
  ) -> typing.TypeGuard[AsyncGeneratorType[typing.Any, None]]:
34
103
  return inspect.isasyncgenfunction(function)
35
104
 
36
105
 
37
- def get_node_calc_lst(node: type["Node"]) -> list[type["Node"]]:
38
- """Returns flattened list of node types in ordering required to calculate given node.
39
- Provides caching for passed node type."""
106
+ def unwrap_node(node: type[NodeType], /) -> tuple[type[NodeType], ...]:
107
+ """Unwrap node as flattened tuple of node types in ordering required to calculate given node.
40
108
 
41
- if calc_lst := getattr(node, "__nodes_calc_lst__", None):
42
- return calc_lst
43
- nodes_lst: list[type[Node]] = []
44
- for node_type in node.as_node().get_subnodes().values():
45
- nodes_lst.extend(get_node_calc_lst(node_type))
46
- calc_lst = [*nodes_lst, node]
47
- setattr(node, "__nodes_calc_lst__", calc_lst)
48
- return calc_lst
109
+ Provides caching for passed node type.
110
+ """
111
+ if (unwrapped := getattr(node, UNWRAPPED_NODE_KEY, None)) is not None:
112
+ return unwrapped
113
+
114
+ stack = deque([(node, node.get_subnodes().values())])
115
+ visited = list[type[NodeType]]()
116
+
117
+ while stack:
118
+ parent, child_nodes = stack.pop()
119
+
120
+ if parent not in visited:
121
+ visited.insert(0, parent)
122
+
123
+ for child in child_nodes:
124
+ stack.append((child, child.get_subnodes().values()))
125
+
126
+ unwrapped = tuple(visited)
127
+ setattr(node, UNWRAPPED_NODE_KEY, unwrapped)
128
+ return unwrapped
49
129
 
50
130
 
51
131
  class ComposeError(BaseException):
52
132
  pass
53
133
 
54
134
 
135
+ @typing.runtime_checkable
136
+ class Composable[R](typing.Protocol):
137
+ @classmethod
138
+ def compose(cls, *args: typing.Any, **kwargs: typing.Any) -> ComposeResult[R]: ...
139
+
140
+
141
+ class NodeImpersonation(typing.Protocol):
142
+ @classmethod
143
+ def as_node(cls) -> type[NodeProto[typing.Any]]: ...
144
+
145
+
146
+ class NodeComposeFunction[R](typing.Protocol):
147
+ __name__: str
148
+ __code__: CodeType
149
+
150
+ def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> ComposeResult[R]: ...
151
+
152
+
153
+ @typing.runtime_checkable
154
+ class NodeProto[R](Composable[R], NodeImpersonation, typing.Protocol):
155
+ @classmethod
156
+ def get_subnodes(cls) -> dict[str, type[NodeType]]: ...
157
+
158
+ @classmethod
159
+ def is_generator(cls) -> bool: ...
160
+
161
+
55
162
  class Node(abc.ABC):
56
163
  node: str = "node"
57
164
  scope: NodeScope = NodeScope.PER_EVENT
@@ -62,11 +169,7 @@ class Node(abc.ABC):
62
169
  pass
63
170
 
64
171
  @classmethod
65
- def compose_error(cls, error: str | None = None) -> typing.NoReturn:
66
- raise ComposeError(error)
67
-
68
- @classmethod
69
- def get_subnodes(cls) -> dict[str, type["Node"]]:
172
+ def get_subnodes(cls) -> dict[str, type[NodeType]]:
70
173
  return get_nodes(cls.compose)
71
174
 
72
175
  @classmethod
@@ -78,6 +181,41 @@ class Node(abc.ABC):
78
181
  return is_generator(cls.compose)
79
182
 
80
183
 
184
+ class scalar_node[T]: # noqa: N801
185
+ @typing.overload
186
+ def __new__(cls, x: NodeComposeFunction[Composable[T]], /) -> type[T]: ...
187
+
188
+ @typing.overload
189
+ def __new__(cls, x: NodeComposeFunction[T], /) -> type[T]: ...
190
+
191
+ @typing.overload
192
+ def __new__(
193
+ cls,
194
+ /,
195
+ *,
196
+ scope: NodeScope,
197
+ ) -> typing.Callable[[NodeComposeFunction[Composable[T]] | NodeComposeFunction[T]], type[T]]: ...
198
+
199
+ def __new__(cls, x=None, /, *, scope=NodeScope.PER_EVENT) -> typing.Any:
200
+ def inner(node_or_func, /) -> typing.Any:
201
+ namespace = {"node": "scalar", "scope": scope, "__module__": node_or_func.__module__}
202
+
203
+ if isinstance(node_or_func, type):
204
+ bases: list[type[typing.Any]] = [node_or_func]
205
+ node_bases = resolve_bases(node_or_func.__bases__)
206
+ if not any(issubclass(base, Node) for base in node_bases if isinstance(base, type)):
207
+ bases.append(Node)
208
+ return type(node_or_func.__name__, tuple(bases), namespace)
209
+ else:
210
+ base_node = generate_node(
211
+ func=node_or_func,
212
+ subnodes=tuple(get_nodes(node_or_func).values()),
213
+ )
214
+ return type(to_pascal_case(node_or_func.__name__), (base_node,), namespace)
215
+
216
+ return inner if x is None else inner(x)
217
+
218
+
81
219
  @typing.dataclass_transform(kw_only_default=True)
82
220
  class FactoryNode(Node, abc.ABC):
83
221
  node = "factory"
@@ -87,10 +225,10 @@ class FactoryNode(Node, abc.ABC):
87
225
  def compose(cls, *args, **kwargs) -> ComposeResult:
88
226
  pass
89
227
 
90
- def __new__(cls, **context: typing.Any) -> typing.Self:
228
+ def __new__(cls, **context: typing.Any) -> type[typing.Self]:
91
229
  namespace = dict(**cls.__dict__)
92
230
  namespace.pop("__new__", None)
93
- return type(cls.__name__, (cls,), context | namespace) # type: ignore
231
+ return type(cls.__name__, (cls,), namespace | context) # type: ignore
94
232
 
95
233
 
96
234
  @typing.dataclass_transform()
@@ -103,64 +241,54 @@ class DataNode(Node, abc.ABC):
103
241
  pass
104
242
 
105
243
 
106
- class ScalarNodeProto(Node, abc.ABC):
244
+ @scalar_node(scope=NodeScope.PER_CALL)
245
+ class Name:
107
246
  @classmethod
108
- @abc.abstractmethod
109
- def compose(cls, *args, **kwargs) -> ComposeResult:
110
- pass
111
-
112
-
113
- SCALAR_NODE = type("ScalarNode", (), {"node": "scalar"})
247
+ def compose(cls) -> str: ...
114
248
 
115
249
 
116
- if typing.TYPE_CHECKING:
250
+ class GlobalNode[Value](Node):
251
+ scope = NodeScope.GLOBAL
117
252
 
118
- class ScalarNode(ScalarNodeProto, abc.ABC):
119
- pass
253
+ @classmethod
254
+ def set(cls, value: Value, /) -> None:
255
+ setattr(cls, "_value", value)
120
256
 
121
- else:
257
+ @typing.overload
258
+ @classmethod
259
+ def get(cls) -> Value: ...
122
260
 
123
- def __init_subclass__(cls, *args, **kwargs): # noqa: N807
124
- if any(issubclass(base, ScalarNodeProto) for base in cls.__bases__ if base is not ScalarNode):
125
- raise RuntimeError("Scalar nodes do not support inheritance.")
126
-
127
- def _as_node(cls, bases, dct):
128
- if not hasattr(cls, "_scalar_node_type"):
129
- dct.update(cls.__dict__)
130
- scalar_node_type = type(cls.__name__, bases, dct)
131
- setattr(cls, "_scalar_node_type", scalar_node_type)
132
- return scalar_node_type
133
- return getattr(cls, "_scalar_node_type")
134
-
135
- def create_class(name, bases, dct):
136
- return type(
137
- "Scalar",
138
- (SCALAR_NODE,),
139
- {
140
- "as_node": classmethod(lambda cls: _as_node(cls, bases, dct)),
141
- "scope": Node.scope,
142
- "__init_subclass__": __init_subclass__,
143
- },
144
- )
145
-
146
- class ScalarNode(ScalarNodeProto, abc.ABC, metaclass=create_class):
147
- pass
261
+ @typing.overload
262
+ @classmethod
263
+ def get[Default](cls, *, default: Default) -> Value | Default: ...
148
264
 
265
+ @classmethod
266
+ def get(cls, **kwargs: typing.Any) -> typing.Any:
267
+ sentinel = object()
268
+ default = kwargs.pop("default", sentinel)
269
+ return getattr(cls, "_value") if default is sentinel else getattr(cls, "_value", default)
149
270
 
150
- class Name(ScalarNode, str):
151
271
  @classmethod
152
- def compose(cls) -> str: ...
272
+ def unset(cls) -> None:
273
+ if hasattr(cls, "_value"):
274
+ delattr(cls, "_value")
153
275
 
154
276
 
155
277
  __all__ = (
278
+ "Composable",
156
279
  "ComposeError",
157
280
  "DataNode",
158
281
  "FactoryNode",
282
+ "GlobalNode",
283
+ "IsNode",
159
284
  "Name",
160
285
  "Node",
161
- "SCALAR_NODE",
162
- "ScalarNode",
163
- "ScalarNodeProto",
286
+ "NodeImpersonation",
287
+ "NodeProto",
288
+ "NodeType",
289
+ "as_node",
164
290
  "get_nodes",
165
291
  "is_node",
292
+ "scalar_node",
293
+ "unwrap_node",
166
294
  )
@@ -4,25 +4,22 @@ from fntypes.result import Error, Ok
4
4
 
5
5
  from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
6
6
  from telegrinder.msgspec_utils import msgspec_convert
7
- from telegrinder.node.base import ComposeError, FactoryNode, Name, ScalarNode
8
- from telegrinder.node.update import UpdateNode
7
+ from telegrinder.node.base import ComposeError, FactoryNode, Name, scalar_node
9
8
 
10
- FieldType = typing.TypeVar("FieldType")
11
9
 
12
-
13
- class CallbackQueryNode(ScalarNode, CallbackQueryCute):
10
+ @scalar_node
11
+ class CallbackQueryData:
14
12
  @classmethod
15
- def compose(cls, update: UpdateNode) -> CallbackQueryCute:
16
- if not update.callback_query:
17
- raise ComposeError("Update is not a callback_query.")
18
- return update.callback_query.unwrap()
13
+ def compose(cls, callback_query: CallbackQueryCute) -> str:
14
+ return callback_query.data.expect(ComposeError("Cannot complete decode callback query data."))
19
15
 
20
16
 
21
- class CallbackQueryData(ScalarNode, dict[str, typing.Any]):
17
+ @scalar_node
18
+ class CallbackQueryDataJson:
22
19
  @classmethod
23
- def compose(cls, callback_query: CallbackQueryNode) -> dict[str, typing.Any]:
24
- return callback_query.decode_callback_data().expect(
25
- ComposeError("Cannot complete decode callback query data.")
20
+ def compose(cls, callback_query: CallbackQueryCute) -> dict:
21
+ return callback_query.decode_data().expect(
22
+ ComposeError("Cannot complete decode callback query data."),
26
23
  )
27
24
 
28
25
 
@@ -33,7 +30,7 @@ class _Field(FactoryNode):
33
30
  return cls(field_type=field_type)
34
31
 
35
32
  @classmethod
36
- def compose(cls, callback_query_data: CallbackQueryData, data_name: Name) -> typing.Any:
33
+ def compose(cls, callback_query_data: CallbackQueryDataJson, data_name: Name) -> typing.Any:
37
34
  if data := callback_query_data.get(data_name):
38
35
  match msgspec_convert(data, cls.field_type):
39
36
  case Ok(value):
@@ -45,9 +42,13 @@ class _Field(FactoryNode):
45
42
 
46
43
 
47
44
  if typing.TYPE_CHECKING:
48
- Field = typing.Annotated[FieldType, ...]
45
+ type Field[FieldType] = typing.Annotated[FieldType, ...]
49
46
  else:
50
47
  Field = _Field
51
48
 
52
49
 
53
- __all__ = ("CallbackQueryData", "CallbackQueryNode", "Field")
50
+ __all__ = (
51
+ "CallbackQueryData",
52
+ "CallbackQueryDataJson",
53
+ "Field",
54
+ )
@@ -4,7 +4,8 @@ from dataclasses import dataclass, field
4
4
  from fntypes.option import Nothing, Option, Some
5
5
 
6
6
  from telegrinder.node.base import DataNode
7
- from telegrinder.node.text import Text
7
+ from telegrinder.node.either import Either
8
+ from telegrinder.node.text import Caption, Text
8
9
 
9
10
 
10
11
  def single_split(s: str, separator: str) -> tuple[str, str]:
@@ -24,7 +25,7 @@ class CommandInfo(DataNode):
24
25
  mention: Option[str] = field(default_factory=Nothing)
25
26
 
26
27
  @classmethod
27
- def compose(cls, text: Text) -> typing.Self:
28
+ def compose(cls, text: Either[Text, Caption]) -> typing.Self:
28
29
  name, arguments = single_split(text, separator=" ")
29
30
  name, mention = cut_mention(name)
30
31
  return cls(name, arguments, mention)