telegrinder 1.0.0rc1__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.
Files changed (215) hide show
  1. telegrinder/__init__.py +258 -0
  2. telegrinder/__meta__.py +1 -0
  3. telegrinder/api/__init__.py +15 -0
  4. telegrinder/api/api.py +175 -0
  5. telegrinder/api/error.py +50 -0
  6. telegrinder/api/response.py +23 -0
  7. telegrinder/api/token.py +30 -0
  8. telegrinder/api/validators.py +30 -0
  9. telegrinder/bot/__init__.py +144 -0
  10. telegrinder/bot/bot.py +70 -0
  11. telegrinder/bot/cute_types/__init__.py +41 -0
  12. telegrinder/bot/cute_types/base.py +228 -0
  13. telegrinder/bot/cute_types/base.pyi +49 -0
  14. telegrinder/bot/cute_types/business_connection.py +9 -0
  15. telegrinder/bot/cute_types/business_messages_deleted.py +9 -0
  16. telegrinder/bot/cute_types/callback_query.py +248 -0
  17. telegrinder/bot/cute_types/chat_boost_removed.py +9 -0
  18. telegrinder/bot/cute_types/chat_boost_updated.py +9 -0
  19. telegrinder/bot/cute_types/chat_join_request.py +59 -0
  20. telegrinder/bot/cute_types/chat_member_updated.py +158 -0
  21. telegrinder/bot/cute_types/chosen_inline_result.py +11 -0
  22. telegrinder/bot/cute_types/inline_query.py +41 -0
  23. telegrinder/bot/cute_types/message.py +2809 -0
  24. telegrinder/bot/cute_types/message_reaction_count_updated.py +9 -0
  25. telegrinder/bot/cute_types/message_reaction_updated.py +9 -0
  26. telegrinder/bot/cute_types/paid_media_purchased.py +11 -0
  27. telegrinder/bot/cute_types/poll.py +9 -0
  28. telegrinder/bot/cute_types/poll_answer.py +9 -0
  29. telegrinder/bot/cute_types/pre_checkout_query.py +36 -0
  30. telegrinder/bot/cute_types/shipping_query.py +11 -0
  31. telegrinder/bot/cute_types/update.py +209 -0
  32. telegrinder/bot/cute_types/utils.py +141 -0
  33. telegrinder/bot/dispatch/__init__.py +99 -0
  34. telegrinder/bot/dispatch/abc.py +74 -0
  35. telegrinder/bot/dispatch/action.py +99 -0
  36. telegrinder/bot/dispatch/context.py +162 -0
  37. telegrinder/bot/dispatch/dispatch.py +362 -0
  38. telegrinder/bot/dispatch/handler/__init__.py +23 -0
  39. telegrinder/bot/dispatch/handler/abc.py +25 -0
  40. telegrinder/bot/dispatch/handler/audio_reply.py +43 -0
  41. telegrinder/bot/dispatch/handler/base.py +34 -0
  42. telegrinder/bot/dispatch/handler/document_reply.py +43 -0
  43. telegrinder/bot/dispatch/handler/func.py +73 -0
  44. telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
  45. telegrinder/bot/dispatch/handler/message_reply.py +35 -0
  46. telegrinder/bot/dispatch/handler/photo_reply.py +43 -0
  47. telegrinder/bot/dispatch/handler/sticker_reply.py +36 -0
  48. telegrinder/bot/dispatch/handler/video_reply.py +43 -0
  49. telegrinder/bot/dispatch/middleware/__init__.py +13 -0
  50. telegrinder/bot/dispatch/middleware/abc.py +112 -0
  51. telegrinder/bot/dispatch/middleware/box.py +32 -0
  52. telegrinder/bot/dispatch/middleware/filter.py +88 -0
  53. telegrinder/bot/dispatch/middleware/media_group.py +69 -0
  54. telegrinder/bot/dispatch/process.py +93 -0
  55. telegrinder/bot/dispatch/return_manager/__init__.py +21 -0
  56. telegrinder/bot/dispatch/return_manager/abc.py +107 -0
  57. telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
  58. telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
  59. telegrinder/bot/dispatch/return_manager/message.py +34 -0
  60. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +19 -0
  61. telegrinder/bot/dispatch/return_manager/utils.py +20 -0
  62. telegrinder/bot/dispatch/router/__init__.py +4 -0
  63. telegrinder/bot/dispatch/router/abc.py +15 -0
  64. telegrinder/bot/dispatch/router/base.py +154 -0
  65. telegrinder/bot/dispatch/view/__init__.py +15 -0
  66. telegrinder/bot/dispatch/view/abc.py +15 -0
  67. telegrinder/bot/dispatch/view/base.py +226 -0
  68. telegrinder/bot/dispatch/view/box.py +207 -0
  69. telegrinder/bot/dispatch/view/media_group.py +25 -0
  70. telegrinder/bot/dispatch/waiter_machine/__init__.py +25 -0
  71. telegrinder/bot/dispatch/waiter_machine/actions.py +16 -0
  72. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +13 -0
  73. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +53 -0
  74. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +61 -0
  75. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +49 -0
  76. telegrinder/bot/dispatch/waiter_machine/machine.py +264 -0
  77. telegrinder/bot/dispatch/waiter_machine/middleware.py +77 -0
  78. telegrinder/bot/dispatch/waiter_machine/short_state.py +105 -0
  79. telegrinder/bot/polling/__init__.py +4 -0
  80. telegrinder/bot/polling/abc.py +25 -0
  81. telegrinder/bot/polling/error_handler.py +93 -0
  82. telegrinder/bot/polling/polling.py +167 -0
  83. telegrinder/bot/polling/utils.py +12 -0
  84. telegrinder/bot/rules/__init__.py +166 -0
  85. telegrinder/bot/rules/abc.py +150 -0
  86. telegrinder/bot/rules/button.py +20 -0
  87. telegrinder/bot/rules/callback_data.py +109 -0
  88. telegrinder/bot/rules/chat_join.py +28 -0
  89. telegrinder/bot/rules/chat_member_updated.py +145 -0
  90. telegrinder/bot/rules/command.py +137 -0
  91. telegrinder/bot/rules/enum_text.py +29 -0
  92. telegrinder/bot/rules/func.py +21 -0
  93. telegrinder/bot/rules/fuzzy.py +21 -0
  94. telegrinder/bot/rules/inline.py +45 -0
  95. telegrinder/bot/rules/integer.py +19 -0
  96. telegrinder/bot/rules/is_from.py +213 -0
  97. telegrinder/bot/rules/logic.py +22 -0
  98. telegrinder/bot/rules/magic.py +60 -0
  99. telegrinder/bot/rules/markup.py +51 -0
  100. telegrinder/bot/rules/media.py +13 -0
  101. telegrinder/bot/rules/mention.py +15 -0
  102. telegrinder/bot/rules/message_entities.py +37 -0
  103. telegrinder/bot/rules/node.py +43 -0
  104. telegrinder/bot/rules/payload.py +89 -0
  105. telegrinder/bot/rules/payment_invoice.py +14 -0
  106. telegrinder/bot/rules/regex.py +34 -0
  107. telegrinder/bot/rules/rule_enum.py +71 -0
  108. telegrinder/bot/rules/start.py +73 -0
  109. telegrinder/bot/rules/state.py +35 -0
  110. telegrinder/bot/rules/text.py +27 -0
  111. telegrinder/bot/rules/update.py +14 -0
  112. telegrinder/bot/scenario/__init__.py +5 -0
  113. telegrinder/bot/scenario/abc.py +16 -0
  114. telegrinder/bot/scenario/checkbox.py +183 -0
  115. telegrinder/bot/scenario/choice.py +44 -0
  116. telegrinder/client/__init__.py +11 -0
  117. telegrinder/client/abc.py +136 -0
  118. telegrinder/client/form_data.py +34 -0
  119. telegrinder/client/rnet.py +198 -0
  120. telegrinder/model.py +133 -0
  121. telegrinder/model.pyi +57 -0
  122. telegrinder/modules.py +1081 -0
  123. telegrinder/msgspec_utils/__init__.py +42 -0
  124. telegrinder/msgspec_utils/abc.py +16 -0
  125. telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
  126. telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
  127. telegrinder/msgspec_utils/custom_types/enum_meta.py +61 -0
  128. telegrinder/msgspec_utils/custom_types/literal.py +25 -0
  129. telegrinder/msgspec_utils/custom_types/option.py +17 -0
  130. telegrinder/msgspec_utils/decoder.py +388 -0
  131. telegrinder/msgspec_utils/encoder.py +204 -0
  132. telegrinder/msgspec_utils/json.py +15 -0
  133. telegrinder/msgspec_utils/tools.py +80 -0
  134. telegrinder/node/__init__.py +80 -0
  135. telegrinder/node/compose.py +193 -0
  136. telegrinder/node/nodes/__init__.py +96 -0
  137. telegrinder/node/nodes/attachment.py +169 -0
  138. telegrinder/node/nodes/callback_query.py +25 -0
  139. telegrinder/node/nodes/channel.py +97 -0
  140. telegrinder/node/nodes/command.py +33 -0
  141. telegrinder/node/nodes/error.py +43 -0
  142. telegrinder/node/nodes/event.py +70 -0
  143. telegrinder/node/nodes/file.py +39 -0
  144. telegrinder/node/nodes/global_node.py +66 -0
  145. telegrinder/node/nodes/i18n.py +110 -0
  146. telegrinder/node/nodes/me.py +26 -0
  147. telegrinder/node/nodes/message_entities.py +15 -0
  148. telegrinder/node/nodes/payload.py +84 -0
  149. telegrinder/node/nodes/reply_message.py +14 -0
  150. telegrinder/node/nodes/source.py +172 -0
  151. telegrinder/node/nodes/state_mutator.py +71 -0
  152. telegrinder/node/nodes/text.py +62 -0
  153. telegrinder/node/scope.py +88 -0
  154. telegrinder/node/utils.py +38 -0
  155. telegrinder/py.typed +0 -0
  156. telegrinder/rules.py +1 -0
  157. telegrinder/tools/__init__.py +183 -0
  158. telegrinder/tools/aio.py +147 -0
  159. telegrinder/tools/final.py +21 -0
  160. telegrinder/tools/formatting/__init__.py +85 -0
  161. telegrinder/tools/formatting/deep_links/__init__.py +39 -0
  162. telegrinder/tools/formatting/deep_links/links.py +468 -0
  163. telegrinder/tools/formatting/deep_links/parsing.py +88 -0
  164. telegrinder/tools/formatting/deep_links/validators.py +8 -0
  165. telegrinder/tools/formatting/html.py +241 -0
  166. telegrinder/tools/fullname.py +82 -0
  167. telegrinder/tools/global_context/__init__.py +13 -0
  168. telegrinder/tools/global_context/abc.py +63 -0
  169. telegrinder/tools/global_context/builtin_context.py +45 -0
  170. telegrinder/tools/global_context/global_context.py +614 -0
  171. telegrinder/tools/input_file_directory.py +30 -0
  172. telegrinder/tools/keyboard/__init__.py +6 -0
  173. telegrinder/tools/keyboard/abc.py +84 -0
  174. telegrinder/tools/keyboard/base.py +108 -0
  175. telegrinder/tools/keyboard/button.py +181 -0
  176. telegrinder/tools/keyboard/data.py +31 -0
  177. telegrinder/tools/keyboard/keyboard.py +160 -0
  178. telegrinder/tools/keyboard/utils.py +95 -0
  179. telegrinder/tools/lifespan.py +188 -0
  180. telegrinder/tools/limited_dict.py +35 -0
  181. telegrinder/tools/loop_wrapper.py +271 -0
  182. telegrinder/tools/magic/__init__.py +29 -0
  183. telegrinder/tools/magic/annotations.py +172 -0
  184. telegrinder/tools/magic/descriptors.py +57 -0
  185. telegrinder/tools/magic/function.py +254 -0
  186. telegrinder/tools/magic/inspect.py +16 -0
  187. telegrinder/tools/magic/shortcut.py +107 -0
  188. telegrinder/tools/member_descriptor_proxy.py +95 -0
  189. telegrinder/tools/parse_mode.py +12 -0
  190. telegrinder/tools/serialization/__init__.py +5 -0
  191. telegrinder/tools/serialization/abc.py +34 -0
  192. telegrinder/tools/serialization/json_ser.py +60 -0
  193. telegrinder/tools/serialization/msgpack_ser.py +197 -0
  194. telegrinder/tools/serialization/utils.py +18 -0
  195. telegrinder/tools/singleton/__init__.py +4 -0
  196. telegrinder/tools/singleton/abc.py +14 -0
  197. telegrinder/tools/singleton/singleton.py +18 -0
  198. telegrinder/tools/state_mutator/__init__.py +4 -0
  199. telegrinder/tools/state_mutator/mutation.py +85 -0
  200. telegrinder/tools/state_storage/__init__.py +4 -0
  201. telegrinder/tools/state_storage/abc.py +38 -0
  202. telegrinder/tools/state_storage/memory.py +27 -0
  203. telegrinder/tools/strings.py +22 -0
  204. telegrinder/types/__init__.py +323 -0
  205. telegrinder/types/enums.py +754 -0
  206. telegrinder/types/input_file.py +51 -0
  207. telegrinder/types/methods.py +6143 -0
  208. telegrinder/types/methods_utils.py +66 -0
  209. telegrinder/types/objects.py +8184 -0
  210. telegrinder/types/webapp.py +129 -0
  211. telegrinder/verification_utils.py +35 -0
  212. telegrinder-1.0.0rc1.dist-info/METADATA +166 -0
  213. telegrinder-1.0.0rc1.dist-info/RECORD +215 -0
  214. telegrinder-1.0.0rc1.dist-info/WHEEL +4 -0
  215. telegrinder-1.0.0rc1.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,204 @@
