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,84 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from kungfu.library.monad.result import Error, Ok
5
+ from nodnod.error import NodeError
6
+ from nodnod.interface.generic import generic_node
7
+ from nodnod.interface.polymorphic import case, polymorphic
8
+ from nodnod.interface.scalar import scalar_node
9
+
10
+ from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
11
+ from telegrinder.bot.cute_types.message import MessageCute
12
+ from telegrinder.bot.cute_types.pre_checkout_query import PreCheckoutQueryCute
13
+ from telegrinder.bot.cute_types.shipping_query import ShippingQueryCute
14
+ from telegrinder.node.nodes.global_node import GlobalNode
15
+ from telegrinder.tools.serialization.abc import ABCDataSerializer
16
+ from telegrinder.tools.serialization.json_ser import JSONSerializer
17
+
18
+ type _UndefinedSerializer = typing.Any
19
+
20
+
21
+ @scalar_node
22
+ @polymorphic[str]
23
+ class Payload:
24
+ @case
25
+ def compose_callback_query(cls, event: CallbackQueryCute) -> str:
26
+ return event.data.expect(NodeError("CallbackQuery has no data."))
27
+
28
+ @case
29
+ def compose_pre_checkout_query(cls, event: PreCheckoutQueryCute) -> str:
30
+ return event.invoice_payload
31
+
32
+ @case
33
+ def compose_shipping_query(cls, event: ShippingQueryCute) -> str:
34
+ return event.invoice_payload
35
+
36
+ @case
37
+ def compose_message(cls, event: MessageCute) -> str:
38
+ return event.successful_payment.map(
39
+ lambda payment: payment.invoice_payload,
40
+ ).expect(NodeError("Message has no successful payment."))
41
+
42
+
43
+ @dataclasses.dataclass(frozen=True)
44
+ class PayloadSerializer[T: type[ABCDataSerializer] = typing.Any](GlobalNode[T]):
45
+ serializer: type[ABCDataSerializer[typing.Any]]
46
+
47
+ @classmethod
48
+ def __compose__(cls) -> typing.Self:
49
+ return cls(serializer=JSONSerializer)
50
+
51
+
52
+ @generic_node
53
+ class _PayloadData[Data, Serializer: ABCDataSerializer = _UndefinedSerializer]:
54
+ @classmethod
55
+ def __compose__(
56
+ cls,
57
+ payload: Payload,
58
+ data: type[Data],
59
+ payload_serializer: type[Serializer],
60
+ global_serializer: PayloadSerializer,
61
+ ) -> typing.Any:
62
+ if payload_serializer is _UndefinedSerializer:
63
+ serializer = getattr(data, "__serializer__", global_serializer.serializer)
64
+ else:
65
+ serializer = payload_serializer
66
+
67
+ match serializer(data).deserialize(payload):
68
+ case Ok(value):
69
+ return value
70
+ case Error(err):
71
+ raise NodeError(err)
72
+
73
+
74
+ if typing.TYPE_CHECKING:
75
+ type PayloadData[
76
+ DataType = typing.Any,
77
+ Serializer: ABCDataSerializer = _UndefinedSerializer,
78
+ ] = typing.Annotated[DataType, Serializer]
79
+
80
+ else:
81
+ PayloadData = _PayloadData
82
+
83
+
84
+ __all__ = ("Payload", "PayloadData", "PayloadSerializer")
@@ -0,0 +1,14 @@
1
+ from nodnod.error import NodeError
2
+ from nodnod.interface.scalar import scalar_node
3
+
4
+ from telegrinder.bot.cute_types.message import MessageCute
5
+
6
+
7
+ @scalar_node
8
+ class ReplyMessage:
9
+ @classmethod
10
+ def __compose__(cls, message: MessageCute) -> MessageCute:
11
+ return message.reply_to_message.expect(NodeError("Message doesn't have reply"))
12
+
13
+
14
+ __all__ = ("ReplyMessage",)
@@ -0,0 +1,172 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from kungfu.library.monad.option import Nothing, Option, Some
5
+ from nodnod.error import NodeError
6
+ from nodnod.interface.polymorphic import case, polymorphic
7
+ from nodnod.interface.scalar import scalar_node
8
+
9
+ from telegrinder.api.api import API
10
+ from telegrinder.bot.cute_types import (
11
+ CallbackQueryCute,
12
+ ChatJoinRequestCute,
13
+ ChatMemberUpdatedCute,
14
+ ChosenInlineResultCute,
15
+ InlineQueryCute,
16
+ MessageCute,
17
+ MessageReactionUpdatedCute,
18
+ PaidMediaPurchasedCute,
19
+ PollAnswerCute,
20
+ PreCheckoutQueryCute,
21
+ ShippingQueryCute,
22
+ )
23
+ from telegrinder.types.objects import Chat, Message, User
24
+
25
+
26
+ @scalar_node
27
+ @polymorphic["Source"]
28
+ @dataclasses.dataclass(kw_only=True)
29
+ class Source:
30
+ api: API
31
+ from_user: User
32
+ chat: Option[Chat] = dataclasses.field(default_factory=Nothing)
33
+ thread_id: Option[int] = dataclasses.field(default_factory=Nothing)
34
+
35
+ @case
36
+ def compose_message(cls, message: MessageCute) -> typing.Self:
37
+ return cls(
38
+ api=message.api,
39
+ from_user=message.from_.expect(NodeError("Message is from a channel.")),
40
+ chat=Some(message.chat),
41
+ thread_id=message.message_thread_id,
42
+ )
43
+
44
+ @case
45
+ def compose_callback_query(cls, callback_query: CallbackQueryCute) -> typing.Self:
46
+ return cls(
47
+ api=callback_query.api,
48
+ from_user=callback_query.from_user,
49
+ chat=callback_query.chat,
50
+ thread_id=callback_query.message_thread_id,
51
+ )
52
+
53
+ @case
54
+ def compose_inline_query(cls, inline_query: InlineQueryCute) -> typing.Self:
55
+ return cls(
56
+ api=inline_query.api,
57
+ from_user=inline_query.from_user,
58
+ )
59
+
60
+ @case
61
+ def compose_chosen_inline_result(cls, chosen_inline_result: ChosenInlineResultCute) -> typing.Self:
62
+ return cls(
63
+ api=chosen_inline_result.api,
64
+ from_user=chosen_inline_result.from_user,
65
+ )
66
+
67
+ @case
68
+ def compose_chat_member_updated(cls, chat_member_updated: ChatMemberUpdatedCute) -> typing.Self:
69
+ return cls(
70
+ api=chat_member_updated.api,
71
+ from_user=chat_member_updated.from_user,
72
+ chat=Some(chat_member_updated.chat),
73
+ )
74
+
75
+ @case
76
+ def compose_chat_join_request(cls, chat_join_request: ChatJoinRequestCute) -> typing.Self:
77
+ return cls(
78
+ api=chat_join_request.api,
79
+ from_user=chat_join_request.from_user,
80
+ chat=Some(chat_join_request.chat),
81
+ )
82
+
83
+ @case
84
+ def compose_pre_checkout_query(cls, pre_checkout_query: PreCheckoutQueryCute) -> typing.Self:
85
+ return cls(
86
+ api=pre_checkout_query.api,
87
+ from_user=pre_checkout_query.from_user,
88
+ )
89
+
90
+ @case
91
+ def compose_message_reaction_updated(cls, message_reaction_updated: MessageReactionUpdatedCute) -> typing.Self:
92
+ return cls(
93
+ api=message_reaction_updated.api,
94
+ from_user=message_reaction_updated.user.expect(NodeError("Message reaction is from an anonymous user.")),
95
+ chat=Some(message_reaction_updated.chat),
96
+ )
97
+
98
+ @case
99
+ def compose_paid_media_purchased(cls, paid_media_purchased: PaidMediaPurchasedCute) -> typing.Self:
100
+ return cls(
101
+ api=paid_media_purchased.api,
102
+ from_user=paid_media_purchased.from_user,
103
+ )
104
+
105
+ @case
106
+ def compose_poll_answer(cls, poll_answer: PollAnswerCute) -> typing.Self:
107
+ return cls(
108
+ api=poll_answer.api,
109
+ from_user=poll_answer.user.expect(NodeError("Poll answer is from an anonymous user.")),
110
+ chat=poll_answer.voter_chat,
111
+ )
112
+
113
+ @case
114
+ def compose_shipping_query(cls, shipping_query: ShippingQueryCute) -> typing.Self:
115
+ return cls(
116
+ api=shipping_query.api,
117
+ from_user=shipping_query.from_user,
118
+ )
119
+
120
+ async def send(self, text: str, **kwargs: typing.Any) -> Message:
121
+ result = await self.api.send_message(
122
+ chat_id=self.chat.map_or(self.from_user.id, lambda chat: chat.id).unwrap(),
123
+ message_thread_id=self.thread_id.unwrap_or_none(),
124
+ text=text,
125
+ **kwargs,
126
+ )
127
+ return result.unwrap()
128
+
129
+
130
+ @scalar_node
131
+ class ChatSource:
132
+ @classmethod
133
+ def __compose__(cls, source: Source) -> Chat:
134
+ return source.chat.expect(NodeError("Source has no chat."))
135
+
136
+
137
+ @scalar_node
138
+ class UserSource:
139
+ @classmethod
140
+ def __compose__(cls, source: Source) -> User:
141
+ return source.from_user
142
+
143
+
144
+ @scalar_node
145
+ class ChatId:
146
+ @classmethod
147
+ def __compose__(cls, chat: ChatSource) -> int:
148
+ return chat.id
149
+
150
+
151
+ @scalar_node
152
+ class UserId:
153
+ @classmethod
154
+ def __compose__(cls, user: UserSource) -> int:
155
+ return user.id
156
+
157
+
158
+ @scalar_node
159
+ class Locale:
160
+ @classmethod
161
+ def __compose__(cls, user: UserSource) -> str:
162
+ return user.language_code.expect(NodeError("User has no language code."))
163
+
164
+
165
+ __all__ = (
166
+ "ChatId",
167
+ "ChatSource",
168
+ "Locale",
169
+ "Source",
170
+ "UserId",
171
+ "UserSource",
172
+ )
@@ -0,0 +1,71 @@
1
+ import typing
2
+
3
+ from kungfu.library.monad.option import Some
4
+ from nodnod.error import NodeError
5
+ from nodnod.node import Node
6
+
7
+ from telegrinder.node.nodes.payload import PayloadSerializer
8
+ from telegrinder.node.nodes.source import Source
9
+ from telegrinder.tools.fullname import fullname
10
+ from telegrinder.tools.serialization import ABCDataSerializer
11
+ from telegrinder.tools.state_storage.memory import ABCStateStorage, MemoryStateStorage
12
+
13
+
14
+ class StateMutator(Node):
15
+ STORAGE = MemoryStateStorage[str]() # TODO: use nodnod injection to get storage inside StateMutator.compose
16
+ KEY_MAP: dict[str, type[State]] = {}
17
+
18
+ def __init__(
19
+ self,
20
+ storage: ABCStateStorage[str],
21
+ user_id: int,
22
+ serializer: type[ABCDataSerializer[State]],
23
+ ) -> None:
24
+ self.storage = storage
25
+ self.user_id = user_id
26
+ self.serializer = serializer
27
+
28
+ async def get(self) -> State | None:
29
+ match await self.storage.get(self.user_id):
30
+ case Some(state_data) if state_data.key in self.KEY_MAP:
31
+ return (
32
+ self.serializer(self.KEY_MAP[state_data.key])
33
+ .deserialize(state_data.payload)
34
+ .map(lambda state: state.bind(self))
35
+ .unwrap_or_none()
36
+ )
37
+
38
+ return None
39
+
40
+ async def set(self, state: State) -> None:
41
+ state_cls = state.__class__
42
+ key = fullname(state_cls)
43
+ payload = self.serializer(state_cls).serialize(state)
44
+ await self.storage.set(self.user_id, key, payload)
45
+ self.KEY_MAP[key] = state_cls
46
+
47
+ @classmethod
48
+ def __compose__(cls, src: Source, serializer: PayloadSerializer) -> typing.Self:
49
+ return cls(cls.STORAGE, src.from_user.id, serializer.serializer)
50
+
51
+
52
+ class State:
53
+ def bind(self, mutator: StateMutator) -> typing.Self:
54
+ self.__mutator__ = mutator
55
+ return self
56
+
57
+ async def enter(self) -> None:
58
+ await self.__mutator__.set(state=self)
59
+
60
+ async def exit(self) -> None:
61
+ pass
62
+
63
+ @classmethod
64
+ async def __compose__(cls, mutator: StateMutator) -> typing.Self:
65
+ current_state = await mutator.get()
66
+ if current_state is None or not isinstance(current_state, cls):
67
+ raise NodeError("State mismatch.")
68
+ return current_state
69
+
70
+
71
+ __all__ = ("State", "StateMutator")
@@ -0,0 +1,62 @@
1
+ import typing
2
+
3
+ from nodnod.error import NodeError
4
+ from nodnod.interface.node_constructor import NodeConstructor
5
+ from nodnod.interface.scalar import scalar_node
6
+
7
+ from telegrinder.bot.cute_types.message import MessageCute
8
+
9
+
10
+ @scalar_node
11
+ class Caption:
12
+ @classmethod
13
+ def __compose__(cls, message: MessageCute) -> str:
14
+ return message.caption.expect(NodeError("Message has no caption."))
15
+
16
+
17
+ @scalar_node
18
+ class HTMLCaption:
19
+ @classmethod
20
+ def __compose__(cls, message: MessageCute) -> str:
21
+ return message.html_caption.expect(NodeError("Message has no HTML caption."))
22
+
23
+
24
+ @scalar_node
25
+ class Text:
26
+ @classmethod
27
+ def __compose__(cls, message: MessageCute) -> str:
28
+ return message.text.expect(NodeError("Message has no text."))
29
+
30
+
31
+ @scalar_node
32
+ class HTMLText:
33
+ @classmethod
34
+ def __compose__(cls, message: MessageCute) -> str:
35
+ return message.html_text.expect(NodeError("Message has no HTML text."))
36
+
37
+
38
+ @scalar_node
39
+ class TextInteger:
40
+ @classmethod
41
+ def __compose__(cls, text: Text | Caption) -> int:
42
+ if not text.isdigit():
43
+ raise NodeError("Text is not digit.")
44
+ return int(text)
45
+
46
+
47
+ if typing.TYPE_CHECKING:
48
+ from typing import Literal as TextLiteral
49
+
50
+ else:
51
+
52
+ class TextLiteral(NodeConstructor):
53
+ def __init__(self, *texts: str) -> None:
54
+ self.texts = texts
55
+
56
+ def __compose__(self, text: Text | Caption) -> str:
57
+ if text in self.texts:
58
+ return text
59
+ raise NodeError("Text mismatched literal.")
60
+
61
+
62
+ __all__ = ("Caption", "HTMLCaption", "HTMLText", "Text", "TextInteger", "TextLiteral")
@@ -0,0 +1,88 @@
1
+ """Specific `scopes` for node scope system.
2
+
3
+ Scopes:
4
+ - `GLOBAL`: compose only once during runtime, and later be stored and reused when needed ~ `@global_node`
5
+ - `PER_EVENT`: compose once per event, so if during the composition the node was already composed, it will be reused and won't be composed twice ~ `@per_event`
6
+ - `PER_CALL`: compose each time any node will require it to build itself or if we require it to be delivered into the handler ~ `@per_call`
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import enum
12
+ import typing
13
+
14
+ from nodnod.node import Node
15
+ from nodnod.scope import Scope
16
+
17
+ from telegrinder.modules import logger
18
+ from telegrinder.node.utils import as_node
19
+ from telegrinder.tools.global_context.builtin_context import TelegrinderContext
20
+
21
+ if typing.TYPE_CHECKING:
22
+ from telegrinder.node.scope import NodeScope
23
+
24
+ # Import members from NodeScope
25
+ GLOBAL, PER_CALL, PER_EVENT = NodeScope.GLOBAL, NodeScope.PER_CALL, NodeScope.PER_EVENT
26
+
27
+ TELEGRINDER_CONTEXT: typing.Final = TelegrinderContext()
28
+
29
+
30
+ @TELEGRINDER_CONTEXT.loop_wrapper.lifespan.on_shutdown
31
+ async def close_node_global_scope() -> None:
32
+ logger.debug("Closing node global scope")
33
+
34
+ try:
35
+ await TELEGRINDER_CONTEXT.close_global_scope()
36
+ except Exception as error:
37
+ logger.error("While closing node global scope, an error occurred: {!r}", error)
38
+
39
+
40
+ class NodeScopeInfo(dict[type[Node], "NodeScope"]):
41
+ def set_node_scope[T: Node[typing.Any, typing.Any]](self, node: type[T], scope: NodeScope, /) -> None:
42
+ self[node] = scope
43
+
44
+ def get_node_scope[T: Node[typing.Any, typing.Any]](self, node: type[T], /) -> NodeScope | None:
45
+ return self.get(node, None)
46
+
47
+
48
+ NODE_SCOPE_INFO: typing.Final = NodeScopeInfo()
49
+
50
+
51
+ class MappedScopes(dict[type[Node], Scope]):
52
+ scopes: dict[NodeScope, Scope]
53
+
54
+ def __init__(self, global_scope: Scope, per_event_scope: Scope) -> None:
55
+ self.scopes = {NodeScope.GLOBAL: global_scope, NodeScope.PER_EVENT: per_event_scope}
56
+
57
+ def get(self, key: type[Node], default: Scope | None = None) -> Scope | None:
58
+ scope = NODE_SCOPE_INFO.get_node_scope(key) or NodeScope.PER_EVENT
59
+ return self.scopes.get(scope, default)
60
+
61
+
62
+ # Declare NodeScope members in a global scope
63
+ @enum.global_enum
64
+ class NodeScope(enum.StrEnum):
65
+ GLOBAL = "global"
66
+ PER_CALL = "local"
67
+ PER_EVENT = "event"
68
+
69
+ def __call__[T](self, node: type[T], /) -> type[T]:
70
+ NODE_SCOPE_INFO.set_node_scope(as_node(node), self)
71
+ return node
72
+
73
+
74
+ # Decorators
75
+ global_node, per_call, per_event = GLOBAL, PER_CALL, PER_EVENT
76
+
77
+
78
+ __all__ = (
79
+ "GLOBAL",
80
+ "NODE_SCOPE_INFO",
81
+ "PER_CALL",
82
+ "PER_EVENT",
83
+ "MappedScopes",
84
+ "NodeScope",
85
+ "global_node",
86
+ "per_call",
87
+ "per_event",
88
+ )
@@ -0,0 +1,38 @@
1
+ import sys
2
+ import typing
3
+
4
+ from nodnod.interface.is_node import is_node
5
+ from nodnod.node import Node
6
+
7
+ from telegrinder.tools.fullname import fullname
8
+
9
+
10
+ def as_node(obj: typing.Any, /) -> type[Node[typing.Any, typing.Any]]:
11
+ if not is_node(obj):
12
+ raise TypeError(f"Object `{fullname(obj)}` is not a node.")
13
+ return obj
14
+
15
+
16
+ def get_locals_from_function(func: typing.Callable[..., typing.Any], /) -> typing.Mapping[str, typing.Any] | None:
17
+ bound_class = func.__self__ if hasattr(func, "__self__") else None
18
+ bound_class = func.__objclass__ if bound_class is None and hasattr(func, "__objclass__") else None
19
+
20
+ if bound_class is not None:
21
+ return (bound_class if isinstance(bound_class, type) else type(bound_class)).__dict__
22
+
23
+ return None
24
+
25
+
26
+ def get_globals_from_function(func: typing.Callable[..., typing.Any], /) -> dict[str, typing.Any]:
27
+ if hasattr(func, "__globals__"):
28
+ return getattr(func, "__globals__")
29
+
30
+ module = getattr(func, "__module__", type(func).__module__)
31
+
32
+ if module in sys.modules:
33
+ return vars(sys.modules[module])
34
+
35
+ return {}
36
+
37
+
38
+ __all__ = ("as_node", "get_globals_from_function", "get_locals_from_function", "is_node")
telegrinder/py.typed ADDED
File without changes
telegrinder/rules.py ADDED
@@ -0,0 +1 @@
1
+ from telegrinder.bot.rules import *