telegrinder 0.3.4__py3-none-any.whl → 0.4.0__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.

Potentially problematic release.


This version of telegrinder might be problematic. Click here for more details.

Files changed (192) hide show
  1. telegrinder/__init__.py +148 -149
  2. telegrinder/api/__init__.py +9 -8
  3. telegrinder/api/api.py +101 -93
  4. telegrinder/api/error.py +20 -16
  5. telegrinder/api/response.py +20 -20
  6. telegrinder/api/token.py +36 -36
  7. telegrinder/bot/__init__.py +72 -66
  8. telegrinder/bot/bot.py +83 -76
  9. telegrinder/bot/cute_types/__init__.py +19 -17
  10. telegrinder/bot/cute_types/base.py +184 -258
  11. telegrinder/bot/cute_types/callback_query.py +400 -385
  12. telegrinder/bot/cute_types/chat_join_request.py +62 -61
  13. telegrinder/bot/cute_types/chat_member_updated.py +157 -160
  14. telegrinder/bot/cute_types/inline_query.py +44 -43
  15. telegrinder/bot/cute_types/message.py +2590 -2637
  16. telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
  17. telegrinder/bot/cute_types/update.py +112 -104
  18. telegrinder/bot/cute_types/utils.py +62 -95
  19. telegrinder/bot/dispatch/__init__.py +59 -55
  20. telegrinder/bot/dispatch/abc.py +76 -77
  21. telegrinder/bot/dispatch/context.py +96 -98
  22. telegrinder/bot/dispatch/dispatch.py +254 -202
  23. telegrinder/bot/dispatch/handler/__init__.py +13 -13
  24. telegrinder/bot/dispatch/handler/abc.py +23 -24
  25. telegrinder/bot/dispatch/handler/audio_reply.py +44 -44
  26. telegrinder/bot/dispatch/handler/base.py +57 -57
  27. telegrinder/bot/dispatch/handler/document_reply.py +44 -44
  28. telegrinder/bot/dispatch/handler/func.py +129 -135
  29. telegrinder/bot/dispatch/handler/media_group_reply.py +44 -43
  30. telegrinder/bot/dispatch/handler/message_reply.py +36 -36
  31. telegrinder/bot/dispatch/handler/photo_reply.py +44 -44
  32. telegrinder/bot/dispatch/handler/sticker_reply.py +37 -37
  33. telegrinder/bot/dispatch/handler/video_reply.py +44 -44
  34. telegrinder/bot/dispatch/middleware/__init__.py +3 -3
  35. telegrinder/bot/dispatch/middleware/abc.py +97 -22
  36. telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
  37. telegrinder/bot/dispatch/process.py +151 -157
  38. telegrinder/bot/dispatch/return_manager/__init__.py +15 -13
  39. telegrinder/bot/dispatch/return_manager/abc.py +104 -108
  40. telegrinder/bot/dispatch/return_manager/callback_query.py +20 -20
  41. telegrinder/bot/dispatch/return_manager/inline_query.py +15 -15
  42. telegrinder/bot/dispatch/return_manager/message.py +36 -36
  43. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +20 -0
  44. telegrinder/bot/dispatch/view/__init__.py +15 -13
  45. telegrinder/bot/dispatch/view/abc.py +45 -41
  46. telegrinder/bot/dispatch/view/base.py +231 -200
  47. telegrinder/bot/dispatch/view/box.py +140 -129
  48. telegrinder/bot/dispatch/view/callback_query.py +16 -17
  49. telegrinder/bot/dispatch/view/chat_join_request.py +11 -16
  50. telegrinder/bot/dispatch/view/chat_member.py +37 -39
  51. telegrinder/bot/dispatch/view/inline_query.py +16 -17
  52. telegrinder/bot/dispatch/view/message.py +43 -44
  53. telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
  54. telegrinder/bot/dispatch/view/raw.py +116 -114
  55. telegrinder/bot/dispatch/waiter_machine/__init__.py +17 -17
  56. telegrinder/bot/dispatch/waiter_machine/actions.py +14 -13
  57. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +8 -8
  58. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +55 -55
  59. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +59 -57
  60. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +51 -51
  61. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +20 -19
  62. telegrinder/bot/dispatch/waiter_machine/machine.py +251 -172
  63. telegrinder/bot/dispatch/waiter_machine/middleware.py +94 -89
  64. telegrinder/bot/dispatch/waiter_machine/short_state.py +57 -68
  65. telegrinder/bot/polling/__init__.py +4 -4
  66. telegrinder/bot/polling/abc.py +25 -25
  67. telegrinder/bot/polling/polling.py +139 -131
  68. telegrinder/bot/rules/__init__.py +85 -62
  69. telegrinder/bot/rules/abc.py +213 -206
  70. telegrinder/bot/rules/callback_data.py +122 -163
  71. telegrinder/bot/rules/chat_join.py +45 -43
  72. telegrinder/bot/rules/command.py +126 -126
  73. telegrinder/bot/rules/enum_text.py +33 -36
  74. telegrinder/bot/rules/func.py +28 -26
  75. telegrinder/bot/rules/fuzzy.py +24 -24
  76. telegrinder/bot/rules/id.py +24 -0
  77. telegrinder/bot/rules/inline.py +58 -56
  78. telegrinder/bot/rules/integer.py +21 -20
  79. telegrinder/bot/rules/is_from.py +127 -127
  80. telegrinder/bot/rules/logic.py +18 -0
  81. telegrinder/bot/rules/markup.py +42 -43
  82. telegrinder/bot/rules/mention.py +14 -14
  83. telegrinder/bot/rules/message.py +15 -17
  84. telegrinder/bot/rules/message_entities.py +33 -35
  85. telegrinder/bot/rules/node.py +33 -27
  86. telegrinder/bot/rules/payload.py +81 -0
  87. telegrinder/bot/rules/payment_invoice.py +29 -0
  88. telegrinder/bot/rules/regex.py +36 -37
  89. telegrinder/bot/rules/rule_enum.py +72 -72
  90. telegrinder/bot/rules/start.py +42 -42
  91. telegrinder/bot/rules/state.py +35 -37
  92. telegrinder/bot/rules/text.py +38 -33
  93. telegrinder/bot/rules/update.py +15 -15
  94. telegrinder/bot/scenario/__init__.py +5 -5
  95. telegrinder/bot/scenario/abc.py +17 -19
  96. telegrinder/bot/scenario/checkbox.py +174 -176
  97. telegrinder/bot/scenario/choice.py +48 -51
  98. telegrinder/client/__init__.py +12 -4
  99. telegrinder/client/abc.py +100 -75
  100. telegrinder/client/aiohttp.py +134 -130
  101. telegrinder/client/form_data.py +31 -0
  102. telegrinder/client/sonic.py +212 -0
  103. telegrinder/model.py +208 -315
  104. telegrinder/modules.py +239 -237
  105. telegrinder/msgspec_json.py +14 -14
  106. telegrinder/msgspec_utils.py +478 -410
  107. telegrinder/node/__init__.py +86 -25
  108. telegrinder/node/attachment.py +163 -87
  109. telegrinder/node/base.py +288 -160
  110. telegrinder/node/callback_query.py +54 -53
  111. telegrinder/node/command.py +34 -33
  112. telegrinder/node/composer.py +163 -198
  113. telegrinder/node/container.py +33 -27
  114. telegrinder/node/either.py +82 -0
  115. telegrinder/node/event.py +54 -65
  116. telegrinder/node/file.py +51 -0
  117. telegrinder/node/me.py +15 -16
  118. telegrinder/node/payload.py +78 -0
  119. telegrinder/node/polymorphic.py +67 -48
  120. telegrinder/node/rule.py +72 -76
  121. telegrinder/node/scope.py +36 -38
  122. telegrinder/node/source.py +87 -71
  123. telegrinder/node/text.py +53 -41
  124. telegrinder/node/tools/__init__.py +3 -3
  125. telegrinder/node/tools/generator.py +36 -40
  126. telegrinder/py.typed +0 -0
  127. telegrinder/rules.py +1 -62
  128. telegrinder/tools/__init__.py +152 -93
  129. telegrinder/tools/adapter/__init__.py +19 -0
  130. telegrinder/tools/adapter/abc.py +49 -0
  131. telegrinder/tools/adapter/dataclass.py +56 -0
  132. telegrinder/{bot/rules → tools}/adapter/errors.py +5 -5
  133. telegrinder/{bot/rules → tools}/adapter/event.py +63 -65
  134. telegrinder/{bot/rules → tools}/adapter/node.py +46 -48
  135. telegrinder/{bot/rules → tools}/adapter/raw_event.py +27 -27
  136. telegrinder/{bot/rules → tools}/adapter/raw_update.py +30 -30
  137. telegrinder/tools/buttons.py +106 -80
  138. telegrinder/tools/callback_data_serilization/__init__.py +5 -0
  139. telegrinder/tools/callback_data_serilization/abc.py +51 -0
  140. telegrinder/tools/callback_data_serilization/json_ser.py +60 -0
  141. telegrinder/tools/callback_data_serilization/msgpack_ser.py +172 -0
  142. telegrinder/tools/error_handler/__init__.py +7 -7
  143. telegrinder/tools/error_handler/abc.py +30 -33
  144. telegrinder/tools/error_handler/error.py +9 -9
  145. telegrinder/tools/error_handler/error_handler.py +179 -193
  146. telegrinder/tools/formatting/__init__.py +83 -63
  147. telegrinder/tools/formatting/deep_links.py +541 -0
  148. telegrinder/tools/formatting/{html.py → html_formatter.py} +266 -294
  149. telegrinder/tools/formatting/spec_html_formats.py +71 -117
  150. telegrinder/tools/functional.py +8 -12
  151. telegrinder/tools/global_context/__init__.py +7 -7
  152. telegrinder/tools/global_context/abc.py +63 -63
  153. telegrinder/tools/global_context/global_context.py +387 -412
  154. telegrinder/tools/global_context/telegrinder_ctx.py +27 -27
  155. telegrinder/tools/i18n/__init__.py +7 -7
  156. telegrinder/tools/i18n/abc.py +30 -30
  157. telegrinder/tools/i18n/middleware/__init__.py +3 -3
  158. telegrinder/tools/i18n/middleware/abc.py +22 -25
  159. telegrinder/tools/i18n/simple.py +43 -43
  160. telegrinder/tools/input_file_directory.py +30 -0
  161. telegrinder/tools/keyboard.py +128 -128
  162. telegrinder/tools/lifespan.py +105 -0
  163. telegrinder/tools/limited_dict.py +32 -37
  164. telegrinder/tools/loop_wrapper/__init__.py +4 -4
  165. telegrinder/tools/loop_wrapper/abc.py +20 -15
  166. telegrinder/tools/loop_wrapper/loop_wrapper.py +169 -224
  167. telegrinder/tools/magic.py +307 -157
  168. telegrinder/tools/parse_mode.py +6 -6
  169. telegrinder/tools/state_storage/__init__.py +4 -4
  170. telegrinder/tools/state_storage/abc.py +31 -35
  171. telegrinder/tools/state_storage/memory.py +25 -25
  172. telegrinder/tools/strings.py +13 -0
  173. telegrinder/types/__init__.py +268 -260
  174. telegrinder/types/enums.py +711 -701
  175. telegrinder/types/input_file.py +51 -0
  176. telegrinder/types/methods.py +5055 -4633
  177. telegrinder/types/objects.py +7058 -6950
  178. telegrinder/verification_utils.py +30 -32
  179. {telegrinder-0.3.4.dist-info → telegrinder-0.4.0.dist-info}/LICENSE +22 -22
  180. telegrinder-0.4.0.dist-info/METADATA +144 -0
  181. telegrinder-0.4.0.dist-info/RECORD +182 -0
  182. {telegrinder-0.3.4.dist-info → telegrinder-0.4.0.dist-info}/WHEEL +1 -1
  183. telegrinder/bot/rules/adapter/__init__.py +0 -17
  184. telegrinder/bot/rules/adapter/abc.py +0 -31
  185. telegrinder/node/message.py +0 -14
  186. telegrinder/node/update.py +0 -15
  187. telegrinder/tools/formatting/links.py +0 -38
  188. telegrinder/tools/kb_set/__init__.py +0 -4
  189. telegrinder/tools/kb_set/base.py +0 -15
  190. telegrinder/tools/kb_set/yaml.py +0 -63
  191. telegrinder-0.3.4.dist-info/METADATA +0 -110
  192. telegrinder-0.3.4.dist-info/RECORD +0 -165