1
+ import datetime as dt
2
+ import typing
3
+ from contextlib import contextmanager
4
+
5
+ import kungfu.library
6
+ import msgspec
7
+
8
+ from telegrinder.msgspec_utils.abc import SupportsCast
9
+ from telegrinder.msgspec_utils.custom_types.datetime import datetime, timedelta
10
+ from telegrinder.msgspec_utils.custom_types.enum_meta import BaseEnumMeta
11
+ from telegrinder.msgspec_utils.tools import bundle, fullname, get_origin
12
+
13
+ type Context = dict[str, typing.Any]
14
+ type Order = typing.Literal["deterministic", "sorted"]
15
+ type EncHook[T] = typing.Callable[typing.Concatenate[T, ...], typing.Any]
16
+
17
+
18
+ def to_builtins(
19
+ obj: typing.Any,
20
+ *,
21
+ str_keys: bool = False,
22
+ builtin_types: typing.Iterable[type[typing.Any]] | None = None,
23
+ order: Order | None = None,
24
+ context: Context | None = None,
25
+ ) -> kungfu.library.Result[typing.Any, msgspec.ValidationError]:
26
+ try:
27
+ return kungfu.library.Ok(
28
+ encoder.to_builtins(
29
+ obj,
30
+ str_keys=str_keys,
31
+ builtin_types=builtin_types,
32
+ order=order,
33
+ context=context,
34
+ ),
35
+ )
36
+ except msgspec.ValidationError as error:
37
+ return kungfu.library.Error(error)
38
+
39
+
40
+ class Encoder:
41
+ """Class `Encoder` for `msgspec` module with encode hooks for objects.
42
+
43
+ ```
44
+ from datetime import datetime
45
+
46
+ class MyDatetime(datetime):
47
+ ...
48
+
49
+ encoder = Encoder()
50
+ encoder.enc_hooks[MyDatetime] = lambda d: int(d.timestamp())
51
+
52
+ encoder.enc_hook(MyDatetime.now()) #> 1713354732
53
+ encoder.encode({"digit": Digit.ONE}) #> '{"digit":1}'
54
+ ```
55
+ """
56
+
57
+ cast_types: dict[type[typing.Any], type[SupportsCast]]
58
+ enc_hooks: dict[typing.Any, EncHook[typing.Any]]
59
+ abstract_enc_hooks: dict[typing.Any, EncHook[typing.Any]]
60
+
61
+ def __init__(self) -> None:
62
+ self.cast_types = { # type: ignore
63
+ dt.datetime: datetime,
64
+ dt.timedelta: timedelta,
65
+ }
66
+ self.enc_hooks = {
67
+ kungfu.library.Some: lambda some: some.value,
68
+ kungfu.library.Nothing: lambda _: None,
69
+ kungfu.library.Sum: lambda s: s.v,
70
+ datetime: lambda date: int(date.timestamp()),
71
+ timedelta: lambda time: time.total_seconds(),
72
+ }
73
+ self.abstract_enc_hooks = {
74
+ BaseEnumMeta: lambda enum_member: enum_member.value,
75
+ }
76
+
77
+ def __repr__(self) -> str:
78
+ return "<{}: cast_types={!r}, enc_hooks={!r}, abstract_enc_hooks={!r}>".format(
79
+ type(self).__name__,
80
+ self.cast_types,
81
+ self.enc_hooks,
82
+ self.abstract_enc_hooks,
83
+ )
84
+
85
+ @contextmanager
86
+ def __call__(
87
+ self,
88
+ *,
89
+ decimal_format: typing.Literal["string", "number"] = "string",
90
+ uuid_format: typing.Literal["canonical", "hex"] = "canonical",
91
+ order: Order | None = None,
92
+ context: Context | None = None,
93
+ ) -> typing.Generator[msgspec.json.Encoder, typing.Any, None]:
94
+ """Context manager returns the `msgspec.json.Encoder` object with passed the `enc_hook`."""
95
+ yield msgspec.json.Encoder(
96
+ enc_hook=self.enc_hook(context),
97
+ decimal_format=decimal_format,
98
+ uuid_format=uuid_format,
99
+ order=order,
100
+ )
101
+
102
+ def add_enc_hook[T](self, t: type[T], /) -> typing.Callable[[EncHook[T]], EncHook[T]]:
103
+ def decorator(func: EncHook[T], /) -> EncHook[T]:
104
+ encode_hook = self.enc_hooks.setdefault(get_origin(t), func)
105
+ return func if encode_hook is not func else encode_hook
106
+
107
+ return decorator
108
+
109
+ def add_abstract_enc_hook[T](self, abstract_type: type[T], /) -> typing.Callable[[EncHook[T]], EncHook[T]]:
110
+ def decorator(func: EncHook[T], /) -> EncHook[T]:
111
+ return self.abstract_enc_hooks.setdefault(get_origin(abstract_type), func)
112
+
113
+ return decorator
114
+
115
+ def get_abstract_enc_hook(self, subtype: type[typing.Any], /) -> EncHook[typing.Any] | None:
116
+ for abstract, enc_hook in self.abstract_enc_hooks.items():
117
+ if issubclass(subtype, abstract) or issubclass(type(subtype), abstract):
118
+ return enc_hook
119
+
120
+ return None
121
+
122
+ def enc_hook(self, context: Context | None = None, /) -> EncHook[typing.Any]:
123
+ def inner(obj: typing.Any, /) -> typing.Any:
124
+ origin_type = get_origin(obj)
125
+
126
+ if (enc_hook_func := self.enc_hooks.get(origin_type)) is None and (
127
+ enc_hook_func := self.get_abstract_enc_hook(origin_type)
128
+ ) is None:
129
+ raise NotImplementedError(
130
+ f"Not implemented encode hook for object of type `{fullname(origin_type)}`. "
131
+ "You can implement encode hook for this object.",
132
+ )
133
+
134
+ return bundle(enc_hook_func, context or {}, start_idx=1)(obj)
135
+
136
+ return inner
137
+
138
+ @typing.overload
139
+ def encode(
140
+ self,
141
+ obj: typing.Any,
142
+ *,
143
+ order: Order | None = None,
144
+ context: Context | None = None,
145
+ ) -> str: ...
146
+
147
+ @typing.overload
148
+ def encode(
149
+ self,
150
+ obj: typing.Any,
151
+ *,
152
+ as_str: typing.Literal[True],
153
+ order: Order | None = None,
154
+ context: Context | None = None,
155
+ ) -> str: ...
156
+
157
+ @typing.overload
158
+ def encode(
159
+ self,
160
+ obj: typing.Any,
161
+ *,
162
+ as_str: typing.Literal[False],
163
+ order: Order | None = None,
164
+ context: Context | None = None,
165
+ ) -> bytes: ...
166
+
167
+ def encode(
168
+ self,
169
+ obj: typing.Any,
170
+ *,
171
+ as_str: bool = True,
172
+ order: Order | None = None,
173
+ context: Context | None = None,
174
+ ) -> str | bytes:
175
+ buf = msgspec.json.encode(obj, enc_hook=self.enc_hook(context), order=order)
176
+ return buf.decode() if as_str else buf
177
+
178
+ def to_builtins(
179
+ self,
180
+ obj: typing.Any,
181
+ *,
182
+ str_keys: bool = False,
183
+ builtin_types: typing.Iterable[type[typing.Any]] | None = None,
184
+ order: Order | None = None,
185
+ context: Context | None = None,
186
+ ) -> typing.Any:
187
+ return msgspec.to_builtins(
188
+ obj,
189
+ str_keys=str_keys,
190
+ builtin_types=builtin_types,
191
+ enc_hook=self.enc_hook(context),
192
+ order=order,
193
+ )
194
+
195
+ def cast(self, obj: typing.Any, /) -> typing.Any:
196
+ if (caster := self.cast_types.get(get_origin(obj))) is not None:
197
+ return caster.cast(obj)
198
+ return obj
199
+
200
+
201
+ encoder: typing.Final = Encoder()
202
+
203
+
204
+ __all__ = ("Encoder", "encoder", "to_builtins")
@@ -0,0 +1,15 @@
1
+ import typing
2
+
3
+ from telegrinder.msgspec_utils.decoder import decoder
4
+ from telegrinder.msgspec_utils.encoder import encoder
5
+
6
+
7
+ def dumps(o: typing.Any) -> str:
8
+ return encoder.encode(o)
9
+
10
+
11
+ def loads(s: str | bytes) -> typing.Any:
12
+ return decoder.decode(s)
13
+
14
+
15
+ __all__ = ("dumps", "loads")
@@ -0,0 +1,80 @@
1
+ import typing
2
+
3
+ import kungfu.library
4
+ import msgspec
5
+ from kungfu.library.monad.option import NOTHING, Nothing
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, kungfu.library.Sum))
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_none(obj: typing.Any, /) -> typing.TypeIs[Nothing | None]:
39
+ return obj is None or obj is NOTHING
40
+
41
+
42
+ def is_common_type[T](t: T, /) -> typing.TypeGuard[type[T]]:
43
+ if not isinstance(t, type):
44
+ return False
45
+ return t in _COMMON_TYPES or issubclass(t, msgspec.Struct) or hasattr(t, "__dataclass_fields__")
46
+
47
+
48
+ def struct_asdict(
49
+ struct: msgspec.Struct,
50
+ /,
51
+ *,
52
+ exclude_unset: bool = True,
53
+ unset_as_nothing: bool = False,
54
+ ) -> dict[str, typing.Any]:
55
+ return {
56
+ k: v if not unset_as_nothing else NOTHING if v is msgspec.UNSET else v
57
+ for k, v in msgspec.structs.asdict(struct).items()
58
+ if not (exclude_unset and (is_none(v) or v is msgspec.UNSET))
59
+ }
60
+
61
+
62
+ def type_check(obj: typing.Any, t: typing.Any) -> bool:
63
+ return (
64
+ isinstance(obj, t)
65
+ if isinstance(t, type) and issubclass(t, msgspec.Struct)
66
+ else type(obj) in t
67
+ if isinstance(t, tuple)
68
+ else type(obj) is t
69
+ )
70
+
71
+
72
+ __all__ = (
73
+ "get_class_annotations",
74
+ "get_origin",
75
+ "get_type_hints",
76
+ "is_common_type",
77
+ "is_none",
78
+ "struct_asdict",
79
+ "type_check",
80
+ )
@@ -0,0 +1,80 @@
1
+ """`Node` module which implements [nodnod](https://github.com/timoniq/nodnod)
2
+ integration for convenient dependency injection via `nodes`.
3
+
4
+
5
+ ```python
6
+ from telegrinder.node import scalar_node, global_node, per_call
7
+
8
+ @global_node
9
+ @scalar_node
10
+ class Hello:
11
+ @classmethod
12
+ def __compose__(cls) -> str:
13
+ return "Hello"
14
+
15
+ @global_node
16
+ @scalar_node
17
+ class World:
18
+ @classmethod
19
+ def __compose__(cls) -> str:
20
+ return "World"
21
+
22
+ @per_call
23
+ @scalar_node
24
+ class Greeter:
25
+ @classmethod
26
+ def __compose__(cls, hello: Hello, world: World) -> str:
27
+ return f"{hello}, {world}!"
28
+
29
+ @bot.on.message()
30
+ async def hello_world(greeter: Greeter) -> str:
31
+ return greeter
32
+ ```
33
+ """
34
+
35
+ # pyright: reportUnusedImport=false, reportUnsupportedDunderAll=false
36
+
37
+ from nodnod import (
38
+ DataNode,
39
+ Injection,
40
+ Node,
41
+ NodeConstructor,
42
+ ResultNode,
43
+ Scalar,
44
+ case,
45
+ generic_node,
46
+ polymorphic,
47
+ scalar_node,
48
+ )
49
+
50
+ from telegrinder.node.compose import Composable, compose, create_composable, run_agent
51
+ from telegrinder.node.nodes import *
52
+ from telegrinder.node.nodes import __all__ as nodes_all
53
+ from telegrinder.node.scope import GLOBAL, PER_CALL, PER_EVENT, NodeScope, global_node, per_call, per_event
54
+ from telegrinder.node.utils import as_node, is_node
55
+
56
+ __all__ = nodes_all + (
57
+ "GLOBAL",
58
+ "PER_CALL",
59
+ "PER_EVENT",
60
+ "Composable",
61
+ "DataNode",
62
+ "NodeConstructor",
63
+ "NodeScope",
64
+ "Node",
65
+ "ResultNode",
66
+ "Injection",
67
+ "Scalar",
68
+ "as_node",
69
+ "compose",
70
+ "create_composable",
71
+ "run_agent",
72
+ "global_node",
73
+ "is_node",
74
+ "case",
75
+ "generic_node",
76
+ "polymorphic",
77
+ "scalar_node",
78
+ "per_call",
79
+ "per_event",
80
+ )
@@ -0,0 +1,193 @@
1
+ """Compose nodes with specific agent and context.
2
+
3
+ This module provides the `compose` function for executing nodes with dependency injection.
4
+
5
+ Key features:
6
+ - Execute functions as nodes with automatic dependency resolution
7
+ - Work with Node classes and ready-made Composable objects
8
+ - Integration with `Context` for access to API, events, data, and other nodes
9
+
10
+ ```python
11
+ # Executing a function node
12
+ async def process_data(user_id: int, api: API) -> str:
13
+ user = await api.get_chat(user_id)
14
+ return f"Hello, {user.first_name}!"
15
+
16
+ async def handler(context: Context):
17
+ async with compose(process_data, context) as result:
18
+ match result:
19
+ case Ok(value):
20
+ print(value) # "Hello, John!"
21
+ case Error(error):
22
+ print(f"Error: {error}")
23
+
24
+ # Executing a node with additional dependencies
25
+ from telegrinder.node import per_call, scalar_node
26
+
27
+ @per_call
28
+ @scalar_node
29
+ class Database:
30
+ @classmethod
31
+ def __compose__(cls) -> "Database":
32
+ return cls()
33
+
34
+ async def get_user_data(db: Database, user_id: int) -> dict[str, Any]:
35
+ # Use db to fetch data
36
+ return {"id": user_id, "name": "User"}
37
+
38
+ async def handler(context: Context):
39
+ async with compose(get_user_data, context) as result:
40
+ user_data = result.unwrap()
41
+
42
+ # Passing additional root dependencies
43
+ async def handler(context: Context):
44
+ custom_data = {"custom_key": "custom_value"}
45
+
46
+ async with compose(
47
+ my_node,
48
+ context,
49
+ roots={CustomType: custom_data}
50
+ ) as result:
51
+ # roots allows injecting additional dependencies
52
+ pass
53
+ ```
54
+
55
+ The `compose` function returns an async context manager that:
56
+
57
+ - Creates a local scope for node composing
58
+ - Automatically resolves all node dependencies
59
+ - Returns `Result[T, NodeError]` with the composed result
60
+ """
61
+
62
+ import dataclasses
63
+ import typing
64
+ from contextlib import asynccontextmanager
65
+ from functools import lru_cache
66
+
67
+ from kungfu.library.monad.result import Error, Ok, Result
68
+ from nodnod.agent.base import Agent
69
+ from nodnod.agent.event_loop.agent import EventLoopAgent
70
+ from nodnod.error import NodeError
71
+ from nodnod.interface import create_agent_from_node, create_node_from_function, inject_internals
72
+ from nodnod.interface.is_node import is_node
73
+ from nodnod.interface.node_from_function import Externals
74
+ from nodnod.node import Node
75
+ from nodnod.scope import Scope
76
+ from nodnod.utils.is_type import is_type
77
+
78
+ from telegrinder.node.scope import TELEGRINDER_CONTEXT, MappedScopes, NodeScope
79
+ from telegrinder.tools.aio import maybe_awaitable
80
+ from telegrinder.tools.magic.function import Function
81
+
82
+ if typing.TYPE_CHECKING:
83
+ from telegrinder.bot.dispatch.context import Context
84
+
85
+ type _Composable[T: Agent] = Function[..., typing.Any] | type[Node[typing.Any, typing.Any]] | Composable[T]
86
+
87
+ NODE_GLOBAL_SCOPE: typing.Final = TELEGRINDER_CONTEXT.node_global_scope
88
+
89
+
90
+ @lru_cache(maxsize=1024 * 4)
91
+ def create_composable[T: Agent](
92
+ node_or_function: type[Node[typing.Any, typing.Any]] | Function[..., typing.Any],
93
+ /,
94
+ *,
95
+ agent_cls: type[T] = EventLoopAgent,
96
+ ) -> Composable[T]:
97
+ if not isinstance(node_or_function, type):
98
+ node_or_function = create_node_from_function(node_or_function)
99
+ return Composable(node_or_function, create_agent_from_node(node_or_function, agent_cls=agent_cls))
100
+
101
+
102
+ @asynccontextmanager
103
+ async def run_agent(
104
+ agent: Agent,
105
+ context: Context,
106
+ *,
107
+ roots: dict[type[typing.Any], typing.Any] | None = None,
108
+ per_event_scope: Scope | None = None,
109
+ ) -> typing.AsyncGenerator[Result[Scope, NodeError]]:
110
+ event_scope = per_event_scope if per_event_scope is not None else context.per_event_scope
111
+ mapped_scopes = MappedScopes(global_scope=NODE_GLOBAL_SCOPE, per_event_scope=event_scope)
112
+ internals = {type(context): context, Externals: context}
113
+
114
+ async with event_scope.create_child(detail=NodeScope.PER_CALL) as local_scope:
115
+ try:
116
+ inject_internals(local_scope, internals | (roots or {}))
117
+ await maybe_awaitable(agent.run(local_scope, mapped_scopes))
118
+ yield Ok(local_scope.merge())
119
+ except NodeError as error:
120
+ yield Error(error)
121
+
122
+
123
+ @typing.overload
124
+ def compose[R](
125
+ function: Function[..., R],
126
+ /,
127
+ context: Context,
128
+ *,
129
+ per_event_scope: Scope | None = None,
130
+ agent_cls: type[Agent] | None = None,
131
+ roots: dict[type[typing.Any], typing.Any] | None = None,
132
+ ) -> typing.AsyncContextManager[Result[R, NodeError], None]: ...
133
+
134
+
135
+ @typing.overload
136
+ def compose[T: Agent](
137
+ composable: Composable[T],
138
+ /,
139
+ context: Context,
140
+ *,
141
+ per_event_scope: Scope | None = None,
142
+ roots: dict[type[typing.Any], typing.Any] | None = None,
143
+ ) -> typing.AsyncContextManager[Result[typing.Any, NodeError], None]: ...
144
+
145
+
146
+ @typing.overload
147
+ def compose[T](
148
+ node: type[Node[T, typing.Any]],
149
+ /,
150
+ context: Context,
151
+ *,
152
+ agent: Agent,
153
+ per_event_scope: Scope | None = None,
154
+ roots: dict[type[typing.Any], typing.Any] | None = None,
155
+ ) -> typing.AsyncContextManager[Result[T, NodeError], None]: ...
156
+
157
+
158
+ @asynccontextmanager
159
+ async def compose[T = typing.Any](
160
+ node: _Composable[Agent],
161
+ context: Context,
162
+ *,
163
+ per_event_scope: Scope | None = None,
164
+ agent: Agent | None = None,
165
+ agent_cls: type[Agent] | None = None,
166
+ roots: dict[type[typing.Any], typing.Any] | None = None,
167
+ ) -> typing.AsyncGenerator[Result[T, NodeError], None]:
168
+ composable = None
169
+
170
+ if isinstance(node, Composable):
171
+ composable = node
172
+ elif not is_node(node):
173
+ composable = create_composable(node, agent_cls=agent_cls or EventLoopAgent)
174
+
175
+ if composable is not None:
176
+ node, agent = composable.node, typing.cast("Agent", composable.agent)
177
+ elif not is_type(node, Node):
178
+ raise TypeError("Compose requires function, node, or composable.")
179
+
180
+ if agent is None:
181
+ raise ValueError("Agent is required.")
182
+
183
+ async with run_agent(agent, context, roots=roots, per_event_scope=per_event_scope) as result:
184
+ yield result.map(lambda scope: scope[node].value)
185
+
186
+
187
+ @dataclasses.dataclass(frozen=True, slots=True)
188
+ class Composable[T: Agent = Agent]:
189
+ node: type[Node[typing.Any, typing.Any]]
190
+ agent: T
191
+
192
+
193
+ __all__ = ("Composable", "compose", "create_composable", "run_agent")
@@ -0,0 +1,96 @@
1
+ """Built-in nodes."""
2
+
3
+ from telegrinder.node.nodes.attachment import (
4
+ Animation,
5
+ Attachment,
6
+ Audio,
7
+ Document,
8
+ MediaGroup,
9
+ Photo,
10
+ Poll,
11
+ SuccessfulPayment,
12
+ Video,
13
+ VideoNote,
14
+ Voice,
15
+ )
16
+ from telegrinder.node.nodes.callback_query import CallbackQueryData, CallbackQueryDataJson
17
+ from telegrinder.node.nodes.channel import (
18
+ Channel,
19
+ ChannelId,
20
+ ChannelPost,
21
+ ChannelPostId,
22
+ ChatMessageChannelPost,
23
+ ChatMessageChannelPostAuthor,
24
+ ChatMessageChannelPostChannel,
25
+ ChatMessageChannelPostChannelId,
26
+ ChatMessageChannelPostId,
27
+ )
28
+ from telegrinder.node.nodes.command import CommandInfo
29
+ from telegrinder.node.nodes.error import Error
30
+ from telegrinder.node.nodes.event import EventNode
31
+ from telegrinder.node.nodes.file import File, FileId
32
+ from telegrinder.node.nodes.global_node import GlobalNode
33
+ from telegrinder.node.nodes.i18n import ABCTranslator, BaseTranslator, I18NConfig, KeySeparator
34
+ from telegrinder.node.nodes.me import BotUsername, Me
35
+ from telegrinder.node.nodes.message_entities import MessageEntities
36
+ from telegrinder.node.nodes.payload import Payload, PayloadData, PayloadSerializer
37
+ from telegrinder.node.nodes.reply_message import ReplyMessage
38
+ from telegrinder.node.nodes.source import ChatId, ChatSource, Locale, Source, UserId, UserSource
39
+ from telegrinder.node.nodes.state_mutator import State, StateMutator
40
+ from telegrinder.node.nodes.text import Caption, HTMLCaption, HTMLText, Text, TextInteger, TextLiteral
41
+
42
+ __all__ = (
43
+ "ABCTranslator",
44
+ "Animation",
45
+ "Attachment",
46
+ "Audio",
47
+ "BaseTranslator",
48
+ "BotUsername",
49
+ "CallbackQueryData",
50
+ "CallbackQueryDataJson",
51
+ "Caption",
52
+ "Channel",
53
+ "ChannelId",
54
+ "ChannelPost",
55
+ "ChannelPostId",
56
+ "ChatId",
57
+ "ChatMessageChannelPost",
58
+ "ChatMessageChannelPostAuthor",
59
+ "ChatMessageChannelPostChannel",
60
+ "ChatMessageChannelPostChannelId",
61
+ "ChatMessageChannelPostId",
62
+ "ChatSource",
63
+ "CommandInfo",
64
+ "Document",
65
+ "Error",
66
+ "EventNode",
67
+ "File",
68
+ "FileId",
69
+ "GlobalNode",
70
+ "HTMLCaption",
71
+ "HTMLText",
72
+ "I18NConfig",
73
+ "KeySeparator",
74
+ "Locale",
75
+ "Me",
76
+ "MediaGroup",
77
+ "MessageEntities",
78
+ "Payload",
79
+ "PayloadData",
80
+ "PayloadSerializer",
81
+ "Photo",
82
+ "Poll",
83
+ "ReplyMessage",
84
+ "Source",
85
+ "State",
86
+ "StateMutator",
87
+ "SuccessfulPayment",
88
+ "Text",
89
+ "TextInteger",
90
+ "TextLiteral",
91
+ "UserId",
92
+ "UserSource",
93
+ "Video",
94
+ "VideoNote",
95
+ "Voice",
96
+ )