telegrinder 0.4.2__py3-none-any.whl → 0.5.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 (233) hide show
  1. telegrinder/__init__.py +37 -55
  2. telegrinder/__meta__.py +1 -0
  3. telegrinder/api/__init__.py +6 -4
  4. telegrinder/api/api.py +100 -26
  5. telegrinder/api/error.py +42 -8
  6. telegrinder/api/response.py +4 -1
  7. telegrinder/api/token.py +2 -2
  8. telegrinder/bot/__init__.py +9 -25
  9. telegrinder/bot/bot.py +31 -25
  10. telegrinder/bot/cute_types/__init__.py +0 -0
  11. telegrinder/bot/cute_types/base.py +103 -61
  12. telegrinder/bot/cute_types/callback_query.py +447 -400
  13. telegrinder/bot/cute_types/chat_join_request.py +59 -62
  14. telegrinder/bot/cute_types/chat_member_updated.py +154 -157
  15. telegrinder/bot/cute_types/inline_query.py +41 -44
  16. telegrinder/bot/cute_types/message.py +2621 -2590
  17. telegrinder/bot/cute_types/pre_checkout_query.py +38 -42
  18. telegrinder/bot/cute_types/update.py +1 -8
  19. telegrinder/bot/cute_types/utils.py +1 -1
  20. telegrinder/bot/dispatch/__init__.py +10 -15
  21. telegrinder/bot/dispatch/abc.py +12 -11
  22. telegrinder/bot/dispatch/action.py +104 -0
  23. telegrinder/bot/dispatch/context.py +32 -26
  24. telegrinder/bot/dispatch/dispatch.py +61 -134
  25. telegrinder/bot/dispatch/handler/__init__.py +2 -0
  26. telegrinder/bot/dispatch/handler/abc.py +10 -8
  27. telegrinder/bot/dispatch/handler/audio_reply.py +2 -3
  28. telegrinder/bot/dispatch/handler/base.py +10 -33
  29. telegrinder/bot/dispatch/handler/document_reply.py +2 -3
  30. telegrinder/bot/dispatch/handler/func.py +55 -87
  31. telegrinder/bot/dispatch/handler/media_group_reply.py +2 -3
  32. telegrinder/bot/dispatch/handler/message_reply.py +2 -3
  33. telegrinder/bot/dispatch/handler/photo_reply.py +2 -3
  34. telegrinder/bot/dispatch/handler/sticker_reply.py +2 -3
  35. telegrinder/bot/dispatch/handler/video_reply.py +2 -3
  36. telegrinder/bot/dispatch/middleware/__init__.py +0 -0
  37. telegrinder/bot/dispatch/middleware/abc.py +79 -55
  38. telegrinder/bot/dispatch/middleware/global_middleware.py +18 -33
  39. telegrinder/bot/dispatch/process.py +84 -105
  40. telegrinder/bot/dispatch/return_manager/__init__.py +0 -0
  41. telegrinder/bot/dispatch/return_manager/abc.py +102 -65
  42. telegrinder/bot/dispatch/return_manager/callback_query.py +4 -5
  43. telegrinder/bot/dispatch/return_manager/inline_query.py +3 -4
  44. telegrinder/bot/dispatch/return_manager/message.py +8 -10
  45. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +4 -5
  46. telegrinder/bot/dispatch/view/__init__.py +4 -4
  47. telegrinder/bot/dispatch/view/abc.py +6 -16
  48. telegrinder/bot/dispatch/view/base.py +54 -178
  49. telegrinder/bot/dispatch/view/box.py +19 -18
  50. telegrinder/bot/dispatch/view/callback_query.py +4 -8
  51. telegrinder/bot/dispatch/view/chat_join_request.py +5 -6
  52. telegrinder/bot/dispatch/view/chat_member.py +5 -25
  53. telegrinder/bot/dispatch/view/error.py +9 -0
  54. telegrinder/bot/dispatch/view/inline_query.py +4 -8
  55. telegrinder/bot/dispatch/view/message.py +5 -25
  56. telegrinder/bot/dispatch/view/pre_checkout_query.py +4 -8
  57. telegrinder/bot/dispatch/view/raw.py +3 -109
  58. telegrinder/bot/dispatch/waiter_machine/__init__.py +2 -5
  59. telegrinder/bot/dispatch/waiter_machine/actions.py +6 -4
  60. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +1 -3
  61. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +1 -1
  62. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +11 -7
  63. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
  64. telegrinder/bot/dispatch/waiter_machine/machine.py +43 -60
  65. telegrinder/bot/dispatch/waiter_machine/middleware.py +19 -23
  66. telegrinder/bot/dispatch/waiter_machine/short_state.py +6 -5
  67. telegrinder/bot/polling/__init__.py +0 -0
  68. telegrinder/bot/polling/abc.py +0 -0
  69. telegrinder/bot/polling/polling.py +209 -88
  70. telegrinder/bot/rules/__init__.py +3 -16
  71. telegrinder/bot/rules/abc.py +42 -122
  72. telegrinder/bot/rules/callback_data.py +29 -49
  73. telegrinder/bot/rules/chat_join.py +5 -23
  74. telegrinder/bot/rules/command.py +8 -4
  75. telegrinder/bot/rules/enum_text.py +3 -4
  76. telegrinder/bot/rules/func.py +7 -14
  77. telegrinder/bot/rules/fuzzy.py +3 -4
  78. telegrinder/bot/rules/inline.py +8 -20
  79. telegrinder/bot/rules/integer.py +2 -3
  80. telegrinder/bot/rules/is_from.py +12 -11
  81. telegrinder/bot/rules/logic.py +11 -5
  82. telegrinder/bot/rules/markup.py +22 -14
  83. telegrinder/bot/rules/mention.py +8 -7
  84. telegrinder/bot/rules/message_entities.py +8 -4
  85. telegrinder/bot/rules/node.py +23 -12
  86. telegrinder/bot/rules/payload.py +5 -4
  87. telegrinder/bot/rules/payment_invoice.py +6 -21
  88. telegrinder/bot/rules/regex.py +2 -4
  89. telegrinder/bot/rules/rule_enum.py +8 -7
  90. telegrinder/bot/rules/start.py +5 -6
  91. telegrinder/bot/rules/state.py +1 -1
  92. telegrinder/bot/rules/text.py +4 -15
  93. telegrinder/bot/rules/update.py +3 -4
  94. telegrinder/bot/scenario/__init__.py +0 -0
  95. telegrinder/bot/scenario/abc.py +6 -5
  96. telegrinder/bot/scenario/checkbox.py +1 -1
  97. telegrinder/bot/scenario/choice.py +30 -39
  98. telegrinder/client/__init__.py +3 -5
  99. telegrinder/client/abc.py +11 -6
  100. telegrinder/client/aiohttp.py +141 -27
  101. telegrinder/client/form_data.py +1 -1
  102. telegrinder/model.py +61 -89
  103. telegrinder/modules.py +325 -102
  104. telegrinder/msgspec_utils/__init__.py +40 -0
  105. telegrinder/msgspec_utils/abc.py +18 -0
  106. telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
  107. telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
  108. telegrinder/msgspec_utils/custom_types/enum_meta.py +43 -0
  109. telegrinder/msgspec_utils/custom_types/literal.py +25 -0
  110. telegrinder/msgspec_utils/custom_types/option.py +17 -0
  111. telegrinder/msgspec_utils/decoder.py +389 -0
  112. telegrinder/msgspec_utils/encoder.py +206 -0
  113. telegrinder/{msgspec_json.py → msgspec_utils/json.py} +6 -5
  114. telegrinder/msgspec_utils/tools.py +75 -0
  115. telegrinder/node/__init__.py +24 -7
  116. telegrinder/node/attachment.py +1 -0
  117. telegrinder/node/base.py +154 -72
  118. telegrinder/node/callback_query.py +5 -5
  119. telegrinder/node/collection.py +39 -0
  120. telegrinder/node/command.py +1 -2
  121. telegrinder/node/composer.py +121 -72
  122. telegrinder/node/container.py +11 -8
  123. telegrinder/node/context.py +48 -0
  124. telegrinder/node/either.py +27 -40
  125. telegrinder/node/error.py +41 -0
  126. telegrinder/node/event.py +37 -11
  127. telegrinder/node/exceptions.py +7 -0
  128. telegrinder/node/file.py +0 -0
  129. telegrinder/node/i18n.py +108 -0
  130. telegrinder/node/me.py +3 -2
  131. telegrinder/node/payload.py +1 -1
  132. telegrinder/node/polymorphic.py +63 -28
  133. telegrinder/node/reply_message.py +12 -0
  134. telegrinder/node/rule.py +6 -13
  135. telegrinder/node/scope.py +14 -5
  136. telegrinder/node/session.py +53 -0
  137. telegrinder/node/source.py +41 -9
  138. telegrinder/node/text.py +1 -2
  139. telegrinder/node/tools/__init__.py +0 -0
  140. telegrinder/node/tools/generator.py +3 -5
  141. telegrinder/node/utility.py +16 -0
  142. telegrinder/py.typed +0 -0
  143. telegrinder/rules.py +0 -0
  144. telegrinder/tools/__init__.py +48 -88
  145. telegrinder/tools/aio.py +103 -0
  146. telegrinder/tools/callback_data_serialization/__init__.py +5 -0
  147. telegrinder/tools/{callback_data_serilization → callback_data_serialization}/abc.py +0 -0
  148. telegrinder/tools/{callback_data_serilization → callback_data_serialization}/json_ser.py +2 -3
  149. telegrinder/tools/{callback_data_serilization → callback_data_serialization}/msgpack_ser.py +45 -27
  150. telegrinder/tools/final.py +21 -0
  151. telegrinder/tools/formatting/__init__.py +2 -18
  152. telegrinder/tools/formatting/deep_links/__init__.py +39 -0
  153. telegrinder/tools/formatting/{deep_links.py → deep_links/links.py} +12 -85
  154. telegrinder/tools/formatting/deep_links/parsing.py +90 -0
  155. telegrinder/tools/formatting/deep_links/validators.py +8 -0
  156. telegrinder/tools/formatting/html_formatter.py +18 -45
  157. telegrinder/tools/fullname.py +83 -0
  158. telegrinder/tools/global_context/__init__.py +4 -3
  159. telegrinder/tools/global_context/abc.py +17 -14
  160. telegrinder/tools/global_context/builtin_context.py +39 -0
  161. telegrinder/tools/global_context/global_context.py +138 -39
  162. telegrinder/tools/input_file_directory.py +0 -0
  163. telegrinder/tools/keyboard/__init__.py +39 -0
  164. telegrinder/tools/keyboard/abc.py +159 -0
  165. telegrinder/tools/keyboard/base.py +77 -0
  166. telegrinder/tools/keyboard/buttons/__init__.py +14 -0
  167. telegrinder/tools/keyboard/buttons/base.py +18 -0
  168. telegrinder/tools/{buttons.py → keyboard/buttons/buttons.py} +71 -23
  169. telegrinder/tools/keyboard/buttons/static_buttons.py +56 -0
  170. telegrinder/tools/keyboard/buttons/tools.py +18 -0
  171. telegrinder/tools/keyboard/data.py +20 -0
  172. telegrinder/tools/keyboard/keyboard.py +131 -0
  173. telegrinder/tools/keyboard/static_keyboard.py +83 -0
  174. telegrinder/tools/lifespan.py +87 -51
  175. telegrinder/tools/limited_dict.py +4 -1
  176. telegrinder/tools/loop_wrapper.py +332 -0
  177. telegrinder/tools/magic/__init__.py +32 -0
  178. telegrinder/tools/magic/annotations.py +165 -0
  179. telegrinder/tools/magic/dictionary.py +20 -0
  180. telegrinder/tools/magic/function.py +246 -0
  181. telegrinder/tools/magic/shortcut.py +111 -0
  182. telegrinder/tools/parse_mode.py +9 -3
  183. telegrinder/tools/singleton/__init__.py +4 -0
  184. telegrinder/tools/singleton/abc.py +14 -0
  185. telegrinder/tools/singleton/singleton.py +18 -0
  186. telegrinder/tools/state_storage/__init__.py +0 -0
  187. telegrinder/tools/state_storage/abc.py +6 -1
  188. telegrinder/tools/state_storage/memory.py +1 -1
  189. telegrinder/tools/strings.py +0 -0
  190. telegrinder/types/__init__.py +307 -268
  191. telegrinder/types/enums.py +64 -37
  192. telegrinder/types/input_file.py +3 -3
  193. telegrinder/types/methods.py +5699 -5055
  194. telegrinder/types/methods_utils.py +62 -0
  195. telegrinder/types/objects.py +7846 -7058
  196. telegrinder/verification_utils.py +3 -1
  197. telegrinder-0.5.0.dist-info/METADATA +162 -0
  198. telegrinder-0.5.0.dist-info/RECORD +200 -0
  199. {telegrinder-0.4.2.dist-info → telegrinder-0.5.0.dist-info}/licenses/LICENSE +2 -2
  200. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +0 -20
  201. telegrinder/bot/rules/id.py +0 -24
  202. telegrinder/bot/rules/message.py +0 -15
  203. telegrinder/client/sonic.py +0 -212
  204. telegrinder/msgspec_utils.py +0 -478
  205. telegrinder/tools/adapter/__init__.py +0 -19
  206. telegrinder/tools/adapter/abc.py +0 -49
  207. telegrinder/tools/adapter/dataclass.py +0 -56
  208. telegrinder/tools/adapter/errors.py +0 -5
  209. telegrinder/tools/adapter/event.py +0 -61
  210. telegrinder/tools/adapter/node.py +0 -46
  211. telegrinder/tools/adapter/raw_event.py +0 -27
  212. telegrinder/tools/adapter/raw_update.py +0 -30
  213. telegrinder/tools/callback_data_serilization/__init__.py +0 -5
  214. telegrinder/tools/error_handler/__init__.py +0 -10
  215. telegrinder/tools/error_handler/abc.py +0 -30
  216. telegrinder/tools/error_handler/error.py +0 -9
  217. telegrinder/tools/error_handler/error_handler.py +0 -179
  218. telegrinder/tools/formatting/spec_html_formats.py +0 -75
  219. telegrinder/tools/functional.py +0 -8
  220. telegrinder/tools/global_context/telegrinder_ctx.py +0 -27
  221. telegrinder/tools/i18n/__init__.py +0 -12
  222. telegrinder/tools/i18n/abc.py +0 -32
  223. telegrinder/tools/i18n/middleware/__init__.py +0 -3
  224. telegrinder/tools/i18n/middleware/abc.py +0 -22
  225. telegrinder/tools/i18n/simple.py +0 -43
  226. telegrinder/tools/keyboard.py +0 -132
  227. telegrinder/tools/loop_wrapper/__init__.py +0 -4
  228. telegrinder/tools/loop_wrapper/abc.py +0 -20
  229. telegrinder/tools/loop_wrapper/loop_wrapper.py +0 -169
  230. telegrinder/tools/magic.py +0 -344
  231. telegrinder-0.4.2.dist-info/METADATA +0 -151
  232. telegrinder-0.4.2.dist-info/RECORD +0 -182
  233. {telegrinder-0.4.2.dist-info → telegrinder-0.5.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,75 @@
1
+ import types
2
+ import typing
3
+
4
+ import msgspec
5
+ from fntypes.co import Nothing, Variative
6
+
7
+ if typing.TYPE_CHECKING:
8
+ from telegrinder.tools.fullname import fullname
9
+ from telegrinder.tools.magic.function import bundle
10
+
11
+ def get_class_annotations(obj: typing.Any, /) -> dict[str, typing.Any]: ...
12
+
13
+ def get_type_hints(obj: typing.Any, /) -> dict[str, typing.Any]: ...
14
+
15
+ else:
16
+ from msgspec._utils import get_class_annotations, get_type_hints
17
+
18
+ def bundle(*args, **kwargs):
19
+ from telegrinder.tools.magic.function import bundle
20
+
21
+ return bundle(*args, **kwargs)
22
+
23
+ def fullname(*args, **kwargs):
24
+ from telegrinder.tools.fullname import fullname
25
+
26
+ return fullname(*args, **kwargs)
27
+
28
+
29
+ _COMMON_TYPES = frozenset((str, int, float, bool, None, Variative))
30
+
31
+
32
+ def get_origin[T](t: T, /) -> type[T]:
33
+ t_ = typing.get_origin(t) or t
34
+ t_ = type(t_) if not isinstance(t_, type) else t_
35
+ return typing.cast("type[T]", t_)
36
+
37
+
38
+ def is_common_type[T](t: T, /) -> typing.TypeGuard[type[T]]:
39
+ if not isinstance(t, type):
40
+ return False
41
+ return t in _COMMON_TYPES or issubclass(t, msgspec.Struct) or hasattr(t, "__dataclass_fields__")
42
+
43
+
44
+ def struct_asdict(
45
+ struct: msgspec.Struct,
46
+ /,
47
+ *,
48
+ exclude_unset: bool = True,
49
+ unset_as_nothing: bool = False,
50
+ ) -> dict[str, typing.Any]:
51
+ return {
52
+ k: v if not unset_as_nothing else Nothing() if v is msgspec.UNSET else v
53
+ for k, v in msgspec.structs.asdict(struct).items()
54
+ if not (exclude_unset and isinstance(v, msgspec.UnsetType | types.NoneType | Nothing))
55
+ }
56
+
57
+
58
+ def type_check(obj: typing.Any, t: typing.Any) -> bool:
59
+ return (
60
+ isinstance(obj, t)
61
+ if isinstance(t, type) and issubclass(t, msgspec.Struct)
62
+ else type(obj) in t
63
+ if isinstance(t, tuple)
64
+ else type(obj) is t
65
+ )
66
+
67
+
68
+ __all__ = (
69
+ "get_class_annotations",
70
+ "get_origin",
71
+ "get_type_hints",
72
+ "is_common_type",
73
+ "struct_asdict",
74
+ "type_check",
75
+ )
@@ -19,7 +19,7 @@ from .base import (
19
19
  Name,
20
20
  Node,
21
21
  NodeComposeFunction,
22
- NodeImpersonation,
22
+ NodeConvertable,
23
23
  NodeProto,
24
24
  NodeType,
25
25
  as_node,
@@ -32,15 +32,20 @@ from .callback_query import (
32
32
  CallbackQueryDataJson,
33
33
  Field,
34
34
  )
35
+ from .collection import Collection
35
36
  from .command import CommandInfo
36
37
  from .composer import NodeCollection, NodeSession, compose_node, compose_nodes
37
38
  from .container import ContainerNode
39
+ from .context import NODE_CONTEXT, NodeGlobalContext
38
40
  from .either import Either, Optional
41
+ from .error import Error
39
42
  from .event import EventNode
40
43
  from .file import File, FileId
44
+ from .i18n import ABCTranslator, KeySeparator
41
45
  from .me import Me
42
46
  from .payload import Payload, PayloadData, PayloadSerializer
43
47
  from .polymorphic import Polymorphic, impl
48
+ from .reply_message import ReplyMessage
44
49
  from .rule import RuleChain
45
50
  from .scope import (
46
51
  GLOBAL,
@@ -51,17 +56,26 @@ from .scope import (
51
56
  per_call,
52
57
  per_event,
53
58
  )
54
- from .source import ChatSource, Source, UserId, UserSource
55
- from .text import Text, TextInteger, TextLiteral
59
+ from .source import ChatId, ChatSource, Locale, Source, UserId, UserSource
60
+ from .text import Caption, Text, TextInteger, TextLiteral
56
61
  from .tools import generate_node
62
+ from .utility import TypeArgs
57
63
 
58
64
  __all__ = (
65
+ "GLOBAL",
66
+ "NODE_CONTEXT",
67
+ "PER_CALL",
68
+ "PER_EVENT",
69
+ "ABCTranslator",
59
70
  "Animation",
60
71
  "Attachment",
61
72
  "Audio",
62
73
  "CallbackQueryData",
63
74
  "CallbackQueryDataJson",
75
+ "Caption",
76
+ "ChatId",
64
77
  "ChatSource",
78
+ "Collection",
65
79
  "CommandInfo",
66
80
  "Composable",
67
81
  "ComposeError",
@@ -69,39 +83,42 @@ __all__ = (
69
83
  "DataNode",
70
84
  "Document",
71
85
  "Either",
86
+ "Error",
72
87
  "EventNode",
73
88
  "FactoryNode",
74
89
  "Field",
75
90
  "Field",
76
91
  "File",
77
92
  "FileId",
78
- "GLOBAL",
79
93
  "GlobalNode",
80
94
  "IsNode",
95
+ "KeySeparator",
96
+ "Locale",
81
97
  "Me",
82
98
  "Name",
83
99
  "Node",
84
100
  "NodeCollection",
85
101
  "NodeComposeFunction",
86
- "NodeImpersonation",
102
+ "NodeConvertable",
103
+ "NodeGlobalContext",
87
104
  "NodeProto",
88
105
  "NodeScope",
89
106
  "NodeSession",
90
107
  "NodeType",
91
108
  "Optional",
92
- "PER_CALL",
93
- "PER_EVENT",
94
109
  "Payload",
95
110
  "PayloadData",
96
111
  "PayloadSerializer",
97
112
  "Photo",
98
113
  "Polymorphic",
114
+ "ReplyMessage",
99
115
  "RuleChain",
100
116
  "Source",
101
117
  "SuccessfulPayment",
102
118
  "Text",
103
119
  "TextInteger",
104
120
  "TextLiteral",
121
+ "TypeArgs",
105
122
  "UserId",
106
123
  "UserSource",
107
124
  "Video",
@@ -68,6 +68,7 @@ class Attachment(DataNode):
68
68
  match getattr(message, attachment_type, Nothing()):
69
69
  case Some(attachment):
70
70
  return cls(attachment_type, **{attachment_type: Some(attachment)})
71
+
71
72
  raise ComposeError("No attachment found in message.")
72
73
 
73
74
 
telegrinder/node/base.py CHANGED
@@ -3,12 +3,19 @@ from __future__ import annotations
3
3
  import abc
4
4
  import inspect
5
5
  from collections import deque
6
- from types import AsyncGeneratorType, CodeType, resolve_bases
6
+ from functools import reduce
7
+ from itertools import islice
8
+ from types import CodeType, NoneType, UnionType, resolve_bases
7
9
 
8
10
  import typing_extensions as typing
9
11
 
12
+ from telegrinder.node.context import NODE_CONTEXT
13
+ from telegrinder.node.exceptions import ComposeError
10
14
  from telegrinder.node.scope import NodeScope
11
- from telegrinder.tools.magic import cache_magic_value, get_annotations
15
+ from telegrinder.node.session import NodeSession
16
+ from telegrinder.tools.aio import Generator
17
+ from telegrinder.tools.fullname import fullname
18
+ from telegrinder.tools.magic.function import function_context, get_func_annotations
12
19
  from telegrinder.tools.strings import to_pascal_case
13
20
 
14
21
  if typing.TYPE_CHECKING:
@@ -16,94 +23,138 @@ if typing.TYPE_CHECKING:
16
23
  else:
17
24
 
18
25
  def generate_node(*args, **kwargs):
19
- globalns = globals()
20
- if "__generate_node" not in globalns:
21
- import telegrinder.node.tools.generator
26
+ from telegrinder.node.tools.generator import generate_node
22
27
 
23
- globals()["__generate_node"] = telegrinder.node.tools.generator.generate_node
24
-
25
- return globals()["__generate_node"](*args, **kwargs)
28
+ return generate_node(*args, **kwargs)
26
29
 
27
30
 
28
31
  type NodeType = Node | NodeProto[typing.Any]
29
32
  type IsNode = NodeType | type[NodeType]
33
+ type AnyNode = IsNode | NodeConvertable
30
34
 
31
35
  T = typing.TypeVar("T", default=typing.Any)
32
36
 
33
- ComposeResult: typing.TypeAlias = T | typing.Awaitable[T] | typing.AsyncGenerator[T, None]
37
+ ComposeResult: typing.TypeAlias = T | typing.Awaitable[T] | Generator[T, typing.Any, typing.Any]
38
+
39
+ _NODEFAULT: typing.Final[object] = object()
40
+ _NONE_TYPES: typing.Final[set[typing.Any]] = {None, NoneType}
41
+ _UNION_TYPES: typing.Final[set[typing.Any]] = {typing.Union, UnionType}
42
+ UNWRAPPED_NODE_KEY: typing.Final[str] = "__unwrapped_node__"
43
+
34
44
 
35
- UNWRAPPED_NODE_KEY = "__unwrapped_node__"
45
+ def is_node(maybe_node: typing.Any, /) -> typing.TypeIs[AnyNode]:
46
+ return hasattr(maybe_node, "as_node") or is_node_type(maybe_node)
36
47
 
37
48
 
38
49
  @typing.overload
39
- def is_node(maybe_node: type[typing.Any], /) -> typing.TypeIs[type[NodeType]]: ...
50
+ def as_node(maybe_node: typing.Any, /) -> IsNode: ...
40
51
 
41
52
 
42
53
  @typing.overload
43
- def is_node(maybe_node: typing.Any, /) -> typing.TypeIs[NodeType]: ...
54
+ def as_node(
55
+ maybe_node: typing.Any,
56
+ /,
57
+ *,
58
+ raise_exception: typing.Literal[False],
59
+ ) -> IsNode | None: ...
60
+
44
61
 
62
+ def union_as_node(union: UnionType, /) -> IsNode | None:
63
+ from telegrinder.node.either import _Either
45
64
 
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
65
+ args = typing.get_args(union)
66
+ if not args:
67
+ return None
51
68
 
52
- return (
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))
69
+ plain, opt = [t for t in args if t not in _NONE_TYPES], any(t in _NONE_TYPES for t in args)
70
+ if not plain:
71
+ return None
72
+
73
+ nodes = typing.cast("IsNode | tuple[IsNode, ...] | None", as_node(*plain, raise_exception=False))
74
+ if nodes is None:
75
+ return None
76
+
77
+ nodes = (nodes,) if not isinstance(nodes, tuple) else nodes
78
+ node = reduce(
79
+ lambda left, right: _Either[left, right],
80
+ islice(nodes, 1, None),
81
+ nodes[0], # type: ignore
58
82
  )
83
+ return _Either[node] if opt else node
59
84
 
60
85
 
61
86
  @typing.overload
62
- def as_node(maybe_node: type[typing.Any], /) -> type[NodeType]: ...
87
+ def as_node(*maybe_nodes: typing.Any) -> tuple[IsNode, ...]: ...
63
88
 
64
89
 
65
90
  @typing.overload
66
- def as_node(maybe_node: typing.Any, /) -> NodeType: ...
91
+ def as_node(
92
+ *maybe_nodes: typing.Any,
93
+ raise_exception: typing.Literal[False],
94
+ ) -> tuple[IsNode, ...] | None: ...
67
95
 
68
96
 
69
- @typing.overload
70
- def as_node(*maybe_nodes: type[typing.Any]) -> tuple[type[NodeType], ...]: ...
97
+ def as_node(
98
+ *maybe_nodes: typing.Any,
99
+ raise_exception: bool = True,
100
+ ) -> IsNode | tuple[IsNode, ...] | None:
101
+ nodes = []
71
102
 
103
+ for maybe_node in maybe_nodes:
104
+ if isinstance(maybe_node, typing.TypeAliasType):
105
+ maybe_node = maybe_node.__value__
106
+
107
+ if (typing.get_origin(maybe_node) or maybe_node) in _UNION_TYPES and (
108
+ maybe_node := union_as_node(union := maybe_node)
109
+ ) is None:
110
+ if not raise_exception:
111
+ return None
112
+ raise TypeError(f"Union `{union!r}` doesn't contain all types of Node.")
113
+
114
+ if not is_node_type(orig := typing.get_origin(maybe_node) or maybe_node):
115
+ if not hasattr(orig, "as_node"):
116
+ if not raise_exception:
117
+ return None
118
+
119
+ raise TypeError(
120
+ f"{'Type of' if isinstance(maybe_node, type) else 'Object of type'} "
121
+ f"{fullname(maybe_node)!r} cannot be resolved as Node.",
122
+ )
72
123
 
73
- @typing.overload
74
- def as_node(*maybe_nodes: typing.Any) -> tuple[NodeType, ...]: ...
124
+ maybe_node = orig.as_node()
75
125
 
126
+ nodes.append(maybe_node)
76
127
 
77
- @typing.overload
78
- def as_node(*maybe_nodes: type[typing.Any] | typing.Any) -> tuple[IsNode, ...]: ...
128
+ return nodes[0] if len(nodes) == 1 else tuple(nodes)
79
129
 
80
130
 
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
131
+ def is_node_type(obj: typing.Any, /) -> typing.TypeIs[IsNode]:
132
+ return isinstance(obj, Node | NodeProto) or (isinstance(obj, type) and issubclass(obj, Node | NodeProto))
91
133
 
92
134
 
93
- @cache_magic_value("__nodes__")
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)}
135
+ @function_context("nodes")
136
+ def get_nodes(
137
+ function: typing.Callable[..., typing.Any],
138
+ /,
139
+ *,
140
+ start_idx: int = 0,
141
+ ) -> dict[str, IsNode]:
142
+ return {
143
+ k: node
144
+ for index, (k, v) in enumerate(get_func_annotations(function).items())
145
+ if (node := as_node(v, raise_exception=False)) is not None and index >= start_idx
146
+ }
96
147
 
97
148
 
98
- @cache_magic_value("__is_generator__")
149
+ @function_context("is_generator")
99
150
  def is_generator(
100
151
  function: typing.Callable[..., typing.Any],
101
152
  /,
102
- ) -> typing.TypeGuard[AsyncGeneratorType[typing.Any, None]]:
103
- return inspect.isasyncgenfunction(function)
153
+ ) -> typing.TypeGuard[Generator[typing.Any, typing.Any, typing.Any]]:
154
+ return inspect.isgeneratorfunction(function) or inspect.isasyncgenfunction(function)
104
155
 
105
156
 
106
- def unwrap_node(node: type[NodeType], /) -> tuple[type[NodeType], ...]:
157
+ def unwrap_node(node: IsNode, /) -> tuple[IsNode, ...]:
107
158
  """Unwrap node as flattened tuple of node types in ordering required to calculate given node.
108
159
 
109
160
  Provides caching for passed node type.
@@ -111,34 +162,42 @@ def unwrap_node(node: type[NodeType], /) -> tuple[type[NodeType], ...]:
111
162
  if (unwrapped := getattr(node, UNWRAPPED_NODE_KEY, None)) is not None:
112
163
  return unwrapped
113
164
 
114
- stack = deque([(node, node.get_subnodes().values())])
115
- visited = list[type[NodeType]]()
165
+ stack = deque([(node, node.get_subnodes().values(), [node])])
166
+ visited = list[IsNode]()
116
167
 
117
168
  while stack:
118
- parent, child_nodes = stack.pop()
169
+ parent, child_nodes, path = stack.pop()
170
+ dependencies = set(child_nodes)
171
+
172
+ if parent in dependencies:
173
+ raise ComposeError(f"Node `{fullname(parent)}` refers to itself in dependency tree.")
119
174
 
120
175
  if parent not in visited:
121
176
  visited.insert(0, parent)
122
177
 
123
178
  for child in child_nodes:
124
- stack.append((child, child.get_subnodes().values()))
179
+ subnodes = child.get_subnodes().values()
180
+ dependencies.update(subnodes)
181
+ if child in path:
182
+ raise ComposeError(
183
+ f"Cannot resolve node `{fullname(node)}` due to circular dependency "
184
+ f"({' -> '.join(fullname(n) for n in path[path.index(child) :] + [child])} <...>)",
185
+ )
186
+
187
+ stack.append((child, subnodes, path + [child]))
125
188
 
126
189
  unwrapped = tuple(visited)
127
190
  setattr(node, UNWRAPPED_NODE_KEY, unwrapped)
128
191
  return unwrapped
129
192
 
130
193
 
131
- class ComposeError(BaseException):
132
- pass
133
-
134
-
135
194
  @typing.runtime_checkable
136
195
  class Composable[R](typing.Protocol):
137
196
  @classmethod
138
197
  def compose(cls, *args: typing.Any, **kwargs: typing.Any) -> ComposeResult[R]: ...
139
198
 
140
199
 
141
- class NodeImpersonation(typing.Protocol):
200
+ class NodeConvertable(typing.Protocol):
142
201
  @classmethod
143
202
  def as_node(cls) -> type[NodeProto[typing.Any]]: ...
144
203
 
@@ -151,9 +210,9 @@ class NodeComposeFunction[R](typing.Protocol):
151
210
 
152
211
 
153
212
  @typing.runtime_checkable
154
- class NodeProto[R](Composable[R], NodeImpersonation, typing.Protocol):
213
+ class NodeProto[R](Composable[R], typing.Protocol):
155
214
  @classmethod
156
- def get_subnodes(cls) -> dict[str, type[NodeType]]: ...
215
+ def get_subnodes(cls) -> dict[str, IsNode]: ...
157
216
 
158
217
  @classmethod
159
218
  def is_generator(cls) -> bool: ...
@@ -169,13 +228,9 @@ class Node(abc.ABC):
169
228
  pass
170
229
 
171
230
  @classmethod
172
- def get_subnodes(cls) -> dict[str, type[NodeType]]:
231
+ def get_subnodes(cls) -> dict[str, IsNode]:
173
232
  return get_nodes(cls.compose)
174
233
 
175
- @classmethod
176
- def as_node(cls) -> type[typing.Self]:
177
- return cls
178
-
179
234
  @classmethod
180
235
  def is_generator(cls) -> bool:
181
236
  return is_generator(cls.compose)
@@ -194,17 +249,30 @@ class scalar_node[T]: # noqa: N801
194
249
  /,
195
250
  *,
196
251
  scope: NodeScope,
197
- ) -> typing.Callable[[NodeComposeFunction[Composable[T]] | NodeComposeFunction[T]], type[T]]: ...
252
+ ) -> typing.Callable[[NodeComposeFunction[Composable[T]]], type[T]]: ...
253
+
254
+ @typing.overload
255
+ def __new__(
256
+ cls,
257
+ /,
258
+ *,
259
+ scope: NodeScope,
260
+ ) -> typing.Callable[[NodeComposeFunction[T]], type[T]]: ...
198
261
 
199
- def __new__(cls, x=None, /, *, scope=NodeScope.PER_EVENT) -> typing.Any:
262
+ def __new__(cls, x=None, /, *, scope=_NODEFAULT) -> typing.Any:
200
263
  def inner(node_or_func, /) -> typing.Any:
201
- namespace = {"node": "scalar", "scope": scope, "__module__": node_or_func.__module__}
264
+ namespace = {"node": "scalar", "__module__": node_or_func.__module__}
202
265
 
203
266
  if isinstance(node_or_func, type):
204
267
  bases: list[type[typing.Any]] = [node_or_func]
205
268
  node_bases = resolve_bases(node_or_func.__bases__)
269
+
206
270
  if not any(issubclass(base, Node) for base in node_bases if isinstance(base, type)):
207
271
  bases.append(Node)
272
+ namespace["scope"] = NodeScope.PER_EVENT if scope is _NODEFAULT else scope
273
+ elif scope is not _NODEFAULT:
274
+ namespace["scope"] = scope
275
+
208
276
  return type(node_or_func.__name__, tuple(bases), namespace)
209
277
  else:
210
278
  base_node = generate_node(
@@ -247,12 +315,18 @@ class Name:
247
315
  def compose(cls) -> str: ...
248
316
 
249
317
 
318
+ @scalar_node
319
+ class NodeClass:
320
+ @classmethod
321
+ def compose(cls) -> type[Node]: ...
322
+
323
+
250
324
  class GlobalNode[Value](Node):
251
325
  scope = NodeScope.GLOBAL
252
326
 
253
327
  @classmethod
254
328
  def set(cls, value: Value, /) -> None:
255
- setattr(cls, "_value", value)
329
+ NODE_CONTEXT.global_session[cls] = NodeSession(cls, value)
256
330
 
257
331
  @typing.overload
258
332
  @classmethod
@@ -266,12 +340,18 @@ class GlobalNode[Value](Node):
266
340
  def get(cls, **kwargs: typing.Any) -> typing.Any:
267
341
  sentinel = object()
268
342
  default = kwargs.pop("default", sentinel)
269
- return getattr(cls, "_value") if default is sentinel else getattr(cls, "_value", default)
343
+
344
+ if default is not sentinel and cls not in NODE_CONTEXT.global_sessions:
345
+ return default
346
+
347
+ if (session := NODE_CONTEXT.global_sessions.get(cls)) is None and default is sentinel:
348
+ raise ValueError(f"Node `{fullname(cls)}` has no global value.")
349
+
350
+ return session.value if session is not None else default
270
351
 
271
352
  @classmethod
272
353
  def unset(cls) -> None:
273
- if hasattr(cls, "_value"):
274
- delattr(cls, "_value")
354
+ NODE_CONTEXT.global_sessions.pop(cls, None)
275
355
 
276
356
 
277
357
  __all__ = (
@@ -283,12 +363,14 @@ __all__ = (
283
363
  "IsNode",
284
364
  "Name",
285
365
  "Node",
286
- "NodeImpersonation",
366
+ "NodeClass",
367
+ "NodeConvertable",
287
368
  "NodeProto",
288
369
  "NodeType",
289
370
  "as_node",
290
371
  "get_nodes",
291
372
  "is_node",
373
+ "is_node_type",
292
374
  "scalar_node",
293
375
  "unwrap_node",
294
376
  )
@@ -3,7 +3,7 @@ import typing
3
3
  from fntypes.result import Error, Ok
4
4
 
5
5
  from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
6
- from telegrinder.msgspec_utils import msgspec_convert
6
+ from telegrinder.msgspec_utils import convert
7
7
  from telegrinder.node.base import ComposeError, FactoryNode, Name, scalar_node
8
8
 
9
9
 
@@ -24,15 +24,15 @@ class CallbackQueryDataJson:
24
24
 
25
25
 
26
26
  class _Field(FactoryNode):
27
- field_type: type[typing.Any]
27
+ field_type: typing.Any
28
28
 
29
- def __class_getitem__(cls, field_type: type[typing.Any], /) -> typing.Self:
29
+ def __class_getitem__(cls, field_type: typing.Any, /) -> typing.Self:
30
30
  return cls(field_type=field_type)
31
31
 
32
32
  @classmethod
33
33
  def compose(cls, callback_query_data: CallbackQueryDataJson, data_name: Name) -> typing.Any:
34
34
  if data := callback_query_data.get(data_name):
35
- match msgspec_convert(data, cls.field_type):
35
+ match convert(data, cls.field_type):
36
36
  case Ok(value):
37
37
  return value
38
38
  case Error(err):
@@ -42,7 +42,7 @@ class _Field(FactoryNode):
42
42
 
43
43
 
44
44
  if typing.TYPE_CHECKING:
45
- type Field[FieldType] = typing.Annotated[FieldType, ...]
45
+ type Field[FieldType] = FieldType
46
46
  else:
47
47
  Field = _Field
48
48
 
@@ -0,0 +1,39 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ import msgspec
5
+
6
+ from telegrinder.node.base import IsNode, Node, as_node
7
+ from telegrinder.tools.magic.annotations import Annotations
8
+ from telegrinder.tools.magic.dictionary import extract
9
+
10
+
11
+ class Collection(Node):
12
+ __subnodes__: typing.ClassVar[dict[str, IsNode] | None] = None
13
+
14
+ @classmethod
15
+ def get_subnodes(cls) -> dict[str, IsNode]:
16
+ if cls.__subnodes__ is None:
17
+ cls.__subnodes__ = {
18
+ name: node
19
+ for name, annotation in Annotations(obj=cls).get(cache=True, exclude_forward_refs=True).items()
20
+ if (node := as_node(annotation, raise_exception=False)) is not None
21
+ }
22
+
23
+ return cls.__subnodes__
24
+
25
+ @classmethod
26
+ def compose(cls, **kwargs: typing.Any) -> typing.Self:
27
+ nodes = extract(cls.__subnodes__ or (), kwargs)
28
+
29
+ if dataclasses.is_dataclass(cls) or issubclass(cls, msgspec.Struct):
30
+ return cls(**nodes)
31
+
32
+ instance = cls()
33
+ for name, value in nodes.items():
34
+ setattr(instance, name, value)
35
+
36
+ return instance
37
+
38
+
39
+ __all__ = ("Collection",)
@@ -4,7 +4,6 @@ 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.either import Either
8
7
  from telegrinder.node.text import Caption, Text
9
8
 
10
9
 
@@ -25,7 +24,7 @@ class CommandInfo(DataNode):
25
24
  mention: Option[str] = field(default_factory=Nothing)
26
25
 
27
26
  @classmethod
28
- def compose(cls, text: Either[Text, Caption]) -> typing.Self:
27
+ def compose(cls, text: Text | Caption) -> typing.Self:
29
28
  name, arguments = single_split(text, separator=" ")
30
29
  name, mention = cut_mention(name)
31
30
  return cls(name, arguments, mention)