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,614 @@
1
+ import dataclasses
2
+ import threading
3
+ import typing
4
+ from copy import deepcopy
5
+ from functools import cached_property, wraps
6
+
7
+ from kungfu.library.misc import from_optional
8
+ from kungfu.library.monad import Error, Nothing, Ok, Option, Result, Some
9
+
10
+ from telegrinder.modules import logger
11
+ from telegrinder.msgspec_utils import convert
12
+ from telegrinder.tools.fullname import fullname
13
+ from telegrinder.tools.global_context.abc import NOVALUE, ABCGlobalContext, CtxVar, CtxVariable, GlobalCtxVar
14
+
15
+ if typing.TYPE_CHECKING:
16
+ _: typing.TypeAlias = None
17
+ else:
18
+ _ = lambda: None
19
+
20
+
21
+ def type_check[T = typing.Any](value: typing.Any, value_type: type[T], /) -> typing.TypeGuard[T]:
22
+ if value_type in (typing.Any, object):
23
+ return True
24
+
25
+ match convert(value, value_type):
26
+ case Ok(v):
27
+ return type(value) is type(v)
28
+ case Error(_):
29
+ return False
30
+
31
+ return False
32
+
33
+
34
+ def is_dunder(name: str) -> bool:
35
+ return name.startswith("__") and name.endswith("__")
36
+
37
+
38
+ def get_orig_class[T = typing.Any](obj: T) -> type[T]:
39
+ if "__orig_class__" not in obj.__dict__:
40
+ return type(obj)
41
+ return obj.__dict__["__orig_class__"]
42
+
43
+
44
+ def root_protection[F: typing.Callable[..., typing.Any]](func: F, /) -> F:
45
+ if func.__name__ not in ("__setattr__", "__getattr__", "__delattr__"):
46
+ raise RuntimeError(
47
+ "You cannot decorate a {!r} function with this decorator, only "
48
+ "'__setattr__', __getattr__', '__delattr__' methods.".format(
49
+ func.__name__,
50
+ )
51
+ )
52
+
53
+ @wraps(func)
54
+ def wrapper(self: GlobalContext, name: str, /, *args: typing.Any) -> typing.Any:
55
+ if self.is_root_attribute(name) and name in (self.__dict__ | self.__class__.__dict__):
56
+ root_attr = self.get_root_attribute(name).unwrap()
57
+ if all((not root_attr.can_be_rewritten, not root_attr.can_be_read)):
58
+ raise AttributeError(f"Unable to set, get, delete root attribute {name!r}.")
59
+ if func.__name__ == "__setattr__" and not root_attr.can_be_rewritten:
60
+ raise AttributeError(f"Unable to set root attribute {name!r}.")
61
+ if func.__name__ == "__getattr__" and not root_attr.can_be_read:
62
+ raise AttributeError(f"Unable to get root attribute {name!r}.")
63
+ if func.__name__ == "__delattr__":
64
+ raise AttributeError(f"Unable to delete root attribute {name!r}.")
65
+
66
+ return func(self, name, *args) # type: ignore
67
+
68
+ return wrapper # type: ignore
69
+
70
+
71
+ @typing.overload
72
+ def ctx_var() -> typing.Any: ...
73
+
74
+
75
+ @typing.overload
76
+ def ctx_var(
77
+ *,
78
+ init: bool = ...,
79
+ const: bool = ...,
80
+ ) -> typing.Any: ...
81
+
82
+
83
+ @typing.overload
84
+ def ctx_var(
85
+ *,
86
+ default: typing.Any,
87
+ init: bool = ...,
88
+ const: bool = ...,
89
+ ) -> typing.Any: ...
90
+
91
+
92
+ @typing.overload
93
+ def ctx_var(
94
+ *,
95
+ default_factory: typing.Callable[[], typing.Any],
96
+ init: bool = ...,
97
+ const: bool = ...,
98
+ ) -> typing.Any: ...
99
+
100
+
101
+ def ctx_var(
102
+ *,
103
+ default: typing.Any = NOVALUE,
104
+ default_factory: typing.Any = NOVALUE,
105
+ const: bool = False,
106
+ **_: typing.Any,
107
+ ) -> typing.Any:
108
+ """Example:
109
+ ```
110
+ class MyCtx(GlobalContext):
111
+ __ctx_name__ = "my_ctx"
112
+
113
+ name: str
114
+ URL: typing.Final[str] = ctx_var(default="https://google.com", init=False, const=True)
115
+
116
+ ctx = MyCtx(name="John")
117
+ ctx.URL #: 'https://google.com'
118
+ ctx.URL = '...' #: type checking error & exception 'TypeError'
119
+ ```
120
+ """
121
+ return CtxVar(value=default, factory=default_factory, const=const)
122
+
123
+
124
+ class ConditionalLock:
125
+ def __init__(self, lock: threading.RLock, use_lock: bool) -> None:
126
+ self.lock = lock
127
+ self.use_lock = use_lock
128
+
129
+ def __enter__(self) -> typing.Self:
130
+ if self.use_lock:
131
+ self.lock.__enter__()
132
+ return self
133
+
134
+ def __exit__(self, exc_type: typing.Any, exc_val: typing.Any, exc_tb: typing.Any) -> None:
135
+ if self.use_lock:
136
+ self.lock.__exit__(exc_type, exc_val, exc_tb)
137
+
138
+
139
+ def runtime_init[T: ABCGlobalContext](cls: type[T], /) -> type[T]:
140
+ r'''Initialization the global context at runtime.
141
+
142
+ ```python
143
+ @runtime_init
144
+ class Box(ABCGlobalContext):
145
+ __ctx_name__ = "box"
146
+
147
+ cookies: list[Cookie] = ctx_var(default_factory=lambda: [ChocolateCookie()], init=False)
148
+ """
149
+ init=False means that when calling the class constructor it will not be necessary
150
+ to pass this field to the class constructor, because it will already be initialized.
151
+ """
152
+
153
+ box = Box() # So, this global context has already been initialized, so calling the class
154
+ # immediately returns an initialized instance of this class from the memory storage.
155
+
156
+ box.cookies.append(OatmealCookie())
157
+ print(box.cookies) # [<ChocolateCookie>, <OatmealCookie>]
158
+ ```
159
+ '''
160
+ cls() # Init an instance of the global context.
161
+ return cls
162
+
163
+
164
+ @dataclasses.dataclass(frozen=True, eq=False, slots=True)
165
+ class RootAttr:
166
+ name: str
167
+ can_be_read: bool = dataclasses.field(default=True, kw_only=True)
168
+ can_be_rewritten: bool = dataclasses.field(default=False, kw_only=True)
169
+
170
+ def __eq__(self, __value: str) -> bool:
171
+ return self.name == __value
172
+
173
+
174
+ @dataclasses.dataclass(repr=False, frozen=True, slots=True)
175
+ class Storage:
176
+ """Thread-safe storage for GlobalContext instances.
177
+
178
+ Uses threading.RLock() for thread synchronization to ensure safe
179
+ concurrent access to the internal storage dictionary.
180
+ """
181
+
182
+ _storage: dict[str | None, GlobalContext] = dataclasses.field(
183
+ default_factory=lambda: {},
184
+ init=False,
185
+ )
186
+ _lock: threading.RLock = dataclasses.field(
187
+ default_factory=threading.RLock,
188
+ init=False,
189
+ )
190
+
191
+ def __repr__(self) -> str:
192
+ return "<{}: {}>".format(
193
+ type(self).__name__,
194
+ ", ".join(repr(x) for x in self._storage),
195
+ )
196
+
197
+ @property
198
+ def storage(self) -> dict[str | None, GlobalContext]:
199
+ with self._lock:
200
+ return self._storage.copy()
201
+
202
+ def set(self, name: str | None, context: GlobalContext) -> None:
203
+ with self._lock:
204
+ self._storage.setdefault(name, context)
205
+
206
+ def get(self, ctx_name: str) -> Option[GlobalContext]:
207
+ with self._lock:
208
+ return from_optional(self._storage.get(ctx_name))
209
+
210
+ def delete(self, ctx_name: str) -> None:
211
+ with self._lock:
212
+ assert self._storage.pop(ctx_name, None) is not None, f"Context {ctx_name!r} is not defined in storage."
213
+
214
+
215
+ @typing.dataclass_transform(
216
+ kw_only_default=True,
217
+ order_default=True,
218
+ frozen_default=False,
219
+ field_specifiers=(ctx_var,),
220
+ )
221
+ class GlobalContext[CtxValueT = typing.Any](ABCGlobalContext, dict[str, GlobalCtxVar[CtxValueT]]):
222
+ """Global context class with optional thread safety.
223
+
224
+ `GlobalContext` is a dictionary with additional methods for working with context.
225
+ Thread safety can be enabled via the `thread_safe` parameter for concurrent access.
226
+
227
+ Examples:
228
+ ```
229
+ # Basic usage (not thread-safe, better performance)
230
+ ctx = GlobalContext("my_ctx")
231
+ ctx["client"] = Client()
232
+
233
+ # Thread-safe usage
234
+ ctx = GlobalContext("my_ctx", thread_safe=True)
235
+ ctx.host = CtxVar("128.0.0.7:8888", const=True)
236
+
237
+ # Thread-safe subclass
238
+ class MyContext(GlobalContext, thread_safe=True):
239
+ __ctx_name__ = "my_ctx"
240
+
241
+ friend: str
242
+
243
+ ctx = MyContext() # thread-safe
244
+ ```
245
+
246
+ """
247
+
248
+ __ctx_name__: str | None
249
+ """Global context name."""
250
+
251
+ __thread_safe__: bool
252
+ """Whether this context instance uses thread synchronization."""
253
+
254
+ __storage__: typing.ClassVar[Storage] = Storage()
255
+ """Storage memory; this is the storage where all initialized global contexts are stored."""
256
+
257
+ __context_lock__: typing.ClassVar[threading.RLock] = threading.RLock()
258
+ """Class-level lock for thread-safe GlobalContext operations."""
259
+
260
+ __root_attributes__: typing.ClassVar[tuple[RootAttr, ...]] = (
261
+ RootAttr(name="__ctx_name__"),
262
+ RootAttr(name="__root_attributes__"),
263
+ RootAttr(name="__storage__"),
264
+ RootAttr(name="__context_lock__"),
265
+ )
266
+ """The sequence of root attributes of this class including this attribute."""
267
+
268
+ def __init_subclass__(cls, *, thread_safe: bool = False, **kwargs: typing.Any) -> None:
269
+ super().__init_subclass__(**kwargs)
270
+ cls.__class_thread_safe__ = thread_safe
271
+
272
+ def __new__(
273
+ cls,
274
+ ctx_name: str | None = None,
275
+ /,
276
+ *,
277
+ thread_safe: bool | None = None,
278
+ **variables: typing.Any | CtxVar[CtxValueT],
279
+ ) -> typing.Self:
280
+ """Create or get from storage a new `GlobalContext` object with optional thread safety."""
281
+ # Determine thread_safe setting: explicit parameter > class setting > default False
282
+ if thread_safe is None:
283
+ thread_safe = getattr(cls, "__class_thread_safe__", False)
284
+
285
+ with cls.__context_lock__:
286
+ if cls is not GlobalContext:
287
+ defaults = {}
288
+ for name in cls.__annotations__:
289
+ if name in cls.__dict__ and name not in cls.__root_attributes__:
290
+ defaults[name] = getattr(cls, name)
291
+ delattr(cls, name)
292
+
293
+ default_ = defaults[name]
294
+ if isinstance(default_, CtxVar) and default_.const:
295
+ variables.pop(name, None)
296
+
297
+ variables = defaults | variables
298
+
299
+ ctx_name = getattr(cls, "__ctx_name__", ctx_name)
300
+ if (ctx := cls.__storage__.storage.get(ctx_name)) is None:
301
+ ctx = dict.__new__(cls, ctx_name)
302
+ cls.__storage__.set(ctx_name, ctx)
303
+
304
+ # Set thread_safe attribute outside the critical section
305
+ ctx.__thread_safe__ = True if ctx_name is None else bool(thread_safe)
306
+ ctx.set_context_variables(variables)
307
+ return ctx # type: ignore
308
+
309
+ def __init__(
310
+ self,
311
+ ctx_name: str | None = None,
312
+ /,
313
+ *,
314
+ thread_safe: bool | None = None,
315
+ **variables: CtxValueT | CtxVariable[CtxValueT],
316
+ ) -> None:
317
+ if not hasattr(self, "__ctx_name__"):
318
+ self.__ctx_name__ = ctx_name
319
+
320
+ if not hasattr(self, "__thread_safe__"):
321
+ if thread_safe is None:
322
+ thread_safe = getattr(self.__class__, "__class_thread_safe__", False)
323
+ self.__thread_safe__ = bool(thread_safe)
324
+
325
+ if variables and not self:
326
+ self.set_context_variables(variables)
327
+
328
+ def __repr__(self) -> str:
329
+ return "<{}: {{ {} }}>".format(
330
+ f"{fullname(self)}@{self.ctx_name}{' (thread-safe)' if self.thread_safe else ''}",
331
+ ", ".join(var_name for var_name in self),
332
+ )
333
+
334
+ def __eq__(self, __value: object) -> bool:
335
+ """Returns True if the names of context stores
336
+ that use self and __value instances are equivalent.
337
+ """
338
+ if not isinstance(__value, type(self)):
339
+ return NotImplemented
340
+ return self.__ctx_name__ == __value.__ctx_name__ and self == __value
341
+
342
+ def __setitem__(self, __name: str, __value: CtxValueT | CtxVariable[CtxValueT]) -> None:
343
+ with self.lock_context:
344
+ if is_dunder(__name):
345
+ raise NameError("Cannot set a context variable with a dunder name.")
346
+
347
+ var = self.get(__name)
348
+ if var and (var.value.const and var.value.value is not NOVALUE):
349
+ raise TypeError(f"Unable to set variable {__name!r}, because it's a constant.")
350
+
351
+ dict.__setitem__(
352
+ self,
353
+ __name,
354
+ GlobalCtxVar.from_var(
355
+ name=__name,
356
+ ctx_value=__value,
357
+ const=var.map(lambda var: var.const).unwrap_or(False),
358
+ ),
359
+ )
360
+
361
+ def __getitem__(self, __name: str) -> CtxValueT:
362
+ with self.lock_context:
363
+ value = self.get(__name).unwrap().value
364
+ if value is NOVALUE:
365
+ raise NameError(f"Variable {__name!r} is not defined in {self.ctx_name!r}.")
366
+ return value
367
+
368
+ def __delitem__(self, __name: str) -> None:
369
+ with self.lock_context:
370
+ var = self.get(__name).unwrap()
371
+ if var.const:
372
+ raise TypeError(f"Unable to delete variable {__name!r}, because it's a constant.")
373
+
374
+ if var.value is NOVALUE:
375
+ raise NameError(f"Variable {__name!r} is not defined in {self.ctx_name!r}.")
376
+
377
+ dict.__delitem__(self, __name)
378
+
379
+ @root_protection
380
+ def __setattr__(self, __name: str, __value: CtxValueT | CtxVariable[CtxValueT]) -> None:
381
+ """Setting a context variable."""
382
+ if is_dunder(__name):
383
+ return object.__setattr__(self, __name, __value)
384
+ self.__setitem__(__name, __value)
385
+
386
+ @root_protection
387
+ def __getattr__(self, __name: str) -> CtxValueT:
388
+ """Getting a context variable."""
389
+ if is_dunder(__name):
390
+ return object.__getattribute__(self, __name)
391
+ return self.__getitem__(__name)
392
+
393
+ @root_protection
394
+ def __delattr__(self, __name: str) -> None:
395
+ """Removing a context variable."""
396
+ if is_dunder(__name):
397
+ return object.__delattr__(self, __name)
398
+ self.__delitem__(__name)
399
+
400
+ @property
401
+ def ctx_name(self) -> str:
402
+ """Global context name."""
403
+ return self.__ctx_name__ or "<anonymous global context at %#x>" % id(self)
404
+
405
+ @property
406
+ def thread_safe(self) -> bool:
407
+ return self.__thread_safe__
408
+
409
+ @cached_property
410
+ def lock_context(self) -> ConditionalLock:
411
+ """Conditional lock context manager based on thread_safe setting."""
412
+ return ConditionalLock(type(self).__context_lock__, self.__thread_safe__)
413
+
414
+ @classmethod
415
+ def is_root_attribute(cls, name: str) -> bool:
416
+ """Returns True if name is a root attribute."""
417
+ return name in cls.__root_attributes__
418
+
419
+ def setdefault_value(self, name: str, default: CtxValueT) -> CtxValueT:
420
+ with self.lock_context:
421
+ if name in self:
422
+ return self[name]
423
+ self[name] = default
424
+ return default
425
+
426
+ def set_context_variables(self, variables: typing.Mapping[str, CtxValueT | CtxVariable[CtxValueT]]) -> None:
427
+ """Set context variables from mapping."""
428
+ with self.lock_context:
429
+ for name, var in variables.items():
430
+ if is_dunder(name):
431
+ raise NameError("Cannot set a context variable with a dunder name.")
432
+
433
+ existing_var = self.get(name)
434
+ if existing_var and (existing_var.value.const and existing_var.value.value is not NOVALUE):
435
+ raise TypeError(f"Unable to set variable {name!r}, because it's a constant.")
436
+
437
+ dict.__setitem__(
438
+ self,
439
+ name,
440
+ GlobalCtxVar.from_var(
441
+ name=name,
442
+ ctx_value=var,
443
+ const=existing_var.map(lambda v: v.const).unwrap_or(False),
444
+ ),
445
+ )
446
+
447
+ def get_root_attribute(self, name: str) -> Option[RootAttr]:
448
+ """Get root attribute by name."""
449
+ if self.is_root_attribute(name):
450
+ for rattr in self.__root_attributes__:
451
+ if rattr.name == name:
452
+ return Some(rattr)
453
+ return Nothing()
454
+
455
+ def items(self) -> list[tuple[str, GlobalCtxVar[CtxValueT]]]:
456
+ """Return context variables as set-like items."""
457
+ return list(dict.items(self))
458
+
459
+ def keys(self) -> list[str]:
460
+ """Returns context variable names as keys."""
461
+ return list(dict.keys(self))
462
+
463
+ def values(self) -> list[GlobalCtxVar[CtxValueT]]:
464
+ """Returns context variables as values."""
465
+ return list(dict.values(self))
466
+
467
+ def update(self, other: typing.Self) -> None:
468
+ with self.lock_context:
469
+ dict.update(self, dict(other.items()))
470
+
471
+ def copy(self) -> typing.Self:
472
+ """Copy context. Returns copied context without ctx_name."""
473
+ copied_ctx = self.__class__(thread_safe=self.thread_safe)
474
+ copied_ctx.set_context_variables({name: var.value for name, var in self.dict().items()})
475
+ return copied_ctx
476
+
477
+ def dict(self) -> dict[str, GlobalCtxVar[CtxValueT]]:
478
+ """Returns context as dict."""
479
+ return {name: deepcopy(var) for name, var in self.items()} # type: ignore
480
+
481
+ @typing.overload
482
+ def pop(self, var_name: str) -> Option[GlobalCtxVar[CtxValueT]]: ...
483
+
484
+ @typing.overload
485
+ def pop[T = typing.Any](
486
+ self,
487
+ var_name: str,
488
+ var_value_type: type[T],
489
+ ) -> Option[GlobalCtxVar[T]]: ...
490
+
491
+ def pop(self, var_name: str, var_value_type: typing.Any = object) -> typing.Any:
492
+ with self.lock_context:
493
+ val = self.get(var_name, var_value_type)
494
+
495
+ if val:
496
+ var = dict.get(self, var_name)
497
+ if var and var.const:
498
+ raise TypeError(f"Unable to delete variable {var_name!r}, because it's a constant.")
499
+ if var and var.value is NOVALUE:
500
+ raise NameError(f"Variable {var_name!r} is not defined in {self.ctx_name!r}.")
501
+ dict.__delitem__(self, var_name)
502
+ return val
503
+
504
+ return Nothing()
505
+
506
+ @typing.overload
507
+ def get(self, var_name: str) -> Option[GlobalCtxVar[CtxValueT]]: ...
508
+
509
+ @typing.overload
510
+ def get[T = typing.Any](
511
+ self,
512
+ var_name: str,
513
+ var_value_type: type[T],
514
+ ) -> Option[GlobalCtxVar[T]]: ...
515
+
516
+ def get(self, var_name: str, var_value_type: typing.Any = object) -> typing.Any:
517
+ """Get context variable by name."""
518
+ var_value_type = typing.Any if var_value_type is object else var_value_type
519
+ generic_types = typing.get_args(get_orig_class(self))
520
+
521
+ if generic_types and var_value_type is object:
522
+ var_value_type = generic_types[0]
523
+
524
+ var = dict.get(self, var_name)
525
+ if var is None:
526
+ return Nothing()
527
+
528
+ assert type_check(var.value, var_value_type), (
529
+ "Context variable value type of {!r} does not correspond to the expected type {!r}.".format(
530
+ type(var.value).__name__,
531
+ (getattr(var_value_type, "__name__") if isinstance(var_value_type, type) else repr(var_value_type)),
532
+ )
533
+ )
534
+ return Some(var)
535
+
536
+ @typing.overload
537
+ def get_value(self, var_name: str) -> Option[CtxValueT]: ...
538
+
539
+ @typing.overload
540
+ def get_value[T = typing.Any](
541
+ self,
542
+ var_name: str,
543
+ var_value_type: type[T],
544
+ ) -> Option[T]: ...
545
+
546
+ def get_value(self, var_name: str, var_value_type: typing.Any = object) -> typing.Any:
547
+ """Get context variable value by name."""
548
+ return self.get(var_name, var_value_type).map(lambda var: var.value)
549
+
550
+ def rename(self, old_var_name: str, new_var_name: str) -> Result[_, str]:
551
+ with self.lock_context:
552
+ var = self.get(old_var_name).unwrap()
553
+ if var.const:
554
+ return Error(f"Unable to rename variable {old_var_name!r}, because it's a constant.")
555
+
556
+ # Atomic rename operation
557
+ dict.__delitem__(self, old_var_name)
558
+ dict.__setitem__(
559
+ self,
560
+ new_var_name,
561
+ GlobalCtxVar.from_var(
562
+ name=new_var_name,
563
+ ctx_value=var.value,
564
+ const=var.const,
565
+ ),
566
+ )
567
+ return Ok(_())
568
+
569
+ def clear(self, *, include_consts: bool = False) -> None:
570
+ """Clear context. If `include_consts = True`, the context is fully cleared."""
571
+ with self.lock_context:
572
+ if not self:
573
+ return
574
+
575
+ if include_consts:
576
+ logger.warning(
577
+ "Constants from the global context {!r} have been cleaned up!",
578
+ self.ctx_name + " at %#x" % id(self),
579
+ )
580
+ return dict.clear(self)
581
+
582
+ items_to_delete = []
583
+ for name, var in dict.items(self):
584
+ if not var.const:
585
+ items_to_delete.append(name)
586
+
587
+ for name in items_to_delete:
588
+ dict.__delitem__(self, name)
589
+
590
+ def delete_ctx(self) -> Result[_, str]:
591
+ """Delete context by `ctx_name`."""
592
+ with self.lock_context:
593
+ if not self.__ctx_name__:
594
+ return Error("Cannot delete unnamed context.")
595
+
596
+ ctx = self.__storage__.get(self.ctx_name).unwrap()
597
+ dict.clear(ctx)
598
+ self.__storage__.delete(self.ctx_name)
599
+
600
+ logger.warning(f"Global context {self.ctx_name!r} has been deleted!")
601
+ return Ok(_())
602
+
603
+
604
+ __all__ = (
605
+ "ABCGlobalContext",
606
+ "CtxVar",
607
+ "CtxVariable",
608
+ "GlobalContext",
609
+ "GlobalCtxVar",
610
+ "RootAttr",
611
+ "Storage",
612
+ "ctx_var",
613
+ "runtime_init",
614
+ )
@@ -0,0 +1,30 @@
1
+ import dataclasses
2
+ import pathlib
3
+
4
+ from telegrinder.types.objects import InputFile
5
+
6
+
7
+ @dataclasses.dataclass
8
+ class InputFileDirectory:
9
+ directory: pathlib.Path
10
+ storage: dict[str, InputFile] = dataclasses.field(init=False, repr=False)
11
+
12
+ def __post_init__(self) -> None:
13
+ self.storage = self._load_files()
14
+
15
+ def _load_files(self) -> dict[str, InputFile]:
16
+ files = {}
17
+
18
+ for path in self.directory.rglob("*"):
19
+ if path.is_file():
20
+ relative_path = path.relative_to(self.directory)
21
+ files[str(relative_path)] = InputFile(path.name, path.read_bytes())
22
+
23
+ return files
24
+
25
+ def get(self, filename: str, /) -> InputFile:
26
+ assert filename in self.storage, f"File {filename!r} not found."
27
+ return self.storage[filename]
28
+
29
+
30
+ __all__ = ("InputFileDirectory",)
@@ -0,0 +1,6 @@
1
+ from telegrinder.tools.keyboard.abc import ABCKeyboard
2
+ from telegrinder.tools.keyboard.button import Button, InlineButton
3
+ from telegrinder.tools.keyboard.keyboard import InlineKeyboard, Keyboard
4
+ from telegrinder.tools.keyboard.utils import RowButtons
5
+
6
+ __all__ = ("ABCKeyboard", "Button", "InlineButton", "InlineKeyboard", "Keyboard", "RowButtons")