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,169 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from kungfu.library.monad.option import NOTHING, Nothing, Option
5
+ from nodnod.error import NodeError
6
+ from nodnod.interface.scalar import scalar_node
7
+ from nodnod.node import Node
8
+
9
+ import telegrinder.types
10
+ from telegrinder.bot.cute_types.message import MessageCute
11
+
12
+ type AttachmentType = typing.Literal[
13
+ "animation",
14
+ "audio",
15
+ "document",
16
+ "photo",
17
+ "poll",
18
+ "video",
19
+ "video_note",
20
+ "voice",
21
+ "successful_payment",
22
+ ]
23
+
24
+ ATTACHMENT_TYPES: typing.Final[tuple[AttachmentType, ...]] = typing.get_args(AttachmentType.__value__)
25
+
26
+
27
+ @dataclasses.dataclass
28
+ class Attachment(Node):
29
+ attachment_type: AttachmentType
30
+
31
+ animation: Option[telegrinder.types.Animation] = dataclasses.field(
32
+ default_factory=Nothing,
33
+ kw_only=True,
34
+ )
35
+ audio: Option[telegrinder.types.Audio] = dataclasses.field(
36
+ default_factory=Nothing,
37
+ kw_only=True,
38
+ )
39
+ document: Option[telegrinder.types.Document] = dataclasses.field(
40
+ default_factory=Nothing,
41
+ kw_only=True,
42
+ )
43
+ photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
44
+ default_factory=Nothing,
45
+ kw_only=True,
46
+ )
47
+ poll: Option[telegrinder.types.Poll] = dataclasses.field(
48
+ default_factory=Nothing,
49
+ kw_only=True,
50
+ )
51
+ voice: Option[telegrinder.types.Voice] = dataclasses.field(
52
+ default_factory=Nothing,
53
+ kw_only=True,
54
+ )
55
+ video: Option[telegrinder.types.Video] = dataclasses.field(
56
+ default_factory=Nothing,
57
+ kw_only=True,
58
+ )
59
+ video_note: Option[telegrinder.types.VideoNote] = dataclasses.field(
60
+ default_factory=Nothing,
61
+ kw_only=True,
62
+ )
63
+ successful_payment: Option[telegrinder.types.SuccessfulPayment] = dataclasses.field(
64
+ default_factory=Nothing,
65
+ kw_only=True,
66
+ )
67
+
68
+ @classmethod
69
+ def __compose__(cls, message: MessageCute) -> typing.Self:
70
+ for attachment_type in ATTACHMENT_TYPES:
71
+ attachment = getattr(message, attachment_type, NOTHING)
72
+
73
+ if attachment:
74
+ return cls(attachment_type, **{attachment_type: attachment})
75
+
76
+ raise NodeError("No attachment found in message.")
77
+
78
+
79
+ @dataclasses.dataclass
80
+ class Photo(Node):
81
+ sizes: list[telegrinder.types.PhotoSize]
82
+
83
+ @classmethod
84
+ def __compose__(cls, attachment: Attachment) -> typing.Self:
85
+ return cls(attachment.photo.expect(NodeError("Attachment is not a photo.")))
86
+
87
+
88
+ @scalar_node
89
+ class Video:
90
+ @classmethod
91
+ def __compose__(cls, attachment: Attachment) -> telegrinder.types.Video:
92
+ return attachment.video.expect(NodeError("Attachment is not a video."))
93
+
94
+
95
+ @scalar_node
96
+ class VideoNote:
97
+ @classmethod
98
+ def __compose__(cls, attachment: Attachment) -> telegrinder.types.VideoNote:
99
+ return attachment.video_note.expect(NodeError("Attachment is not a video note."))
100
+
101
+
102
+ @scalar_node
103
+ class Audio:
104
+ @classmethod
105
+ def __compose__(cls, attachment: Attachment) -> telegrinder.types.Audio:
106
+ return attachment.audio.expect(NodeError("Attachment is not an audio."))
107
+
108
+
109
+ @scalar_node
110
+ class Animation:
111
+ @classmethod
112
+ def __compose__(cls, attachment: Attachment) -> telegrinder.types.Animation:
113
+ return attachment.animation.expect(NodeError("Attachment is not an animation."))
114
+
115
+
116
+ @scalar_node
117
+ class Voice:
118
+ @classmethod
119
+ def __compose__(cls, attachment: Attachment) -> telegrinder.types.Voice:
120
+ return attachment.voice.expect(NodeError("Attachment is not a voice."))
121
+
122
+
123
+ @scalar_node
124
+ class Document:
125
+ @classmethod
126
+ def __compose__(cls, attachment: Attachment) -> telegrinder.types.Document:
127
+ return attachment.document.expect(NodeError("Attachment is not a document."))
128
+
129
+
130
+ @scalar_node
131
+ class Poll:
132
+ @classmethod
133
+ def __compose__(cls, attachment: Attachment) -> telegrinder.types.Poll:
134
+ return attachment.poll.expect(NodeError("Attachment is not a poll."))
135
+
136
+
137
+ @scalar_node
138
+ class SuccessfulPayment:
139
+ @classmethod
140
+ def __compose__(cls, attachment: Attachment) -> telegrinder.types.SuccessfulPayment:
141
+ return attachment.successful_payment.expect(NodeError("Attachment is not a successful payment."))
142
+
143
+
144
+ @dataclasses.dataclass
145
+ class MediaGroup(Node):
146
+ id: str
147
+ items: list[MessageCute]
148
+
149
+ @classmethod
150
+ def __compose__(cls, message: MessageCute) -> typing.Self:
151
+ return cls(
152
+ id=message.media_group_id.expect(NodeError("No media group id.")),
153
+ items=message.media_group_messages.expect(NodeError("No messages collected for media group.")),
154
+ )
155
+
156
+
157
+ __all__ = (
158
+ "Animation",
159
+ "Attachment",
160
+ "Audio",
161
+ "Document",
162
+ "MediaGroup",
163
+ "Photo",
164
+ "Poll",
165
+ "SuccessfulPayment",
166
+ "Video",
167
+ "VideoNote",
168
+ "Voice",
169
+ )
@@ -0,0 +1,25 @@
1
+ import typing
2
+
3
+ from nodnod.error import NodeError
4
+ from nodnod.interface.scalar import scalar_node
5
+
6
+ from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
7
+
8
+
9
+ @scalar_node
10
+ class CallbackQueryData:
11
+ @classmethod
12
+ def __compose__(cls, callback_query: CallbackQueryCute) -> str:
13
+ return callback_query.data.expect(NodeError("Cannot complete decode callback query data."))
14
+
15
+
16
+ @scalar_node
17
+ class CallbackQueryDataJson:
18
+ @classmethod
19
+ def __compose__(cls, callback_query: CallbackQueryCute) -> dict[str, typing.Any]:
20
+ return callback_query.decode_data().expect(
21
+ NodeError("Cannot complete decode callback query data."),
22
+ )
23
+
24
+
25
+ __all__ = ("CallbackQueryData", "CallbackQueryDataJson")
@@ -0,0 +1,97 @@
1
+ from nodnod.error import NodeError
2
+ from nodnod.interface.scalar import scalar_node
3
+ from nodnod.node import Scalar
4
+
5
+ from telegrinder.bot.cute_types.message import MessageCute
6
+ from telegrinder.types.enums import ChatType
7
+ from telegrinder.types.objects import Chat, MessageOriginChannel
8
+
9
+ type MessageChannelPost = MessageOriginChannel
10
+ type Post = MessageCute
11
+ type PostId = int
12
+
13
+
14
+ @scalar_node
15
+ class ChatMessageChannelPost:
16
+ @classmethod
17
+ def __compose__(cls, message: MessageCute) -> MessageChannelPost:
18
+ forward_origin = message.forward_origin.expect(NodeError("Message has no forward origin."))
19
+ return forward_origin.only(MessageOriginChannel).expect(NodeError("Message forward origin is not a channel."))
20
+
21
+
22
+ @scalar_node
23
+ class ChatMessageChannelPostId:
24
+ @classmethod
25
+ def __compose__(cls, message_channel_post: ChatMessageChannelPost) -> PostId:
26
+ return message_channel_post.message_id
27
+
28
+
29
+ @scalar_node
30
+ class ChatMessageChannelPostChannel:
31
+ @classmethod
32
+ def __compose__(cls, message_channel_post: ChatMessageChannelPost) -> Channel:
33
+ return message_channel_post.chat
34
+
35
+
36
+ @scalar_node
37
+ class ChatMessageChannelPostChannelId:
38
+ @classmethod
39
+ def __compose__(cls, message_channel_post: ChatMessageChannelPost) -> ChannelId:
40
+ return message_channel_post.chat.id
41
+
42
+
43
+ @scalar_node
44
+ class ChatMessageChannelPostAuthor:
45
+ @classmethod
46
+ def __compose__(cls, message_channel_post: ChatMessageChannelPost) -> str:
47
+ return message_channel_post.author_signature.expect(
48
+ NodeError("Discussion has no signature of the post author."),
49
+ )
50
+
51
+
52
+ @scalar_node
53
+ class ChannelPostNode:
54
+ @classmethod
55
+ def __compose__(cls, message: MessageCute) -> ChannelPost:
56
+ if message.chat.type != ChatType.CHANNEL:
57
+ raise NodeError("Message is not a channel post.")
58
+ return message
59
+
60
+
61
+ @scalar_node
62
+ class ChannelPostId:
63
+ @classmethod
64
+ def __compose__(cls, channel_post: ChannelPostNode) -> PostId:
65
+ return channel_post.message_id
66
+
67
+
68
+ @scalar_node
69
+ class ChannelNode:
70
+ @classmethod
71
+ def __compose__(cls, channel_post: ChannelPostNode) -> Channel:
72
+ return channel_post.chat
73
+
74
+
75
+ @scalar_node
76
+ class ChannelIdNode:
77
+ @classmethod
78
+ def __compose__(cls, channel_post: ChannelPostNode) -> ChannelId:
79
+ return channel_post.chat.id
80
+
81
+
82
+ type Channel = Scalar[Chat, ChannelNode]
83
+ type ChannelId = Scalar[int, ChannelIdNode]
84
+ type ChannelPost = Scalar[MessageCute, ChannelPostNode]
85
+
86
+
87
+ __all__ = (
88
+ "Channel",
89
+ "ChannelId",
90
+ "ChannelPost",
91
+ "ChannelPostId",
92
+ "ChatMessageChannelPost",
93
+ "ChatMessageChannelPostAuthor",
94
+ "ChatMessageChannelPostChannel",
95
+ "ChatMessageChannelPostChannelId",
96
+ "ChatMessageChannelPostId",
97
+ )
@@ -0,0 +1,33 @@
1
+ import typing
2
+ from dataclasses import dataclass, field
3
+
4
+ from kungfu.library.monad.option import NOTHING, Nothing, Option, Some
5
+ from nodnod.interface.data import Node
6
+
7
+ from telegrinder.node.nodes.text import Caption, Text
8
+
9
+
10
+ def single_split(s: str, separator: str) -> tuple[str, str]:
11
+ left, *right = s.split(separator, 1)
12
+ return left, (right[0] if right else "")
13
+
14
+
15
+ def cut_mention(text: str) -> tuple[str, Option[str]]:
16
+ left, right = single_split(text, "@")
17
+ return left, Some(right) if right else NOTHING
18
+
19
+
20
+ @dataclass
21
+ class CommandInfo(Node):
22
+ name: str
23
+ arguments: str
24
+ mention: Option[str] = field(default_factory=Nothing)
25
+
26
+ @classmethod
27
+ def __compose__(cls, text: Text | Caption) -> typing.Self:
28
+ name, arguments = single_split(text, separator=" ")
29
+ name, mention = cut_mention(name)
30
+ return cls(name, arguments, mention)
31
+
32
+
33
+ __all__ = ("CommandInfo", "cut_mention", "single_split")
@@ -0,0 +1,43 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from nodnod.error import NodeError
5
+ from nodnod.interface.generic import generic_node
6
+ from nodnod.node import Node
7
+
8
+ from telegrinder.bot.dispatch.context import Context
9
+
10
+ type ExceptionType = type[Exception]
11
+
12
+
13
+ def can_catch[ExceptionT: Exception](
14
+ exc: Exception | ExceptionType,
15
+ exc_types: type[ExceptionT] | tuple[type[ExceptionT], ...],
16
+ ) -> typing.TypeGuard[ExceptionT]:
17
+ return issubclass(exc, exc_types) if isinstance(exc, type) else isinstance(exc, exc_types)
18
+
19
+
20
+ @generic_node
21
+ @dataclasses.dataclass(kw_only=True, frozen=True)
22
+ class Error[*Exceptions = *tuple[type[Exception], ...]](Node):
23
+ exception_update: Exception
24
+
25
+ @property
26
+ def exception[T: Exception = Exception](self: Error[*tuple[T, ...]]) -> T:
27
+ return self.exception_update # type: ignore[reportReturnType]
28
+
29
+ @classmethod
30
+ def __compose__(
31
+ cls,
32
+ exceptions: tuple[typing.Unpack[Exceptions]],
33
+ context: Context,
34
+ ) -> typing.Self:
35
+ exception_update = context.exception_update.expect(NodeError("No exception."))
36
+
37
+ if can_catch(exception_update, exceptions): # type: ignore
38
+ return cls(exception_update=exception_update)
39
+
40
+ raise NodeError("Foreign exception.")
41
+
42
+
43
+ __all__ = ("Error",)
@@ -0,0 +1,70 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ import msgspec
5
+ from nodnod.error import NodeError
6
+ from nodnod.interface.generic import generic_node
7
+
8
+ from telegrinder.api.api import API
9
+ from telegrinder.bot.cute_types.base import BaseCute
10
+ from telegrinder.bot.cute_types.update import UpdateCute
11
+ from telegrinder.msgspec_utils import decoder
12
+ from telegrinder.tools.fullname import fullname
13
+ from telegrinder.types.objects import Model, Update
14
+
15
+ if typing.TYPE_CHECKING:
16
+ from _typeshed import DataclassInstance
17
+
18
+ type DataclassType = DataclassInstance | msgspec.Struct | dict[str, typing.Any]
19
+
20
+
21
+ @generic_node
22
+ class EventNode[Dataclass: DataclassType]: # type: ignore[reportRedeclaration]
23
+ @classmethod
24
+ def __compose__(
25
+ cls,
26
+ api: API,
27
+ raw_update: Update,
28
+ dataclass: type[Dataclass],
29
+ update_cute: UpdateCute,
30
+ ) -> DataclassType:
31
+ orig_dataclass = typing.get_origin(dataclass) or dataclass
32
+
33
+ if orig_dataclass is UpdateCute:
34
+ return update_cute
35
+
36
+ if issubclass(orig_dataclass, BaseCute | Model):
37
+ incoming_update = (
38
+ update_cute.incoming_update if issubclass(orig_dataclass, BaseCute) else raw_update.incoming_update
39
+ )
40
+
41
+ if type(incoming_update) is not orig_dataclass:
42
+ raise NodeError(f"Incoming update is not `{fullname(orig_dataclass)}`.")
43
+
44
+ return incoming_update
45
+
46
+ try:
47
+ if issubclass(orig_dataclass, msgspec.Struct) or dataclasses.is_dataclass(
48
+ orig_dataclass,
49
+ ):
50
+ obj = decoder.convert(
51
+ obj=raw_update.incoming_update,
52
+ type=dataclass,
53
+ from_attributes=True,
54
+ )
55
+ else:
56
+ obj = dataclass(**raw_update.incoming_update.to_full_dict())
57
+
58
+ return obj
59
+ except Exception as exc:
60
+ raise NodeError(
61
+ f"Cannot parse an update object into `{fullname(dataclass)}`",
62
+ from_error=NodeError(exc),
63
+ )
64
+
65
+
66
+ if typing.TYPE_CHECKING:
67
+ type EventNode[Dataclass: DataclassType] = Dataclass
68
+
69
+
70
+ __all__ = ("EventNode",)
@@ -0,0 +1,39 @@
1
+ import typing
2
+
3
+ from nodnod.error import NodeError
4
+ from nodnod.interface.node_constructor import NodeConstructor
5
+
6
+ import telegrinder.types as tg_types
7
+ from telegrinder.api.api import API
8
+ from telegrinder.node.nodes.attachment import Animation, Audio, Document, Photo, Video, VideoNote, Voice
9
+
10
+ type Attachment = Animation | Audio | Document | Photo | Video | VideoNote | Voice
11
+
12
+
13
+ class FileIdNode(NodeConstructor):
14
+ def __init__(self, attachment_node: type[Attachment], /) -> None:
15
+ self.__map__ = {Attachment: attachment_node}
16
+
17
+ def __compose__(self, attach: Attachment) -> str:
18
+ if isinstance(attach, Photo):
19
+ return attach.sizes[-1].file_id
20
+ return attach.file_id
21
+
22
+
23
+ class FileNode(NodeConstructor):
24
+ def __init__(self, attachment_node: type[Attachment], /) -> None:
25
+ self.__map__ = {str: FileIdNode[attachment_node]}
26
+
27
+ async def __compose__(self, api: API, file_id: str) -> tg_types.File:
28
+ return (await api.get_file(file_id=file_id)).expect(NodeError("File can't be downloaded."))
29
+
30
+
31
+ if typing.TYPE_CHECKING:
32
+ type FileId[T: Attachment] = str
33
+ type File[T: Attachment] = tg_types.File
34
+ else:
35
+ FileId = FileIdNode
36
+ File = FileNode
37
+
38
+
39
+ __all__ = ("File", "FileId")
@@ -0,0 +1,66 @@
1
+ import typing
2
+
3
+ from nodnod.error import NodeError
4
+ from nodnod.node import Node
5
+ from nodnod.value import Value
6
+
7
+ from telegrinder.node.scope import TELEGRINDER_CONTEXT, global_node
8
+ from telegrinder.tools.fullname import fullname
9
+
10
+ NODEFAULT: typing.Final = object()
11
+
12
+ _Unspecialized = typing.NewType("_Unspecialized", type)
13
+
14
+
15
+ @global_node
16
+ class GlobalNode[T = _Unspecialized](Node, abstract=True):
17
+ @typing.overload
18
+ @classmethod
19
+ def set(cls: type[GlobalNode[_Unspecialized]], value: typing.Self, /) -> None: ... # type: ignore
20
+
21
+ @typing.overload
22
+ @classmethod
23
+ def set(cls, value: T, /) -> None: ...
24
+
25
+ @classmethod
26
+ def set(cls, value: T | typing.Self, /) -> None:
27
+ TELEGRINDER_CONTEXT.node_global_scope.push(Value(cls, value))
28
+
29
+ @typing.overload
30
+ @classmethod
31
+ def get(cls: type[GlobalNode[_Unspecialized]], /) -> typing.Self: # type: ignore
32
+ ...
33
+
34
+ @typing.overload
35
+ @classmethod
36
+ def get(cls) -> T: ...
37
+
38
+ @typing.overload
39
+ @classmethod
40
+ def get[Default](cls: type[GlobalNode[_Unspecialized]], *, default: Default) -> typing.Self | Default: # type: ignore
41
+ ...
42
+
43
+ @typing.overload
44
+ @classmethod
45
+ def get[Default](cls, *, default: Default) -> T | Default: ...
46
+
47
+ @classmethod
48
+ def get(cls, *, default: typing.Any = NODEFAULT) -> typing.Any:
49
+ if default is not NODEFAULT and cls not in TELEGRINDER_CONTEXT.node_global_scope:
50
+ return default
51
+
52
+ if (value := TELEGRINDER_CONTEXT.node_global_scope.get(cls)) is None and default is NODEFAULT:
53
+ raise NodeError(f"Node `{fullname(cls)}` has no global value.")
54
+
55
+ return value.value if value is not None else default
56
+
57
+ @classmethod
58
+ def unset(cls) -> None:
59
+ TELEGRINDER_CONTEXT.node_global_scope.pop(cls, None)
60
+
61
+ @classmethod
62
+ def __compose__(cls) -> T:
63
+ return cls.get()
64
+
65
+
66
+ __all__ = ("GlobalNode",)
@@ -0,0 +1,110 @@
1
+ import abc
2
+ import dataclasses
3
+ import gettext
4
+ import os
5
+ import pathlib
6
+ import typing
7
+ from functools import cached_property
8
+
9
+ from nodnod import Node
10
+
11
+ from telegrinder.node.nodes.global_node import GlobalNode
12
+ from telegrinder.node.nodes.source import Locale
13
+
14
+ type Separator = KeySeparator
15
+
16
+ DEFAULT_SEPARATOR: typing.Final = "-"
17
+
18
+
19
+ @dataclasses.dataclass(frozen=True)
20
+ class KeySeparator(GlobalNode[Separator]):
21
+ value: str
22
+
23
+ @classmethod
24
+ def set(cls, value: str, /) -> None:
25
+ super().set(cls(value))
26
+
27
+ @classmethod
28
+ def compose(cls) -> Separator:
29
+ return cls.get(default=cls(DEFAULT_SEPARATOR))
30
+
31
+
32
+ @dataclasses.dataclass(kw_only=True)
33
+ class ABCTranslator(Node, abc.ABC):
34
+ locale: str
35
+ separator: str
36
+ _keys: list[str] = dataclasses.field(default_factory=list[str], init=False)
37
+
38
+ @typing.overload
39
+ def __call__(self, **context: typing.Any) -> str: ...
40
+
41
+ @typing.overload
42
+ def __call__(self, message_id: str, /, **context: typing.Any) -> str: ...
43
+
44
+ def __call__(self, message_id: str | None = None, **context: typing.Any) -> str:
45
+ result = self.translate(message_id or self.message_id, **context)
46
+ if not message_id:
47
+ self._keys.clear()
48
+ return result
49
+
50
+ def __getattr__(self, __key: str) -> typing.Self:
51
+ self._keys.append(__key)
52
+ return self
53
+
54
+ @property
55
+ def message_id(self) -> str:
56
+ return self.separator.join(self._keys)
57
+
58
+ @abc.abstractmethod
59
+ def translate(self, message_id: str, **context: typing.Any) -> str:
60
+ pass
61
+
62
+ @classmethod
63
+ def compose(cls, locale: Locale, separator: KeySeparator) -> typing.Self:
64
+ return cls(locale=locale, separator=separator.value)
65
+
66
+
67
+ @dataclasses.dataclass
68
+ class I18NConfig:
69
+ domain: str
70
+ folder: str | pathlib.Path
71
+ default_locale: str = dataclasses.field(default="en")
72
+
73
+ @cached_property
74
+ def translators(self) -> dict[str, gettext.GNUTranslations]:
75
+ result = {}
76
+
77
+ for name in os.listdir(self.folder):
78
+ if not os.path.isdir(os.path.join(self.folder, name)):
79
+ continue
80
+
81
+ mo_path = os.path.join(self.folder, name, "LC_MESSAGES", f"{self.domain}.mo")
82
+
83
+ if os.path.exists(mo_path):
84
+ with open(mo_path, "rb") as f:
85
+ result[name] = gettext.GNUTranslations(f)
86
+ elif os.path.exists(mo_path[:-2] + "po"):
87
+ raise FileNotFoundError(".po files should be compiled first")
88
+
89
+ return result
90
+
91
+ def get_translator(self, locale: str, /) -> gettext.GNUTranslations:
92
+ locale = locale if locale in self.translators else self.default_locale
93
+ return self.translators[locale]
94
+
95
+
96
+ class BaseTranslator(ABCTranslator):
97
+ config: typing.ClassVar[I18NConfig]
98
+
99
+ def __class_getitem__(cls, config: I18NConfig, /) -> typing.Any:
100
+ return type(cls.__name__, (cls,), dict(config=config, __module__=cls.__module__))
101
+
102
+ @classmethod
103
+ def configure(cls, config: I18NConfig, /) -> None:
104
+ cls.config = config
105
+
106
+ def translate(self, message_id: str, **context: typing.Any) -> str:
107
+ return self.config.get_translator(self.locale).gettext(message_id).format(**context)
108
+
109
+
110
+ __all__ = ("ABCTranslator", "BaseTranslator", "I18NConfig", "KeySeparator")
@@ -0,0 +1,26 @@
1
+ from nodnod.error import NodeError
2
+ from nodnod.interface.scalar import scalar_node
3
+
4
+ from telegrinder.api.api import API
5
+ from telegrinder.node.scope import global_node
6
+ from telegrinder.types.objects import User
7
+
8
+
9
+ @global_node
10
+ @scalar_node
11
+ class Me:
12
+ @classmethod
13
+ async def __compose__(cls, api: API) -> User:
14
+ me = await api.get_me()
15
+ return me.expect(NodeError("Can't complete api.get_me() request."))
16
+
17
+
18
+ @global_node
19
+ @scalar_node
20
+ class BotUsername:
21
+ @classmethod
22
+ def __compose__(cls, me: Me) -> str:
23
+ return me.username.unwrap()
24
+
25
+
26
+ __all__ = ("BotUsername", "Me")
@@ -0,0 +1,15 @@
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
+ from telegrinder.types.objects import MessageEntity
6
+
7
+
8
+ @scalar_node
9
+ class MessageEntities:
10
+ @classmethod
11
+ def __compose__(cls, message: MessageCute) -> list[MessageEntity]:
12
+ return message.entities.expect(NodeError("Message has no entities."))
13
+
14
+
15
+ __all__ = ("MessageEntities",)