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.
- telegrinder/__init__.py +258 -0
- telegrinder/__meta__.py +1 -0
- telegrinder/api/__init__.py +15 -0
- telegrinder/api/api.py +175 -0
- telegrinder/api/error.py +50 -0
- telegrinder/api/response.py +23 -0
- telegrinder/api/token.py +30 -0
- telegrinder/api/validators.py +30 -0
- telegrinder/bot/__init__.py +144 -0
- telegrinder/bot/bot.py +70 -0
- telegrinder/bot/cute_types/__init__.py +41 -0
- telegrinder/bot/cute_types/base.py +228 -0
- telegrinder/bot/cute_types/base.pyi +49 -0
- telegrinder/bot/cute_types/business_connection.py +9 -0
- telegrinder/bot/cute_types/business_messages_deleted.py +9 -0
- telegrinder/bot/cute_types/callback_query.py +248 -0
- telegrinder/bot/cute_types/chat_boost_removed.py +9 -0
- telegrinder/bot/cute_types/chat_boost_updated.py +9 -0
- telegrinder/bot/cute_types/chat_join_request.py +59 -0
- telegrinder/bot/cute_types/chat_member_updated.py +158 -0
- telegrinder/bot/cute_types/chosen_inline_result.py +11 -0
- telegrinder/bot/cute_types/inline_query.py +41 -0
- telegrinder/bot/cute_types/message.py +2809 -0
- telegrinder/bot/cute_types/message_reaction_count_updated.py +9 -0
- telegrinder/bot/cute_types/message_reaction_updated.py +9 -0
- telegrinder/bot/cute_types/paid_media_purchased.py +11 -0
- telegrinder/bot/cute_types/poll.py +9 -0
- telegrinder/bot/cute_types/poll_answer.py +9 -0
- telegrinder/bot/cute_types/pre_checkout_query.py +36 -0
- telegrinder/bot/cute_types/shipping_query.py +11 -0
- telegrinder/bot/cute_types/update.py +209 -0
- telegrinder/bot/cute_types/utils.py +141 -0
- telegrinder/bot/dispatch/__init__.py +99 -0
- telegrinder/bot/dispatch/abc.py +74 -0
- telegrinder/bot/dispatch/action.py +99 -0
- telegrinder/bot/dispatch/context.py +162 -0
- telegrinder/bot/dispatch/dispatch.py +362 -0
- telegrinder/bot/dispatch/handler/__init__.py +23 -0
- telegrinder/bot/dispatch/handler/abc.py +25 -0
- telegrinder/bot/dispatch/handler/audio_reply.py +43 -0
- telegrinder/bot/dispatch/handler/base.py +34 -0
- telegrinder/bot/dispatch/handler/document_reply.py +43 -0
- telegrinder/bot/dispatch/handler/func.py +73 -0
- telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
- telegrinder/bot/dispatch/handler/message_reply.py +35 -0
- telegrinder/bot/dispatch/handler/photo_reply.py +43 -0
- telegrinder/bot/dispatch/handler/sticker_reply.py +36 -0
- telegrinder/bot/dispatch/handler/video_reply.py +43 -0
- telegrinder/bot/dispatch/middleware/__init__.py +13 -0
- telegrinder/bot/dispatch/middleware/abc.py +112 -0
- telegrinder/bot/dispatch/middleware/box.py +32 -0
- telegrinder/bot/dispatch/middleware/filter.py +88 -0
- telegrinder/bot/dispatch/middleware/media_group.py +69 -0
- telegrinder/bot/dispatch/process.py +93 -0
- telegrinder/bot/dispatch/return_manager/__init__.py +21 -0
- telegrinder/bot/dispatch/return_manager/abc.py +107 -0
- telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
- telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
- telegrinder/bot/dispatch/return_manager/message.py +34 -0
- telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +19 -0
- telegrinder/bot/dispatch/return_manager/utils.py +20 -0
- telegrinder/bot/dispatch/router/__init__.py +4 -0
- telegrinder/bot/dispatch/router/abc.py +15 -0
- telegrinder/bot/dispatch/router/base.py +154 -0
- telegrinder/bot/dispatch/view/__init__.py +15 -0
- telegrinder/bot/dispatch/view/abc.py +15 -0
- telegrinder/bot/dispatch/view/base.py +226 -0
- telegrinder/bot/dispatch/view/box.py +207 -0
- telegrinder/bot/dispatch/view/media_group.py +25 -0
- telegrinder/bot/dispatch/waiter_machine/__init__.py +25 -0
- telegrinder/bot/dispatch/waiter_machine/actions.py +16 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +13 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +53 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +61 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +49 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +264 -0
- telegrinder/bot/dispatch/waiter_machine/middleware.py +77 -0
- telegrinder/bot/dispatch/waiter_machine/short_state.py +105 -0
- telegrinder/bot/polling/__init__.py +4 -0
- telegrinder/bot/polling/abc.py +25 -0
- telegrinder/bot/polling/error_handler.py +93 -0
- telegrinder/bot/polling/polling.py +167 -0
- telegrinder/bot/polling/utils.py +12 -0
- telegrinder/bot/rules/__init__.py +166 -0
- telegrinder/bot/rules/abc.py +150 -0
- telegrinder/bot/rules/button.py +20 -0
- telegrinder/bot/rules/callback_data.py +109 -0
- telegrinder/bot/rules/chat_join.py +28 -0
- telegrinder/bot/rules/chat_member_updated.py +145 -0
- telegrinder/bot/rules/command.py +137 -0
- telegrinder/bot/rules/enum_text.py +29 -0
- telegrinder/bot/rules/func.py +21 -0
- telegrinder/bot/rules/fuzzy.py +21 -0
- telegrinder/bot/rules/inline.py +45 -0
- telegrinder/bot/rules/integer.py +19 -0
- telegrinder/bot/rules/is_from.py +213 -0
- telegrinder/bot/rules/logic.py +22 -0
- telegrinder/bot/rules/magic.py +60 -0
- telegrinder/bot/rules/markup.py +51 -0
- telegrinder/bot/rules/media.py +13 -0
- telegrinder/bot/rules/mention.py +15 -0
- telegrinder/bot/rules/message_entities.py +37 -0
- telegrinder/bot/rules/node.py +43 -0
- telegrinder/bot/rules/payload.py +89 -0
- telegrinder/bot/rules/payment_invoice.py +14 -0
- telegrinder/bot/rules/regex.py +34 -0
- telegrinder/bot/rules/rule_enum.py +71 -0
- telegrinder/bot/rules/start.py +73 -0
- telegrinder/bot/rules/state.py +35 -0
- telegrinder/bot/rules/text.py +27 -0
- telegrinder/bot/rules/update.py +14 -0
- telegrinder/bot/scenario/__init__.py +5 -0
- telegrinder/bot/scenario/abc.py +16 -0
- telegrinder/bot/scenario/checkbox.py +183 -0
- telegrinder/bot/scenario/choice.py +44 -0
- telegrinder/client/__init__.py +11 -0
- telegrinder/client/abc.py +136 -0
- telegrinder/client/form_data.py +34 -0
- telegrinder/client/rnet.py +198 -0
- telegrinder/model.py +133 -0
- telegrinder/model.pyi +57 -0
- telegrinder/modules.py +1081 -0
- telegrinder/msgspec_utils/__init__.py +42 -0
- telegrinder/msgspec_utils/abc.py +16 -0
- telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
- telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
- telegrinder/msgspec_utils/custom_types/enum_meta.py +61 -0
- telegrinder/msgspec_utils/custom_types/literal.py +25 -0
- telegrinder/msgspec_utils/custom_types/option.py +17 -0
- telegrinder/msgspec_utils/decoder.py +388 -0
- telegrinder/msgspec_utils/encoder.py +204 -0
- telegrinder/msgspec_utils/json.py +15 -0
- telegrinder/msgspec_utils/tools.py +80 -0
- telegrinder/node/__init__.py +80 -0
- telegrinder/node/compose.py +193 -0
- telegrinder/node/nodes/__init__.py +96 -0
- telegrinder/node/nodes/attachment.py +169 -0
- telegrinder/node/nodes/callback_query.py +25 -0
- telegrinder/node/nodes/channel.py +97 -0
- telegrinder/node/nodes/command.py +33 -0
- telegrinder/node/nodes/error.py +43 -0
- telegrinder/node/nodes/event.py +70 -0
- telegrinder/node/nodes/file.py +39 -0
- telegrinder/node/nodes/global_node.py +66 -0
- telegrinder/node/nodes/i18n.py +110 -0
- telegrinder/node/nodes/me.py +26 -0
- telegrinder/node/nodes/message_entities.py +15 -0
- telegrinder/node/nodes/payload.py +84 -0
- telegrinder/node/nodes/reply_message.py +14 -0
- telegrinder/node/nodes/source.py +172 -0
- telegrinder/node/nodes/state_mutator.py +71 -0
- telegrinder/node/nodes/text.py +62 -0
- telegrinder/node/scope.py +88 -0
- telegrinder/node/utils.py +38 -0
- telegrinder/py.typed +0 -0
- telegrinder/rules.py +1 -0
- telegrinder/tools/__init__.py +183 -0
- telegrinder/tools/aio.py +147 -0
- telegrinder/tools/final.py +21 -0
- telegrinder/tools/formatting/__init__.py +85 -0
- telegrinder/tools/formatting/deep_links/__init__.py +39 -0
- telegrinder/tools/formatting/deep_links/links.py +468 -0
- telegrinder/tools/formatting/deep_links/parsing.py +88 -0
- telegrinder/tools/formatting/deep_links/validators.py +8 -0
- telegrinder/tools/formatting/html.py +241 -0
- telegrinder/tools/fullname.py +82 -0
- telegrinder/tools/global_context/__init__.py +13 -0
- telegrinder/tools/global_context/abc.py +63 -0
- telegrinder/tools/global_context/builtin_context.py +45 -0
- telegrinder/tools/global_context/global_context.py +614 -0
- telegrinder/tools/input_file_directory.py +30 -0
- telegrinder/tools/keyboard/__init__.py +6 -0
- telegrinder/tools/keyboard/abc.py +84 -0
- telegrinder/tools/keyboard/base.py +108 -0
- telegrinder/tools/keyboard/button.py +181 -0
- telegrinder/tools/keyboard/data.py +31 -0
- telegrinder/tools/keyboard/keyboard.py +160 -0
- telegrinder/tools/keyboard/utils.py +95 -0
- telegrinder/tools/lifespan.py +188 -0
- telegrinder/tools/limited_dict.py +35 -0
- telegrinder/tools/loop_wrapper.py +271 -0
- telegrinder/tools/magic/__init__.py +29 -0
- telegrinder/tools/magic/annotations.py +172 -0
- telegrinder/tools/magic/descriptors.py +57 -0
- telegrinder/tools/magic/function.py +254 -0
- telegrinder/tools/magic/inspect.py +16 -0
- telegrinder/tools/magic/shortcut.py +107 -0
- telegrinder/tools/member_descriptor_proxy.py +95 -0
- telegrinder/tools/parse_mode.py +12 -0
- telegrinder/tools/serialization/__init__.py +5 -0
- telegrinder/tools/serialization/abc.py +34 -0
- telegrinder/tools/serialization/json_ser.py +60 -0
- telegrinder/tools/serialization/msgpack_ser.py +197 -0
- telegrinder/tools/serialization/utils.py +18 -0
- telegrinder/tools/singleton/__init__.py +4 -0
- telegrinder/tools/singleton/abc.py +14 -0
- telegrinder/tools/singleton/singleton.py +18 -0
- telegrinder/tools/state_mutator/__init__.py +4 -0
- telegrinder/tools/state_mutator/mutation.py +85 -0
- telegrinder/tools/state_storage/__init__.py +4 -0
- telegrinder/tools/state_storage/abc.py +38 -0
- telegrinder/tools/state_storage/memory.py +27 -0
- telegrinder/tools/strings.py +22 -0
- telegrinder/types/__init__.py +323 -0
- telegrinder/types/enums.py +754 -0
- telegrinder/types/input_file.py +51 -0
- telegrinder/types/methods.py +6143 -0
- telegrinder/types/methods_utils.py +66 -0
- telegrinder/types/objects.py +8184 -0
- telegrinder/types/webapp.py +129 -0
- telegrinder/verification_utils.py +35 -0
- telegrinder-1.0.0rc1.dist-info/METADATA +166 -0
- telegrinder-1.0.0rc1.dist-info/RECORD +215 -0
- telegrinder-1.0.0rc1.dist-info/WHEEL +4 -0
- 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")
|