@@ -1,413 +1,388 @@
1
- import dataclasses
2
- from copy import deepcopy
3
- from functools import wraps
4
-
5
- import typing_extensions as typing
6
- from fntypes.co import Error, Nothing, Ok, Option, Result, Some
7
-
8
- from telegrinder.modules import logger
9
- from telegrinder.msgspec_utils import msgspec_convert
10
-
11
- from .abc import ABCGlobalContext, CtxVar, CtxVariable, GlobalCtxVar
12
-
13
- T = typing.TypeVar("T")
14
- F = typing.TypeVar("F", bound=typing.Callable)
15
- CtxValueT = typing.TypeVar("CtxValueT", default=typing.Any)
16
-
17
- if typing.TYPE_CHECKING:
18
- _: typing.TypeAlias = None
19
- else:
20
- _ = lambda: None
21
-
22
-
23
- def type_check(value: object, value_type: type[T]) -> typing.TypeGuard[T]:
24
- if value_type in (typing.Any, object):
25
- return True
26
- match msgspec_convert(value, value_type):
27
- case Ok(v):
28
- return type(value) is type(v)
29
- case Error(_):
30
- return False
31
-
32
-
33
- def is_dunder(name: str) -> bool:
34
- return name.startswith("__") and name.endswith("__")
35
-
36
-
37
- def get_orig_class(obj: T) -> type[T]:
38
- return getattr(obj, "__orig_class__", obj.__class__)
39
-
40
-
41
- def root_protection(func: F) -> F:
42
- if func.__name__ not in ("__setattr__", "__getattr__", "__delattr__"):
43
- raise RuntimeError(
44
- "You cannot decorate a {!r} function with this decorator, only "
45
- "'__setattr__', __getattr__', '__delattr__' methods.".format(
46
- func.__name__,
47
- )
48
- )
49
-
50
- @wraps(func)
51
- def wrapper(self: "GlobalContext", name: str, /, *args) -> typing.Any:
52
- if self.is_root_attribute(name) and name in (self.__dict__ | self.__class__.__dict__):
53
- root_attr = self.get_root_attribute(name).unwrap()
54
- if all((not root_attr.can_be_rewritten, not root_attr.can_be_read)):
55
- raise AttributeError(f"Unable to set, get, delete root attribute {name!r}.")
56
- if func.__name__ == "__setattr__" and not root_attr.can_be_rewritten:
57
- raise AttributeError(f"Unable to set root attribute {name!r}.")
58
- if func.__name__ == "__getattr__" and not root_attr.can_be_read:
59
- raise AttributeError(f"Unable to get root attribute {name!r}.")
60
- if func.__name__ == "__delattr__":
61
- raise AttributeError(f"Unable to delete root attribute {name!r}.")
62
-
63
- return func(self, name, *args) # type: ignore
64
-
65
- return wrapper # type: ignore
66
-
67
-
68
- def ctx_var(value: T, *, const: bool = False) -> T:
69
- """Example:
70
- ```
71
- class MyCtx(GlobalContext):
72
- name: typing.Final[str]
73
- URL: typing.Final = ctx_var("https://google.com", const=True)
74
-
75
- ctx = MyCtx(name=ctx_var("Alex", const=True))
76
- ctx.URL #: 'https://google.com'
77
- ctx.URL = '...' #: type checking error & exception 'TypeError'
78
- ```
79
- """
80
-
81
- return typing.cast(T, CtxVar(value, const=const))
82
-
83
-
84
- @dataclasses.dataclass(frozen=True, eq=False, slots=True)
85
- class RootAttr:
86
- name: str
87
- can_be_read: bool = dataclasses.field(default=True, kw_only=True)
88
- can_be_rewritten: bool = dataclasses.field(default=False, kw_only=True)
89
-
90
- def __eq__(self, __value: str) -> bool:
91
- return self.name == __value
92
-
93
-
94
- @dataclasses.dataclass(repr=False, frozen=True, slots=True)
95
- class Storage:
96
- _storage: dict[str, "GlobalContext"] = dataclasses.field(
97
- default_factory=lambda: {},
98
- init=False,
99
- )
100
-
101
- def __repr__(self) -> str:
102
- return "<ContextStorage: %s>" % ", ".join("ctx @" + repr(x) for x in self._storage)
103
-
104
- @property
105
- def storage(self) -> dict[str, "GlobalContext"]:
106
- return self._storage.copy()
107
-
108
- def set(self, name: str, ctx: "GlobalContext") -> None:
109
- self._storage.setdefault(name, ctx)
110
-
111
- def get(self, ctx_name: str) -> Option["GlobalContext"]:
112
- ctx = self._storage.get(ctx_name)
113
- return Some(ctx) if ctx is not None else Nothing()
114
-
115
- def delete(self, ctx_name: str) -> None:
116
- assert (
117
- self._storage.pop(ctx_name, None) is not None
118
- ), f"Context {ctx_name!r} is not defined in storage."
119
-
120
-
121
- @typing.dataclass_transform(
122
- kw_only_default=True,
123
- order_default=True,
124
- field_specifiers=(ctx_var,),
125
- )
126
- class GlobalContext(ABCGlobalContext, typing.Generic[CtxValueT], dict[str, GlobalCtxVar[CtxValueT]]):
127
- """GlobalContext.
128
-
129
- ```
130
- ctx = GlobalContext()
131
- ctx["client"] = Client()
132
- ctx.address = CtxVar("128.0.0.7:8888", const=True)
133
-
134
- def request():
135
- data = {"user": "root_user", "password": "secret_password"}
136
- ctx.client.request(ctx.address + "/login", data)
137
- """
138
-
139
- __ctx_name__: str | None
140
- __storage__: typing.ClassVar[Storage] = Storage()
141
- __root_attributes__: typing.ClassVar[tuple[RootAttr, ...]] = (
142
- RootAttr("__ctx_name__"),
143
- RootAttr("__root_attributes__"),
144
- RootAttr("__storage__"),
145
- )
146
-
147
- def __new__(
148
- cls,
149
- ctx_name: str | None = None,
150
- /,
151
- **variables: typing.Any | CtxVar[CtxValueT],
152
- ) -> typing.Self:
153
- """Create or get from storage a new `GlobalContext` object."""
154
-
155
- if not issubclass(GlobalContext, cls):
156
- defaults = {}
157
- for name in cls.__annotations__:
158
- if name in cls.__dict__ and name not in cls.__root_attributes__:
159
- defaults[name] = getattr(cls, name)
160
- delattr(cls, name)
161
- if isinstance(defaults[name], CtxVar) and defaults[name].const:
162
- variables.pop(name, None)
163
-
164
- variables = defaults | variables
165
-
166
- ctx_name = getattr(cls, "__ctx_name__", ctx_name)
167
- if ctx_name is None:
168
- ctx = dict.__new__(cls)
169
- elif ctx_name in cls.__storage__.storage:
170
- ctx = cls.__storage__.get(ctx_name).unwrap()
171
- else:
172
- ctx = dict.__new__(cls, ctx_name)
173
- cls.__storage__.set(ctx_name, ctx)
174
-
175
- ctx.set_context_variables(variables)
176
- return ctx # type: ignore
177
-
178
- def __init__(
179
- self,
180
- ctx_name: str | None = None,
181
- /,
182
- **variables: CtxValueT | CtxVariable[CtxValueT],
183
- ):
184
- """Initialization of `GlobalContext` with passed variables."""
185
-
186
- if not hasattr(self, "__ctx_name__"):
187
- self.__ctx_name__ = ctx_name
188
-
189
- if variables and not self:
190
- self.set_context_variables(variables)
191
-
192
- def __repr__(self) -> str:
193
- return "<{} -> ({})>".format(
194
- f"{self.__class__.__name__}@{self.ctx_name!r}",
195
- ", ".join(repr(var) for var in self),
196
- )
197
-
198
- def __eq__(self, __value: "GlobalContext") -> bool:
199
- """Returns True if the names of context stores
200
- that use self and __value instances are equivalent."""
201
-
202
- return isinstance(__value, GlobalContext) and self.__ctx_name__ == __value.__ctx_name__
203
-
204
- def __setitem__(self, __name: str, __value: CtxValueT | CtxVariable[CtxValueT]):
205
- if is_dunder(__name):
206
- raise NameError("Cannot set a context variable with dunder name.")
207
- var = self.get(__name)
208
- if var and var.unwrap().const:
209
- raise TypeError(f"Unable to set variable {__name!r}, because it's a constant.")
210
- dict.__setitem__(self, __name, GlobalCtxVar.collect(__name, __value))
211
-
212
- def __getitem__(self, __name: str) -> CtxValueT:
213
- return self.get(__name).unwrap().value
214
-
215
- def __delitem__(self, __name: str):
216
- var = self.get(__name).unwrap()
217
- if var.const:
218
- raise TypeError(f"Unable to delete variable {__name!r}, because it's a constant.")
219
- dict.__delitem__(self, __name)
220
-
221
- @root_protection
222
- def __setattr__(self, __name: str, __value: CtxValueT | CtxVariable[CtxValueT]):
223
- """Setting a context variable."""
224
-
225
- if is_dunder(__name):
226
- return object.__setattr__(self, __name, __value)
227
- self.__setitem__(__name, __value)
228
-
229
- @root_protection
230
- def __getattr__(self, __name: str) -> CtxValueT:
231
- """Getting a context variable."""
232
-
233
- if is_dunder(__name):
234
- return object.__getattribute__(self, __name)
235
- return self.__getitem__(__name)
236
-
237
- @root_protection
238
- def __delattr__(self, __name: str) -> None:
239
- """Removing a context variable."""
240
-
241
- if is_dunder(__name):
242
- return object.__delattr__(self, __name)
243
- self.__delitem__(__name)
244
-
245
- @property
246
- def ctx_name(self) -> str:
247
- """Context name."""
248
-
249
- return self.__ctx_name__ or "<Unnamed ctx at %#x>" % id(self)
250
-
251
- @classmethod
252
- def is_root_attribute(cls, name: str) -> bool:
253
- """Returns True if exists root attribute
254
- otherwise False."""
255
-
256
- return name in cls.__root_attributes__
257
-
258
- def set_context_variables(
259
- self, variables: typing.Mapping[str, CtxValueT | CtxVariable[CtxValueT]]
260
- ) -> None:
261
- """Set context variables from mapping."""
262
-
263
- for name, var in variables.items():
264
- self[name] = var
265
-
266
- def get_root_attribute(self, name: str) -> Option[RootAttr]:
267
- """Get root attribute by name."""
268
-
269
- if self.is_root_attribute(name):
270
- for rattr in self.__root_attributes__:
271
- if rattr.name == name:
272
- return Some(rattr)
273
- return Nothing()
274
-
275
- def items(self) -> list[tuple[str, GlobalCtxVar[CtxValueT]]]:
276
- """Return context variables as set-like items."""
277
-
278
- return list(dict.items(self))
279
-
280
- def keys(self) -> list[str]:
281
- """Returns context variable names as keys."""
282
-
283
- return list(dict.keys(self))
284
-
285
- def values(self) -> list[GlobalCtxVar[CtxValueT]]:
286
- """Returns context variables as values."""
287
-
288
- return list(dict.values(self))
289
-
290
- def update(self, other: typing.Self) -> None:
291
- """Update context."""
292
-
293
- dict.update(dict(other.items()))
294
-
295
- def copy(self) -> typing.Self:
296
- """Copy context. Returns copied context without ctx_name."""
297
-
298
- return self.__class__(**self.dict())
299
-
300
- def dict(self) -> dict[str, GlobalCtxVar[CtxValueT]]:
301
- """Returns context as dict."""
302
-
303
- return {name: deepcopy(var) for name, var in self.items()}
304
-
305
- @typing.overload
306
- def pop(self, var_name: str) -> Option[GlobalCtxVar[CtxValueT]]: ...
307
-
308
- @typing.overload
309
- def pop(
310
- self,
311
- var_name: str,
312
- var_value_type: type[T],
313
- ) -> Option[GlobalCtxVar[T]]: ...
314
-
315
- def pop(self, var_name: str, var_value_type=object): # type: ignore
316
- """Pop context variable by name."""
317
-
318
- val = self.get(var_name, var_value_type) # type: ignore
319
- if val:
320
- del self[var_name]
321
- return val
322
- return Nothing()
323
-
324
- @typing.overload
325
- def get(self, var_name: str) -> Option[GlobalCtxVar[CtxValueT]]: ...
326
-
327
- @typing.overload
328
- def get(
329
- self,
330
- var_name: str,
331
- var_value_type: type[T],
332
- ) -> Option[GlobalCtxVar[T]]: ...
333
-
334
- def get(self, var_name, var_value_type=object): # type: ignore
335
- """Get context variable by name."""
336
-
337
- var_value_type = typing.Any if var_value_type is object else var_value_type
338
- generic_types = typing.get_args(get_orig_class(self))
339
- if generic_types and var_value_type is object:
340
- var_value_type = generic_types[0]
341
- var = dict.get(self, var_name)
342
- if var is None:
343
- return Nothing()
344
- assert type_check(
345
- var.value, var_value_type
346
- ), "Context variable value type of {!r} does not correspond to the expected type {!r}.".format(
347
- type(var.value).__name__,
348
- (
349
- getattr(var_value_type, "__name__")
350
- if isinstance(var_value_type, type)
351
- else repr(var_value_type)
352
- ),
353
- )
354
- return Some(var)
355
-
356
- @typing.overload
357
- def get_value(self, var_name: str) -> Option[CtxValueT]: ...
358
-
359
- @typing.overload
360
- def get_value(
361
- self,
362
- var_name: str,
363
- var_value_type: type[T],
364
- ) -> Option[T]: ...
365
-
366
- def get_value(self, var_name, var_value_type=object): # type: ignore
367
- """Get context variable value by name."""
368
-
369
- return self.get(var_name, var_value_type).map(lambda var: var.value)
370
-
371
- def rename(self, old_var_name: str, new_var_name: str) -> Result[_, str]:
372
- """Rename context variable."""
373
-
374
- var = self.get(old_var_name).unwrap()
375
- if var.const:
376
- return Error(f"Unable to rename variable {old_var_name!r}, " "because it's a constant.")
377
- del self[old_var_name]
378
- self[new_var_name] = var.value
379
- return Ok(_())
380
-
381
- def clear(self, *, include_consts: bool = False) -> None:
382
- """Clear context. If `include_consts = True`,
383
- then the context is completely cleared."""
384
-
385
- if not self:
386
- return
387
- if include_consts:
388
- logger.warning(
389
- "Constants from the global context {!r} have been cleaned up!",
390
- self.ctx_name + " at %#x" % id(self),
391
- )
392
- return dict.clear(self)
393
-
394
- for name, var in self.dict().items():
395
- if not var.const:
396
- del self[name]
397
-
398
- def delete_ctx(self) -> Result[_, str]:
399
- """Delete context by `ctx_name`."""
400
-
401
- if not self.__ctx_name__:
402
- return Error("Cannot delete unnamed context.")
403
- ctx = self.__storage__.get(self.ctx_name).unwrap()
404
- dict.clear(ctx)
405
- self.__storage__.delete(self.ctx_name)
406
- logger.warning(f"Global context {self.ctx_name!r} has been deleted!")
407
- return Ok(_())
408
-
409
-
410
- __all__ = (
1
+ import dataclasses
2
+ from copy import deepcopy
3
+ from functools import wraps
4
+
5
+ import typing_extensions as typing
6
+ from fntypes.co import Error, Nothing, Ok, Option, Result, Some
7
+
8
+ from telegrinder.modules import logger
9
+ from telegrinder.msgspec_utils import msgspec_convert
10
+
11
+ from .abc import ABCGlobalContext, CtxVar, CtxVariable, GlobalCtxVar
12
+
13
+ T = typing.TypeVar("T")
14
+ F = typing.TypeVar("F", bound=typing.Callable)
15
+ CtxValueT = typing.TypeVar("CtxValueT", default=typing.Any)
16
+
17
+ if typing.TYPE_CHECKING:
18
+ _: typing.TypeAlias = None
19
+ else:
20
+ _ = lambda: None
21
+
22
+
23
+ def type_check(value: object, value_type: type[T]) -> typing.TypeGuard[T]:
24
+ if value_type in (typing.Any, object):
25
+ return True
26
+ match msgspec_convert(value, value_type):
27
+ case Ok(v):
28
+ return type(value) is type(v)
29
+ case Error(_):
30
+ return False
31
+
32
+
33
+ def is_dunder(name: str) -> bool:
34
+ return name.startswith("__") and name.endswith("__")
35
+
36
+
37
+ def get_orig_class(obj: T) -> type[T]:
38
+ return getattr(obj, "__orig_class__", obj.__class__)
39
+
40
+
41
+ def root_protection(func: F) -> F:
42
+ if func.__name__ not in ("__setattr__", "__getattr__", "__delattr__"):
43
+ raise RuntimeError(
44
+ "You cannot decorate a {!r} function with this decorator, only "
45
+ "'__setattr__', __getattr__', '__delattr__' methods.".format(
46
+ func.__name__,
47
+ )
48
+ )
49
+
50
+ @wraps(func)
51
+ def wrapper(self: "GlobalContext", name: str, /, *args) -> typing.Any:
52
+ if self.is_root_attribute(name) and name in (self.__dict__ | self.__class__.__dict__):
53
+ root_attr = self.get_root_attribute(name).unwrap()
54
+ if all((not root_attr.can_be_rewritten, not root_attr.can_be_read)):
55
+ raise AttributeError(f"Unable to set, get, delete root attribute {name!r}.")
56
+ if func.__name__ == "__setattr__" and not root_attr.can_be_rewritten:
57
+ raise AttributeError(f"Unable to set root attribute {name!r}.")
58
+ if func.__name__ == "__getattr__" and not root_attr.can_be_read:
59
+ raise AttributeError(f"Unable to get root attribute {name!r}.")
60
+ if func.__name__ == "__delattr__":
61
+ raise AttributeError(f"Unable to delete root attribute {name!r}.")
62
+
63
+ return func(self, name, *args) # type: ignore
64
+
65
+ return wrapper # type: ignore
66
+
67
+
68
+ def ctx_var(*, default: T, frozen: bool = False) -> T:
69
+ """Example:
70
+ ```
71
+ class MyCtx(GlobalContext):
72
+ name: str
73
+ URL: str = ctx_var("https://google.com", frozen=True)
74
+
75
+ ctx = MyCtx(name=ctx_var("Alex", frozen=True))
76
+ ctx.URL #: 'https://google.com'
77
+ ctx.URL = '...' #: type checking error & exception 'TypeError'
78
+ ```
79
+ """
80
+ return typing.cast(T, CtxVar(default, const=frozen))
81
+
82
+
83
+ @dataclasses.dataclass(frozen=True, eq=False, slots=True)
84
+ class RootAttr:
85
+ name: str
86
+ can_be_read: bool = dataclasses.field(default=True, kw_only=True)
87
+ can_be_rewritten: bool = dataclasses.field(default=False, kw_only=True)
88
+
89
+ def __eq__(self, __value: str) -> bool:
90
+ return self.name == __value
91
+
92
+
93
+ @dataclasses.dataclass(repr=False, frozen=True, slots=True)
94
+ class Storage:
95
+ _storage: dict[str, "GlobalContext"] = dataclasses.field(
96
+ default_factory=lambda: {},
97
+ init=False,
98
+ )
99
+
100
+ def __repr__(self) -> str:
101
+ return "<ContextStorage: %s>" % ", ".join("ctx @" + repr(x) for x in self._storage)
102
+
103
+ @property
104
+ def storage(self) -> dict[str, "GlobalContext"]:
105
+ return self._storage.copy()
106
+
107
+ def set(self, name: str, ctx: "GlobalContext") -> None:
108
+ self._storage.setdefault(name, ctx)
109
+
110
+ def get(self, ctx_name: str) -> Option["GlobalContext"]:
111
+ ctx = self._storage.get(ctx_name)
112
+ return Some(ctx) if ctx is not None else Nothing()
113
+
114
+ def delete(self, ctx_name: str) -> None:
115
+ assert self._storage.pop(ctx_name, None) is not None, f"Context {ctx_name!r} is not defined in storage."
116
+
117
+
118
+ @typing.dataclass_transform(
119
+ kw_only_default=True,
120
+ order_default=True,
121
+ field_specifiers=(ctx_var,),
122
+ )
123
+ class GlobalContext(ABCGlobalContext, typing.Generic[CtxValueT], dict[str, GlobalCtxVar[CtxValueT]]):
124
+ """GlobalContext.
125
+
126
+ ```
127
+ ctx = GlobalContext()
128
+ ctx["client"] = Client()
129
+ ctx.address = CtxVar("128.0.0.7:8888", const=True)
130
+
131
+ def request():
132
+ data = {"user": "root_user", "password": "secret_password"}
133
+ ctx.client.request(ctx.address + "/login", data)
134
+ """
135
+
136
+ __ctx_name__: str | None
137
+ __storage__: typing.ClassVar[Storage] = Storage()
138
+ __root_attributes__: typing.ClassVar[tuple[RootAttr, ...]] = (
139
+ RootAttr("__ctx_name__"),
140
+ RootAttr("__root_attributes__"),
141
+ RootAttr("__storage__"),
142
+ )
143
+
144
+ def __new__(
145
+ cls,
146
+ ctx_name: str | None = None,
147
+ /,
148
+ **variables: typing.Any | CtxVar[CtxValueT],
149
+ ) -> typing.Self:
150
+ """Create or get from storage a new `GlobalContext` object."""
151
+ if not issubclass(GlobalContext, cls):
152
+ defaults = {}
153
+ for name in cls.__annotations__:
154
+ if name in cls.__dict__ and name not in cls.__root_attributes__:
155
+ defaults[name] = getattr(cls, name)
156
+ delattr(cls, name)
157
+ if isinstance(defaults[name], CtxVar) and defaults[name].const:
158
+ variables.pop(name, None)
159
+
160
+ variables = defaults | variables
161
+
162
+ ctx_name = getattr(cls, "__ctx_name__", ctx_name)
163
+ if ctx_name is None:
164
+ ctx = dict.__new__(cls)
165
+ elif ctx_name in cls.__storage__.storage:
166
+ ctx = cls.__storage__.get(ctx_name).unwrap()
167
+ else:
168
+ ctx = dict.__new__(cls, ctx_name)
169
+ cls.__storage__.set(ctx_name, ctx)
170
+
171
+ ctx.set_context_variables(variables)
172
+ return ctx # type: ignore
173
+
174
+ def __init__(
175
+ self,
176
+ ctx_name: str | None = None,
177
+ /,
178
+ **variables: CtxValueT | CtxVariable[CtxValueT],
179
+ ) -> None:
180
+ if not hasattr(self, "__ctx_name__"):
181
+ self.__ctx_name__ = ctx_name
182
+
183
+ if variables and not self:
184
+ self.set_context_variables(variables)
185
+
186
+ def __repr__(self) -> str:
187
+ return "<{} -> ({})>".format(
188
+ f"{self.__class__.__name__}@{self.ctx_name!r}",
189
+ ", ".join(repr(var) for var in self),
190
+ )
191
+
192
+ def __eq__(self, __value: "GlobalContext") -> bool:
193
+ """Returns True if the names of context stores
194
+ that use self and __value instances are equivalent.
195
+ """
196
+ return isinstance(__value, GlobalContext) and self.__ctx_name__ == __value.__ctx_name__
197
+
198
+ def __setitem__(self, __name: str, __value: CtxValueT | CtxVariable[CtxValueT]):
199
+ if is_dunder(__name):
200
+ raise NameError("Cannot set a context variable with dunder name.")
201
+ var = self.get(__name)
202
+ if var and var.unwrap().const:
203
+ raise TypeError(f"Unable to set variable {__name!r}, because it's a constant.")
204
+ dict.__setitem__(self, __name, GlobalCtxVar.collect(__name, __value))
205
+
206
+ def __getitem__(self, __name: str) -> CtxValueT:
207
+ return self.get(__name).unwrap().value # type: ignore
208
+
209
+ def __delitem__(self, __name: str):
210
+ var = self.get(__name).unwrap()
211
+ if var.const:
212
+ raise TypeError(f"Unable to delete variable {__name!r}, because it's a constant.")
213
+ dict.__delitem__(self, __name)
214
+
215
+ @root_protection
216
+ def __setattr__(self, __name: str, __value: CtxValueT | CtxVariable[CtxValueT]):
217
+ """Setting a context variable."""
218
+ if is_dunder(__name):
219
+ return object.__setattr__(self, __name, __value)
220
+ self.__setitem__(__name, __value)
221
+
222
+ @root_protection
223
+ def __getattr__(self, __name: str) -> CtxValueT:
224
+ """Getting a context variable."""
225
+ if is_dunder(__name):
226
+ return object.__getattribute__(self, __name)
227
+ return self.__getitem__(__name)
228
+
229
+ @root_protection
230
+ def __delattr__(self, __name: str) -> None:
231
+ """Removing a context variable."""
232
+ if is_dunder(__name):
233
+ return object.__delattr__(self, __name)
234
+ self.__delitem__(__name)
235
+
236
+ @property
237
+ def ctx_name(self) -> str:
238
+ """Context name."""
239
+ return self.__ctx_name__ or "<Unnamed ctx at %#x>" % id(self)
240
+
241
+ @classmethod
242
+ def is_root_attribute(cls, name: str) -> bool:
243
+ """Returns True if exists root attribute
244
+ otherwise False.
245
+ """
246
+ return name in cls.__root_attributes__
247
+
248
+ def set_context_variables(self, variables: typing.Mapping[str, CtxValueT | CtxVariable[CtxValueT]]) -> None:
249
+ """Set context variables from mapping."""
250
+ for name, var in variables.items():
251
+ self[name] = var
252
+
253
+ def get_root_attribute(self, name: str) -> Option[RootAttr]:
254
+ """Get root attribute by name."""
255
+ if self.is_root_attribute(name):
256
+ for rattr in self.__root_attributes__:
257
+ if rattr.name == name:
258
+ return Some(rattr)
259
+ return Nothing()
260
+
261
+ def items(self) -> list[tuple[str, GlobalCtxVar[CtxValueT]]]:
262
+ """Return context variables as set-like items."""
263
+ return list(dict.items(self))
264
+
265
+ def keys(self) -> list[str]:
266
+ """Returns context variable names as keys."""
267
+ return list(dict.keys(self))
268
+
269
+ def values(self) -> list[GlobalCtxVar[CtxValueT]]:
270
+ """Returns context variables as values."""
271
+ return list(dict.values(self))
272
+
273
+ def update(self, other: typing.Self) -> None:
274
+ """Update context."""
275
+ dict.update(dict(other.items()))
276
+
277
+ def copy(self) -> typing.Self:
278
+ """Copy context. Returns copied context without ctx_name."""
279
+ return self.__class__(**self.dict())
280
+
281
+ def dict(self) -> dict[str, GlobalCtxVar[CtxValueT]]:
282
+ """Returns context as dict."""
283
+ return {name: deepcopy(var) for name, var in self.items()} # type: ignore
284
+
285
+ @typing.overload
286
+ def pop(self, var_name: str) -> Option[GlobalCtxVar[CtxValueT]]: ...
287
+
288
+ @typing.overload
289
+ def pop(
290
+ self,
291
+ var_name: str,
292
+ var_value_type: type[T],
293
+ ) -> Option[GlobalCtxVar[T]]: ...
294
+
295
+ def pop(self, var_name: str, var_value_type=object): # type: ignore
296
+ """Pop context variable by name."""
297
+ val = self.get(var_name, var_value_type) # type: ignore
298
+ if val:
299
+ del self[var_name]
300
+ return val
301
+ return Nothing()
302
+
303
+ @typing.overload
304
+ def get(self, var_name: str) -> Option[GlobalCtxVar[CtxValueT]]: ...
305
+
306
+ @typing.overload
307
+ def get(
308
+ self,
309
+ var_name: str,
310
+ var_value_type: type[T],
311
+ ) -> Option[GlobalCtxVar[T]]: ...
312
+
313
+ def get(self, var_name, var_value_type=object): # type: ignore
314
+ """Get context variable by name."""
315
+ var_value_type = typing.Any if var_value_type is object else var_value_type
316
+ generic_types = typing.get_args(get_orig_class(self))
317
+ if generic_types and var_value_type is object:
318
+ var_value_type = generic_types[0]
319
+ var = dict.get(self, var_name)
320
+ if var is None:
321
+ return Nothing()
322
+ assert type_check(var.value, var_value_type), (
323
+ "Context variable value type of {!r} does not correspond to the expected type {!r}.".format(
324
+ type(var.value).__name__,
325
+ (
326
+ getattr(var_value_type, "__name__")
327
+ if isinstance(var_value_type, type)
328
+ else repr(var_value_type)
329
+ ),
330
+ )
331
+ )
332
+ return Some(var)
333
+
334
+ @typing.overload
335
+ def get_value(self, var_name: str) -> Option[CtxValueT]: ...
336
+
337
+ @typing.overload
338
+ def get_value(
339
+ self,
340
+ var_name: str,
341
+ var_value_type: type[T],
342
+ ) -> Option[T]: ...
343
+
344
+ def get_value(self, var_name, var_value_type=object): # type: ignore
345
+ """Get context variable value by name."""
346
+ return self.get(var_name, var_value_type).map(lambda var: var.value)
347
+
348
+ def rename(self, old_var_name: str, new_var_name: str) -> Result[_, str]:
349
+ """Rename context variable."""
350
+ var = self.get(old_var_name).unwrap()
351
+ if var.const:
352
+ return Error(f"Unable to rename variable {old_var_name!r}, because it's a constant.")
353
+ del self[old_var_name]
354
+ self[new_var_name] = var.value
355
+ return Ok(_())
356
+
357
+ def clear(self, *, include_consts: bool = False) -> None:
358
+ """Clear context. If `include_consts = True`,
359
+ then the context is completely cleared.
360
+ """
361
+ if not self:
362
+ return
363
+ if include_consts:
364
+ logger.warning(
365
+ "Constants from the global context {!r} have been cleaned up!",
366
+ self.ctx_name + " at %#x" % id(self),
367
+ )
368
+ return dict.clear(self)
369
+
370
+ for name, var in self.dict().items():
371
+ if not var.const:
372
+ del self[name]
373
+
374
+ def delete_ctx(self) -> Result[_, str]:
375
+ """Delete context by `ctx_name`."""
376
+ if not self.__ctx_name__:
377
+ return Error("Cannot delete unnamed context.")
378
+ ctx = self.__storage__.get(self.ctx_name).unwrap()
379
+ dict.clear(ctx)
380
+ self.__storage__.delete(self.ctx_name)
381
+ logger.warning(f"Global context {self.ctx_name!r} has been deleted!")
382
+ return Ok(_())
383
+
384
+
385
+ __all__ = (
411
386
  "ABCGlobalContext",
412
387
  "CtxVar",
413
388
  "CtxVariable",
@@ -419,5 +394,5 @@ __all__ = (
419
394
  "get_orig_class",
420
395
  "is_dunder",
421
396
  "root_protection",
422
- "type_check",
423
- )
397
+ "type_check",
398
+ )