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,197 @@
1
+ import base64
2
+ import binascii
3
+ import dataclasses
4
+ import typing
5
+ from collections import deque
6
+ from contextlib import suppress
7
+ from functools import cached_property
8
+
9
+ import msgspec
10
+ from kungfu.library.monad.result import Error, Ok, Result
11
+
12
+ from telegrinder.msgspec_utils import decoder, encoder, get_class_annotations
13
+ from telegrinder.tools.serialization.abc import ABCDataSerializer, ModelType
14
+ from telegrinder.tools.serialization.utils import get_model_ident_key
15
+
16
+ try:
17
+ import brotli # type: ignore
18
+
19
+ except ImportError:
20
+ brotli = None
21
+
22
+ DESERIALIZE_EXCEPTIONS: typing.Final[frozenset[type[BaseException]]] = frozenset(
23
+ filter(
24
+ None,
25
+ (
26
+ msgspec.DecodeError,
27
+ msgspec.ValidationError,
28
+ binascii.Error,
29
+ ValueError,
30
+ brotli.error if brotli is not None else None, # type: ignore
31
+ ),
32
+ ),
33
+ )
34
+
35
+
36
+ @dataclasses.dataclass(frozen=True, slots=True)
37
+ class ModelParser[Model: ModelType]:
38
+ model_type: type[Model]
39
+
40
+ def _is_model(self, obj: typing.Any, /) -> typing.TypeGuard[ModelType]:
41
+ return dataclasses.is_dataclass(obj) or isinstance(obj, msgspec.Struct)
42
+
43
+ def _model_to_dict(self, model: ModelType) -> dict[str, typing.Any]:
44
+ if dataclasses.is_dataclass(model):
45
+ return dataclasses.asdict(model)
46
+ return msgspec.structs.asdict(model) # type: ignore
47
+
48
+ def _is_union(self, inspected_type: msgspec.inspect.Type, /) -> typing.TypeGuard[msgspec.inspect.UnionType]:
49
+ return isinstance(inspected_type, msgspec.inspect.UnionType)
50
+
51
+ def _is_model_type(
52
+ self,
53
+ inspected_type: msgspec.inspect.Type,
54
+ /,
55
+ ) -> typing.TypeGuard[msgspec.inspect.DataclassType | msgspec.inspect.StructType]:
56
+ return isinstance(inspected_type, msgspec.inspect.DataclassType | msgspec.inspect.StructType)
57
+
58
+ def _is_iter_of_model(
59
+ self,
60
+ inspected_type: msgspec.inspect.Type,
61
+ /,
62
+ ) -> typing.TypeGuard[msgspec.inspect.ListType]:
63
+ return isinstance(
64
+ inspected_type,
65
+ msgspec.inspect.ListType | msgspec.inspect.SetType | msgspec.inspect.FrozenSetType,
66
+ ) and self._is_model_type(inspected_type.item_type)
67
+
68
+ def _inspect_annotation(self, annotation: typing.Any, /) -> tuple[type[ModelType], bool] | None:
69
+ is_iter_of_model = False
70
+ type_args: tuple[msgspec.inspect.Type, ...] = tuple()
71
+ inspected_type = msgspec.inspect.type_info(annotation)
72
+
73
+ if self._is_union(inspected_type):
74
+ type_args = inspected_type.types
75
+ elif self._is_iter_of_model(inspected_type):
76
+ type_args = (inspected_type.item_type,)
77
+ is_iter_of_model = True
78
+ elif self._is_model_type(inspected_type):
79
+ type_args = (inspected_type,)
80
+
81
+ for arg in type_args:
82
+ if self._is_union(arg):
83
+ type_args += arg.types
84
+
85
+ if self._is_model_type(arg):
86
+ return (arg.cls, is_iter_of_model)
87
+
88
+ if self._is_iter_of_model(arg):
89
+ return (arg.item_type.cls, True) # type: ignore
90
+
91
+ return None
92
+
93
+ def parse(self, model: Model) -> list[typing.Any]:
94
+ """Returns a parsed model as linked list."""
95
+ linked: list[typing.Any] = []
96
+ stack: list[typing.Any] = [(list(self._model_to_dict(model).values()), linked)]
97
+
98
+ while stack:
99
+ current_obj, current = stack.pop()
100
+
101
+ for item in current_obj:
102
+ if self._is_model(item):
103
+ item = self._model_to_dict(item)
104
+ if isinstance(item, dict):
105
+ new_list = []
106
+ current.append(new_list)
107
+ stack.append((list(item.values()), new_list))
108
+ elif isinstance(item, list | tuple):
109
+ new_list = []
110
+ current.append(new_list)
111
+ stack.append((item, new_list))
112
+ else:
113
+ current.append(item)
114
+
115
+ return encoder.to_builtins(linked)
116
+
117
+ def compose(self, linked: list[typing.Any]) -> dict[str, typing.Any]:
118
+ """Compose linked list to dictionary based on the model class annotations `(without validation)`."""
119
+ root_converted_data: dict[str, typing.Any] = {}
120
+ stack: deque[typing.Any] = deque([(linked, self.model_type, root_converted_data)])
121
+
122
+ while stack:
123
+ current_data, current_model, converted_data = stack.pop()
124
+
125
+ for index, (field, annotation) in enumerate(get_class_annotations(current_model).items()):
126
+ obj, model_type, is_iter_of_model = current_data[index], None, False
127
+
128
+ if isinstance(obj, list) and (inspected := self._inspect_annotation(annotation)):
129
+ model_type, is_iter_of_model = inspected
130
+
131
+ if model_type is not None:
132
+ if is_iter_of_model:
133
+ converted_data[field] = []
134
+ for item in obj:
135
+ new_converted_data = {}
136
+ converted_data[field].append(new_converted_data)
137
+ stack.append((item, model_type, new_converted_data))
138
+ else:
139
+ new_converted_data = {}
140
+ converted_data[field] = new_converted_data
141
+ stack.append((obj, model_type, new_converted_data))
142
+ else:
143
+ converted_data[field] = obj
144
+
145
+ return root_converted_data
146
+
147
+
148
+ class MsgPackSerializer[Model: ModelType](ABCDataSerializer[Model]):
149
+ @typing.overload
150
+ def __init__(self, model_t: type[Model], /) -> None: ...
151
+
152
+ @typing.overload
153
+ def __init__(self, model_t: type[Model], /, *, ident_key: str | None = ...) -> None: ...
154
+
155
+ def __init__(self, model_t: type[Model], /, *, ident_key: str | None = None) -> None:
156
+ self.model_t = model_t
157
+ self.ident_key = ident_key or get_model_ident_key(model_t)
158
+ self._model_parser = ModelParser(model_t)
159
+
160
+ @classmethod
161
+ def serialize_from_model(cls, model: Model, *, ident_key: str | None = None) -> str:
162
+ return cls(model.__class__, ident_key=ident_key).serialize(model)
163
+
164
+ @classmethod
165
+ def deserialize_to_json(cls, serialized_data: str, model_t: type[Model]) -> Result[Model, str]:
166
+ return cls(model_t).deserialize(serialized_data)
167
+
168
+ @cached_property
169
+ def key(self) -> bytes:
170
+ return msgspec.msgpack.encode(super().key) if self.ident_key else b""
171
+
172
+ def serialize(self, data: Model, /) -> str:
173
+ encoded = self.key + msgspec.msgpack.encode(self._model_parser.parse(data), enc_hook=encoder.enc_hook)
174
+ if brotli is not None:
175
+ return base64.b85encode(brotli.compress(encoded, quality=11)).decode() # type: ignore
176
+ return base64.urlsafe_b64encode(encoded).decode()
177
+
178
+ def deserialize(self, serialized_data: str, /) -> Result[Model, str]:
179
+ with suppress(*DESERIALIZE_EXCEPTIONS):
180
+ if brotli is not None:
181
+ ser_data = typing.cast("bytes", brotli.decompress(base64.b85decode(serialized_data)))
182
+ else:
183
+ ser_data = base64.urlsafe_b64decode(serialized_data)
184
+
185
+ if not ser_data.startswith(self.key):
186
+ return Error("Data is not corresponding to key.")
187
+
188
+ data: list[typing.Any] = msgspec.msgpack.decode(
189
+ ser_data.removeprefix(self.key),
190
+ dec_hook=decoder.dec_hook(),
191
+ )
192
+ return Ok(decoder.convert(self._model_parser.compose(data), type=self.model_t))
193
+
194
+ return Error("Incorrect data.")
195
+
196
+
197
+ __all__ = ("MsgPackSerializer",)
@@ -0,0 +1,18 @@
1
+ import typing
2
+
3
+ if typing.TYPE_CHECKING:
4
+ from telegrinder.tools.serialization.abc import ABCDataSerializer
5
+
6
+ IDENT_KEY: typing.Final = "__key__"
7
+ SERIALIZER_KEY: typing.Final = "__serializer__"
8
+
9
+
10
+ def get_model_ident_key(model: typing.Any, /) -> str | None:
11
+ return getattr(model, IDENT_KEY, None)
12
+
13
+
14
+ def get_model_serializer(model: typing.Any, /) -> type[ABCDataSerializer[typing.Any]] | None:
15
+ return getattr(model, SERIALIZER_KEY, None)
16
+
17
+
18
+ __all__ = ("get_model_ident_key", "get_model_serializer")
@@ -0,0 +1,4 @@
1
+ from telegrinder.tools.singleton.abc import ABCSingleton, ABCSingletonMeta
2
+ from telegrinder.tools.singleton.singleton import Singleton, SingletonMeta
3
+
4
+ __all__ = ("ABCSingleton", "ABCSingletonMeta", "Singleton", "SingletonMeta")
@@ -0,0 +1,14 @@
1
+ import abc
2
+
3
+ from telegrinder.tools.singleton.singleton import SingletonMeta
4
+
5
+
6
+ class ABCSingletonMeta(abc.ABCMeta, SingletonMeta):
7
+ pass
8
+
9
+
10
+ class ABCSingleton(metaclass=ABCSingletonMeta):
11
+ pass
12
+
13
+
14
+ __all__ = ("ABCSingleton", "ABCSingletonMeta")
@@ -0,0 +1,18 @@
1
+ import typing
2
+
3
+
4
+ class SingletonMeta(type):
5
+ if not typing.TYPE_CHECKING:
6
+ __instance = None
7
+
8
+ def __call__(cls, *args, **kwargs):
9
+ if cls.__instance is None:
10
+ cls.__instance = super().__call__(*args, **kwargs)
11
+ return cls.__instance
12
+
13
+
14
+ class Singleton(metaclass=SingletonMeta):
15
+ pass
16
+
17
+
18
+ __all__ = ("Singleton", "SingletonMeta")
@@ -0,0 +1,4 @@
1
+ from telegrinder.node import State, StateMutator
2
+ from telegrinder.tools.state_mutator.mutation import mutation
3
+
4
+ __all__ = ("State", "StateMutator", "mutation")
@@ -0,0 +1,85 @@
1
+ import typing
2
+
3
+ from telegrinder.tools.aio import maybe_awaitable
4
+
5
+ if typing.TYPE_CHECKING:
6
+ from telegrinder.node import State, StateMutator
7
+
8
+ class BoundMethod[**P, T, R](typing.Protocol):
9
+ @staticmethod
10
+ def __call__(self: T, *args: P.args, **kwargs: P.kwargs) -> typing.Coroutine[typing.Any, typing.Any, R] | R: ...
11
+
12
+ @typing.runtime_checkable
13
+ class NotBoundFunction[**P, R](typing.Protocol):
14
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> typing.Coroutine[typing.Any, typing.Any, R] | R: ...
15
+
16
+
17
+ class mutation[**P, IntoState: State, BoundState: State | None]: # noqa: N801
18
+ from_state: State | None
19
+
20
+ @typing.overload
21
+ def __init__(
22
+ self: mutation[P, IntoState, BoundState], # type: ignore
23
+ construct_mutation: BoundMethod[P, BoundState, IntoState],
24
+ ) -> None: ...
25
+
26
+ @typing.overload
27
+ def __init__(
28
+ self: mutation[P, IntoState, None], # type: ignore
29
+ construct_mutation: NotBoundFunction[P, IntoState],
30
+ ) -> None: ...
31
+
32
+ def __init__(
33
+ self,
34
+ construct_mutation: typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, IntoState] | IntoState],
35
+ ) -> None:
36
+ self.construct_mutation = construct_mutation
37
+ self.from_state = None
38
+
39
+ @typing.overload
40
+ async def __call__(
41
+ self: mutation[P, IntoState, None],
42
+ mutator: StateMutator,
43
+ /,
44
+ *args: P.args,
45
+ **kwargs: P.kwargs,
46
+ ) -> IntoState: ...
47
+
48
+ @typing.overload
49
+ async def __call__(
50
+ self: mutation[P, IntoState, State],
51
+ *args: P.args,
52
+ **kwargs: P.kwargs,
53
+ ) -> IntoState: ...
54
+
55
+ async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> IntoState:
56
+ mutator: StateMutator
57
+
58
+ if self.from_state is not None:
59
+ if self.from_state.__mutator__ is None:
60
+ raise RuntimeError("State is not bound. Bind state with .bind(mutator) before mutating")
61
+
62
+ await self.from_state.exit()
63
+
64
+ new_state = await maybe_awaitable(self.construct_mutation(self.from_state, *args, **kwargs))
65
+ mutator = self.from_state.__mutator__
66
+ else:
67
+ mutator, args = args[0], args[1:]
68
+
69
+ if (state := await mutator.get()) is not None:
70
+ await state.exit()
71
+
72
+ new_state = await maybe_awaitable(self.construct_mutation(*args, **kwargs))
73
+
74
+ new_state.bind(mutator)
75
+ await new_state.enter()
76
+
77
+ return new_state
78
+
79
+ def __get__(self, instance: State, owner: typing.Any) -> typing.Self:
80
+ if instance is not None:
81
+ self.from_state = instance
82
+ return self
83
+
84
+
85
+ __all__ = ("mutation",)
@@ -0,0 +1,4 @@
1
+ from telegrinder.tools.state_storage.abc import ABCStateStorage, StateData
2
+ from telegrinder.tools.state_storage.memory import MemoryStateStorage
3
+
4
+ __all__ = ("ABCStateStorage", "MemoryStateStorage", "StateData")
@@ -0,0 +1,38 @@
1
+ import abc
2
+ import dataclasses
3
+ import enum
4
+ import typing
5
+
6
+ from kungfu.library.monad.option import Option
7
+
8
+ if typing.TYPE_CHECKING:
9
+ from telegrinder.bot.rules.state import State, StateMeta
10
+
11
+
12
+ @dataclasses.dataclass
13
+ class StateData[Payload]:
14
+ key: str | enum.Enum
15
+ payload: Payload
16
+
17
+ async def save(self, user_id: int, storage: ABCStateStorage[Payload]) -> None:
18
+ await storage.set(user_id, key=self.key, payload=self.payload)
19
+
20
+
21
+ class ABCStateStorage[Payload](abc.ABC):
22
+ @abc.abstractmethod
23
+ async def get(self, user_id: int) -> Option[StateData[Payload]]: ...
24
+
25
+ @abc.abstractmethod
26
+ async def delete(self, user_id: int) -> None: ...
27
+
28
+ @abc.abstractmethod
29
+ async def set(self, user_id: int, key: str | enum.Enum, payload: Payload) -> None: ...
30
+
31
+ def State(self, key: str | StateMeta | enum.Enum | None = None, /) -> State[Payload]: # noqa: N802
32
+ """Can be used as a shortcut to get a state rule dependant on current storage."""
33
+ from telegrinder.bot.rules.state import State, StateMeta
34
+
35
+ return State(storage=self, key=key or StateMeta.ANY)
36
+
37
+
38
+ __all__ = ("ABCStateStorage", "StateData")
@@ -0,0 +1,27 @@
1
+ import typing
2
+
3
+ from kungfu.library.misc import from_optional
4
+ from kungfu.library.monad.option import Option
5
+
6
+ from telegrinder.tools.state_storage.abc import ABCStateStorage, StateData
7
+
8
+ type Payload = dict[str, typing.Any]
9
+
10
+
11
+ class MemoryStateStorage[T = Payload](ABCStateStorage[T]):
12
+ storage: dict[int, StateData[T]]
13
+
14
+ def __init__(self) -> None:
15
+ self.storage = {}
16
+
17
+ async def get(self, user_id: int) -> Option[StateData[T]]:
18
+ return from_optional(self.storage.get(user_id))
19
+
20
+ async def set(self, user_id: int, key: str, payload: T) -> None:
21
+ self.storage[user_id] = StateData(key, payload)
22
+
23
+ async def delete(self, user_id: int) -> None:
24
+ self.storage.pop(user_id)
25
+
26
+
27
+ __all__ = ("MemoryStateStorage",)
@@ -0,0 +1,22 @@
1
+ def to_utf16_map(s: str, /) -> list[int]:
2
+ utf16_map = list[int]()
3
+ utf16_pos = 0
4
+
5
+ for char in s:
6
+ utf16_map.append(utf16_pos)
7
+ utf16_len = len(char.encode("utf-16-le")) // 2
8
+ utf16_pos += utf16_len
9
+
10
+ utf16_map.append(utf16_pos)
11
+ return utf16_map
12
+
13
+
14
+ def utf16_to_py_index(utf16_map: list[int], utf16_index: int, /) -> int:
15
+ for index, u in enumerate(utf16_map):
16
+ if u >= utf16_index:
17
+ return index
18
+
19
+ return len(utf16_map) - 1
20
+
21
+
22
+ __all__ = ("to_utf16_map", "utf16_to_py_index")