telegrinder 0.2.0.post2__py3-none-any.whl → 0.2.2__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.
- telegrinder/__init__.py +26 -7
- telegrinder/bot/__init__.py +16 -4
- telegrinder/bot/cute_types/chat_member_updated.py +1 -1
- telegrinder/bot/cute_types/message.py +18 -11
- telegrinder/bot/dispatch/__init__.py +18 -2
- telegrinder/bot/dispatch/abc.py +1 -1
- telegrinder/bot/dispatch/context.py +2 -2
- telegrinder/bot/dispatch/dispatch.py +1 -1
- telegrinder/bot/dispatch/handler/__init__.py +17 -1
- telegrinder/bot/dispatch/handler/audio_reply.py +44 -0
- telegrinder/bot/dispatch/handler/base.py +57 -0
- telegrinder/bot/dispatch/handler/document_reply.py +44 -0
- telegrinder/bot/dispatch/handler/func.py +3 -3
- telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
- telegrinder/bot/dispatch/handler/message_reply.py +12 -35
- telegrinder/bot/dispatch/handler/photo_reply.py +44 -0
- telegrinder/bot/dispatch/handler/sticker_reply.py +37 -0
- telegrinder/bot/dispatch/handler/video_reply.py +44 -0
- telegrinder/bot/dispatch/process.py +2 -2
- telegrinder/bot/dispatch/return_manager/abc.py +11 -8
- telegrinder/bot/dispatch/return_manager/callback_query.py +2 -2
- telegrinder/bot/dispatch/return_manager/inline_query.py +2 -2
- telegrinder/bot/dispatch/return_manager/message.py +3 -3
- telegrinder/bot/dispatch/view/__init__.py +2 -1
- telegrinder/bot/dispatch/view/abc.py +2 -181
- telegrinder/bot/dispatch/view/base.py +200 -0
- telegrinder/bot/dispatch/view/callback_query.py +3 -3
- telegrinder/bot/dispatch/view/chat_join_request.py +2 -2
- telegrinder/bot/dispatch/view/chat_member.py +2 -3
- telegrinder/bot/dispatch/view/inline_query.py +2 -2
- telegrinder/bot/dispatch/view/message.py +5 -4
- telegrinder/bot/dispatch/view/raw.py +4 -3
- telegrinder/bot/dispatch/waiter_machine/machine.py +18 -7
- telegrinder/bot/dispatch/waiter_machine/middleware.py +0 -8
- telegrinder/bot/dispatch/waiter_machine/short_state.py +1 -1
- telegrinder/bot/polling/polling.py +5 -2
- telegrinder/bot/rules/__init__.py +5 -2
- telegrinder/bot/rules/abc.py +6 -5
- telegrinder/bot/rules/adapter/__init__.py +1 -1
- telegrinder/bot/rules/integer.py +1 -1
- telegrinder/bot/rules/is_from.py +19 -0
- telegrinder/bot/rules/state.py +37 -0
- telegrinder/bot/scenario/checkbox.py +3 -3
- telegrinder/bot/scenario/choice.py +2 -2
- telegrinder/client/aiohttp.py +5 -7
- telegrinder/model.py +1 -8
- telegrinder/modules.py +16 -25
- telegrinder/msgspec_utils.py +5 -5
- telegrinder/node/base.py +2 -2
- telegrinder/node/composer.py +5 -9
- telegrinder/node/container.py +6 -1
- telegrinder/node/polymorphic.py +7 -7
- telegrinder/node/rule.py +6 -4
- telegrinder/node/scope.py +3 -3
- telegrinder/node/source.py +4 -2
- telegrinder/node/tools/generator.py +7 -6
- telegrinder/rules.py +2 -2
- telegrinder/tools/__init__.py +11 -7
- telegrinder/tools/loop_wrapper/loop_wrapper.py +4 -5
- telegrinder/tools/magic.py +17 -19
- telegrinder/tools/state_storage/__init__.py +4 -0
- telegrinder/tools/state_storage/abc.py +35 -0
- telegrinder/tools/state_storage/memory.py +25 -0
- telegrinder/types/__init__.py +1 -0
- telegrinder/types/methods.py +12 -4
- telegrinder/types/objects.py +57 -6
- {telegrinder-0.2.0.post2.dist-info → telegrinder-0.2.2.dist-info}/METADATA +3 -4
- {telegrinder-0.2.0.post2.dist-info → telegrinder-0.2.2.dist-info}/RECORD +70 -58
- {telegrinder-0.2.0.post2.dist-info → telegrinder-0.2.2.dist-info}/LICENSE +0 -0
- {telegrinder-0.2.0.post2.dist-info → telegrinder-0.2.2.dist-info}/WHEEL +0 -0
telegrinder/model.py
CHANGED
|
@@ -16,17 +16,10 @@ if typing.TYPE_CHECKING:
|
|
|
16
16
|
|
|
17
17
|
T = typing.TypeVar("T")
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
def rename_field(name: str) -> str:
|
|
21
|
-
if name.endswith("_") and name.removesuffix("_") in keyword.kwlist:
|
|
22
|
-
return name.removesuffix("_")
|
|
23
|
-
return name if not keyword.iskeyword(name) else name + "_"
|
|
24
|
-
|
|
25
|
-
|
|
26
19
|
MODEL_CONFIG: typing.Final[dict[str, typing.Any]] = {
|
|
27
20
|
"omit_defaults": True,
|
|
28
21
|
"dict": True,
|
|
29
|
-
"rename":
|
|
22
|
+
"rename": {kw + "_": kw for kw in keyword.kwlist},
|
|
30
23
|
}
|
|
31
24
|
|
|
32
25
|
|
telegrinder/modules.py
CHANGED
|
@@ -4,13 +4,6 @@ import typing
|
|
|
4
4
|
from choicelib import choice_in_order
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
@typing.runtime_checkable
|
|
8
|
-
class JSONModule(typing.Protocol):
|
|
9
|
-
def loads(self, s: str | bytes) -> typing.Any: ...
|
|
10
|
-
|
|
11
|
-
def dumps(self, o: typing.Any) -> str: ...
|
|
12
|
-
|
|
13
|
-
|
|
14
7
|
@typing.runtime_checkable
|
|
15
8
|
class LoggerModule(typing.Protocol):
|
|
16
9
|
def debug(self, __msg: object, *args: object, **kwargs: object) -> None: ...
|
|
@@ -25,25 +18,23 @@ class LoggerModule(typing.Protocol):
|
|
|
25
18
|
|
|
26
19
|
def exception(self, __msg: object, *args: object, **kwargs: object) -> None: ...
|
|
27
20
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
if typing.TYPE_CHECKING:
|
|
22
|
+
|
|
23
|
+
def set_level(
|
|
24
|
+
self,
|
|
25
|
+
level: typing.Literal[
|
|
26
|
+
"DEBUG",
|
|
27
|
+
"INFO",
|
|
28
|
+
"WARNING",
|
|
29
|
+
"ERROR",
|
|
30
|
+
"CRITICAL",
|
|
31
|
+
"EXCEPTION",
|
|
32
|
+
],
|
|
33
|
+
/,
|
|
34
|
+
) -> None: ...
|
|
39
35
|
|
|
40
36
|
|
|
41
37
|
logger: LoggerModule
|
|
42
|
-
json: JSONModule = choice_in_order(
|
|
43
|
-
["orjson", "ujson", "hyperjson"],
|
|
44
|
-
default="telegrinder.msgspec_json",
|
|
45
|
-
do_import=True,
|
|
46
|
-
)
|
|
47
38
|
logging_level = os.getenv("LOGGER_LEVEL", default="DEBUG").upper()
|
|
48
39
|
logging_module = choice_in_order(["loguru"], default="logging")
|
|
49
40
|
asyncio_module = choice_in_order(["uvloop"], default="asyncio")
|
|
@@ -227,7 +218,7 @@ if asyncio_module == "uvloop":
|
|
|
227
218
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) # type: ignore
|
|
228
219
|
|
|
229
220
|
|
|
230
|
-
def _set_logger_level(level):
|
|
221
|
+
def _set_logger_level(level, /):
|
|
231
222
|
level = level.upper()
|
|
232
223
|
if logging_module == "logging":
|
|
233
224
|
import logging
|
|
@@ -243,4 +234,4 @@ def _set_logger_level(level):
|
|
|
243
234
|
setattr(logger, "set_level", staticmethod(_set_logger_level)) # type: ignore
|
|
244
235
|
|
|
245
236
|
|
|
246
|
-
__all__ = ("
|
|
237
|
+
__all__ = ("LoggerModule", "logger")
|
telegrinder/msgspec_utils.py
CHANGED
|
@@ -29,7 +29,6 @@ else:
|
|
|
29
29
|
def __instancecheck__(cls, __instance: typing.Any) -> bool:
|
|
30
30
|
return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
|
|
31
31
|
|
|
32
|
-
|
|
33
32
|
class Option(typing.Generic[Value], metaclass=OptionMeta):
|
|
34
33
|
pass
|
|
35
34
|
|
|
@@ -63,9 +62,10 @@ def is_common_type(type_: typing.Any) -> typing.TypeGuard[type[typing.Any]]:
|
|
|
63
62
|
def type_check(obj: typing.Any, t: typing.Any) -> bool:
|
|
64
63
|
return (
|
|
65
64
|
isinstance(obj, t)
|
|
66
|
-
if isinstance(t, type)
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
if isinstance(t, type) and issubclass(t, msgspec.Struct)
|
|
66
|
+
else type(obj) in t
|
|
67
|
+
if isinstance(t, tuple)
|
|
68
|
+
else type(obj) is t
|
|
69
69
|
)
|
|
70
70
|
|
|
71
71
|
|
|
@@ -344,8 +344,8 @@ __all__ = (
|
|
|
344
344
|
"datetime",
|
|
345
345
|
"decoder",
|
|
346
346
|
"encoder",
|
|
347
|
-
"msgspec_convert",
|
|
348
347
|
"get_class_annotations",
|
|
349
348
|
"get_type_hints",
|
|
349
|
+
"msgspec_convert",
|
|
350
350
|
"msgspec_to_builtins",
|
|
351
351
|
)
|
telegrinder/node/base.py
CHANGED
|
@@ -12,8 +12,8 @@ from telegrinder.tools.magic import (
|
|
|
12
12
|
ComposeResult: typing.TypeAlias = typing.Awaitable[typing.Any] | typing.AsyncGenerator[typing.Any, None]
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def is_node(maybe_node:
|
|
16
|
-
maybe_node = typing.get_origin(maybe_node)
|
|
15
|
+
def is_node(maybe_node: typing.Any) -> typing.TypeGuard[type["Node"]]:
|
|
16
|
+
maybe_node = maybe_node if isinstance(maybe_node, type) else typing.get_origin(maybe_node)
|
|
17
17
|
return (
|
|
18
18
|
isinstance(maybe_node, type)
|
|
19
19
|
and issubclass(maybe_node, Node)
|
telegrinder/node/composer.py
CHANGED
|
@@ -4,7 +4,7 @@ import typing
|
|
|
4
4
|
from fntypes import Error, Ok, Result
|
|
5
5
|
from fntypes.error import UnwrapError
|
|
6
6
|
|
|
7
|
-
from telegrinder.api import API
|
|
7
|
+
from telegrinder.api.api import API
|
|
8
8
|
from telegrinder.bot.cute_types.update import Update, UpdateCute
|
|
9
9
|
from telegrinder.bot.dispatch.context import Context
|
|
10
10
|
from telegrinder.modules import logger
|
|
@@ -41,7 +41,7 @@ async def compose_node(
|
|
|
41
41
|
async def compose_nodes(
|
|
42
42
|
nodes: dict[str, type[Node]],
|
|
43
43
|
ctx: Context,
|
|
44
|
-
data: dict[type, typing.Any] | None = None,
|
|
44
|
+
data: dict[type[typing.Any], typing.Any] | None = None,
|
|
45
45
|
) -> Result["NodeCollection", ComposeError]:
|
|
46
46
|
logger.debug("Composing nodes: {!r}...", nodes)
|
|
47
47
|
|
|
@@ -67,11 +67,7 @@ async def compose_nodes(
|
|
|
67
67
|
local_nodes[node_t] = getattr(node_t, GLOBAL_VALUE_KEY)
|
|
68
68
|
continue
|
|
69
69
|
|
|
70
|
-
subnodes = {
|
|
71
|
-
k: session.value
|
|
72
|
-
for k, session in
|
|
73
|
-
(local_nodes | event_nodes).items()
|
|
74
|
-
}
|
|
70
|
+
subnodes = {k: session.value for k, session in (local_nodes | event_nodes).items()}
|
|
75
71
|
|
|
76
72
|
try:
|
|
77
73
|
local_nodes[node_t] = await compose_node(node_t, subnodes | data)
|
|
@@ -175,11 +171,11 @@ class Composition:
|
|
|
175
171
|
case Ok(col):
|
|
176
172
|
return col
|
|
177
173
|
case Error(err):
|
|
178
|
-
logger.debug(f"Composition failed with error: {err}")
|
|
174
|
+
logger.debug(f"Composition failed with error: {err!r}")
|
|
179
175
|
return None
|
|
180
176
|
|
|
181
177
|
async def __call__(self, **kwargs: typing.Any) -> typing.Any:
|
|
182
|
-
return await self.func(**magic_bundle(self.func, kwargs, start_idx=0, bundle_ctx=False))
|
|
178
|
+
return await self.func(**magic_bundle(self.func, kwargs, start_idx=0, bundle_ctx=False))
|
|
183
179
|
|
|
184
180
|
|
|
185
181
|
__all__ = ("Composition", "NodeCollection", "NodeSession", "compose_node", "compose_nodes")
|
telegrinder/node/container.py
CHANGED
|
@@ -12,7 +12,12 @@ class ContainerNode(Node):
|
|
|
12
12
|
|
|
13
13
|
@classmethod
|
|
14
14
|
def get_subnodes(cls) -> dict[str, type[Node]]:
|
|
15
|
-
|
|
15
|
+
subnodes = getattr(cls, "subnodes", None)
|
|
16
|
+
if subnodes is None:
|
|
17
|
+
subnodes_dct = {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
|
|
18
|
+
setattr(cls, "subnodes", subnodes_dct)
|
|
19
|
+
return subnodes_dct
|
|
20
|
+
return subnodes
|
|
16
21
|
|
|
17
22
|
@classmethod
|
|
18
23
|
def link_nodes(cls, linked_nodes: list[type[Node]]) -> type["ContainerNode"]:
|
telegrinder/node/polymorphic.py
CHANGED
|
@@ -13,21 +13,21 @@ from telegrinder.tools.magic import get_impls, impl
|
|
|
13
13
|
class Polymorphic(Node):
|
|
14
14
|
@classmethod
|
|
15
15
|
async def compose(cls, update: UpdateNode, context: Context) -> typing.Any:
|
|
16
|
-
logger.debug(f"Composing polimorphic node {cls.__name__}")
|
|
16
|
+
logger.debug(f"Composing polimorphic node {cls.__name__!r}...")
|
|
17
17
|
scope = getattr(cls, "scope", None)
|
|
18
18
|
node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
19
19
|
|
|
20
|
-
for i,
|
|
21
|
-
logger.debug("Checking impl {}",
|
|
22
|
-
composition = Composition(
|
|
20
|
+
for i, impl_ in enumerate(get_impls(cls)):
|
|
21
|
+
logger.debug("Checking impl {!r}...", impl_.__name__)
|
|
22
|
+
composition = Composition(impl_, True)
|
|
23
23
|
node_collection = await composition.compose_nodes(update, context)
|
|
24
24
|
if node_collection is None:
|
|
25
|
-
logger.debug("Impl {!r} composition failed",
|
|
25
|
+
logger.debug("Impl {!r} composition failed!", impl_.__name__)
|
|
26
26
|
continue
|
|
27
27
|
|
|
28
28
|
# To determine whether this is a right morph, all subnodes must be resolved
|
|
29
29
|
if scope is NodeScope.PER_EVENT and (cls, i) in node_ctx:
|
|
30
|
-
logger.debug("Morph is already cached as per_event node, using its value. Impl {!r} succeeded",
|
|
30
|
+
logger.debug("Morph is already cached as per_event node, using its value. Impl {!r} succeeded!", impl_.__name__)
|
|
31
31
|
res: NodeSession = node_ctx[(cls, i)]
|
|
32
32
|
await node_collection.close_all()
|
|
33
33
|
return res.value
|
|
@@ -40,7 +40,7 @@ class Polymorphic(Node):
|
|
|
40
40
|
node_ctx[(cls, i)] = NodeSession(cls, result, {})
|
|
41
41
|
|
|
42
42
|
await node_collection.close_all(with_value=result)
|
|
43
|
-
logger.debug("Impl {!r} succeeded with value {}",
|
|
43
|
+
logger.debug("Impl {!r} succeeded with value: {}", impl_.__name__, result)
|
|
44
44
|
return result
|
|
45
45
|
|
|
46
46
|
raise ComposeError("No implementation found.")
|
telegrinder/node/rule.py
CHANGED
|
@@ -11,7 +11,7 @@ if typing.TYPE_CHECKING:
|
|
|
11
11
|
from telegrinder.bot.rules.abc import ABCRule
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class RuleChain(dict[str, typing.Any]):
|
|
14
|
+
class RuleChain(dict[str, typing.Any], Node):
|
|
15
15
|
dataclass = dict
|
|
16
16
|
rules: tuple["ABCRule", ...] = ()
|
|
17
17
|
|
|
@@ -28,7 +28,6 @@ class RuleChain(dict[str, typing.Any]):
|
|
|
28
28
|
def __class_getitem__(cls, items: "ABCRule | tuple[ABCRule, ...]", /) -> typing.Self:
|
|
29
29
|
if not isinstance(items, tuple):
|
|
30
30
|
items = (items,)
|
|
31
|
-
assert all(isinstance(rule, "ABCRule") for rule in items), "All items must be instances of 'ABCRule'."
|
|
32
31
|
return cls(*items)
|
|
33
32
|
|
|
34
33
|
@staticmethod
|
|
@@ -37,6 +36,7 @@ class RuleChain(dict[str, typing.Any]):
|
|
|
37
36
|
|
|
38
37
|
@classmethod
|
|
39
38
|
async def compose(cls, update: UpdateNode) -> typing.Any:
|
|
39
|
+
# Hack to avoid circular import
|
|
40
40
|
globalns = globals()
|
|
41
41
|
if "check_rule" not in globalns:
|
|
42
42
|
globalns.update(
|
|
@@ -54,7 +54,9 @@ class RuleChain(dict[str, typing.Any]):
|
|
|
54
54
|
raise ComposeError(f"Rule {rule!r} failed!")
|
|
55
55
|
|
|
56
56
|
try:
|
|
57
|
-
|
|
57
|
+
if dataclasses.is_dataclass(cls.dataclass):
|
|
58
|
+
return cls.dataclass(**{k: ctx[k] for k in cls.__annotations__})
|
|
59
|
+
return cls.dataclass(**ctx)
|
|
58
60
|
except Exception as exc:
|
|
59
61
|
raise ComposeError(f"Dataclass validation error: {exc}")
|
|
60
62
|
|
|
@@ -63,7 +65,7 @@ class RuleChain(dict[str, typing.Any]):
|
|
|
63
65
|
return cls
|
|
64
66
|
|
|
65
67
|
@classmethod
|
|
66
|
-
def get_subnodes(cls) -> dict:
|
|
68
|
+
def get_subnodes(cls) -> dict[typing.Literal["update"], type[UpdateNode]]:
|
|
67
69
|
return {"update": UpdateNode}
|
|
68
70
|
|
|
69
71
|
@classmethod
|
telegrinder/node/scope.py
CHANGED
telegrinder/node/source.py
CHANGED
|
@@ -39,7 +39,9 @@ class Source(Polymorphic, DataNode):
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
@impl
|
|
42
|
-
async def compose_chat_join_request(
|
|
42
|
+
async def compose_chat_join_request(
|
|
43
|
+
cls, chat_join_request: EventNode[ChatJoinRequestCute]
|
|
44
|
+
) -> typing.Self:
|
|
43
45
|
return cls(
|
|
44
46
|
api=chat_join_request.ctx_api,
|
|
45
47
|
chat=chat_join_request.chat,
|
|
@@ -68,4 +70,4 @@ class UserSource(ScalarNode, User):
|
|
|
68
70
|
return source.from_user
|
|
69
71
|
|
|
70
72
|
|
|
71
|
-
__all__ = ("
|
|
73
|
+
__all__ = ("ChatSource", "Source", "UserSource")
|
|
@@ -20,13 +20,13 @@ def error_on_none(value: T | None) -> T:
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def generate_node(
|
|
23
|
-
subnodes: tuple[type[
|
|
24
|
-
func: typing.Callable[...,
|
|
23
|
+
subnodes: tuple[type[Node], ...],
|
|
24
|
+
func: typing.Callable[..., typing.Any],
|
|
25
25
|
casts: tuple[typing.Callable[[typing.Any], typing.Any], ...] = (cast_false_to_none, error_on_none),
|
|
26
|
-
) -> type[
|
|
27
|
-
async def compose(**kw
|
|
26
|
+
) -> type[Node]:
|
|
27
|
+
async def compose(cls, **kw) -> typing.Any:
|
|
28
28
|
args = await ContainerNode.compose(**kw)
|
|
29
|
-
result = func(*args)
|
|
29
|
+
result = func(*args) # type: ignore
|
|
30
30
|
if inspect.isawaitable(result):
|
|
31
31
|
result = await result
|
|
32
32
|
for cast in casts:
|
|
@@ -34,7 +34,8 @@ def generate_node(
|
|
|
34
34
|
return result
|
|
35
35
|
|
|
36
36
|
container = ContainerNode.link_nodes(list(subnodes))
|
|
37
|
-
|
|
37
|
+
compose.__annotations__ = container.get_subnodes()
|
|
38
|
+
return type("_ContainerNode", (container,), {"compose": classmethod(compose)})
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
__all__ = ("generate_node",)
|
telegrinder/rules.py
CHANGED
|
@@ -26,7 +26,6 @@ __all__ = (
|
|
|
26
26
|
"InlineQueryMarkup",
|
|
27
27
|
"InlineQueryRule",
|
|
28
28
|
"InlineQueryText",
|
|
29
|
-
"IsInteger",
|
|
30
29
|
"IntegerInRange",
|
|
31
30
|
"InviteLinkByCreator",
|
|
32
31
|
"InviteLinkName",
|
|
@@ -39,6 +38,7 @@ __all__ = (
|
|
|
39
38
|
"IsForward",
|
|
40
39
|
"IsForwardType",
|
|
41
40
|
"IsGroup",
|
|
41
|
+
"IsInteger",
|
|
42
42
|
"IsLanguageCode",
|
|
43
43
|
"IsPremium",
|
|
44
44
|
"IsPrivate",
|
|
@@ -50,11 +50,11 @@ __all__ = (
|
|
|
50
50
|
"Markup",
|
|
51
51
|
"MessageEntities",
|
|
52
52
|
"MessageRule",
|
|
53
|
+
"NodeRule",
|
|
53
54
|
"NotRule",
|
|
54
55
|
"OrRule",
|
|
55
56
|
"Regex",
|
|
56
57
|
"RuleEnum",
|
|
57
58
|
"StartCommand",
|
|
58
59
|
"Text",
|
|
59
|
-
"NodeRule",
|
|
60
60
|
)
|
telegrinder/tools/__init__.py
CHANGED
|
@@ -67,12 +67,14 @@ from .limited_dict import LimitedDict
|
|
|
67
67
|
from .loop_wrapper import ABCLoopWrapper, DelayedTask, Lifespan, LoopWrapper
|
|
68
68
|
from .magic import impl, magic_bundle, resolve_arg_names
|
|
69
69
|
from .parse_mode import ParseMode
|
|
70
|
+
from .state_storage import ABCStateStorage, MemoryStateStorage, StateData
|
|
70
71
|
|
|
71
72
|
__all__ = (
|
|
72
73
|
"ABCErrorHandler",
|
|
73
74
|
"ABCGlobalContext",
|
|
74
75
|
"ABCI18n",
|
|
75
76
|
"ABCLoopWrapper",
|
|
77
|
+
"ABCStateStorage",
|
|
76
78
|
"ABCTranslator",
|
|
77
79
|
"ABCTranslatorMiddleware",
|
|
78
80
|
"AnyMarkup",
|
|
@@ -100,6 +102,7 @@ __all__ = (
|
|
|
100
102
|
"LimitedDict",
|
|
101
103
|
"Link",
|
|
102
104
|
"LoopWrapper",
|
|
105
|
+
"MemoryStateStorage",
|
|
103
106
|
"Mention",
|
|
104
107
|
"ParseMode",
|
|
105
108
|
"PreCode",
|
|
@@ -110,8 +113,14 @@ __all__ = (
|
|
|
110
113
|
"SpecialFormat",
|
|
111
114
|
"StartBotLink",
|
|
112
115
|
"StartGroupLink",
|
|
116
|
+
"StateData",
|
|
113
117
|
"TelegrinderContext",
|
|
114
118
|
"TgEmoji",
|
|
119
|
+
"block_quote",
|
|
120
|
+
"bold",
|
|
121
|
+
"channel_boost_link",
|
|
122
|
+
"code_inline",
|
|
123
|
+
"ctx_var",
|
|
115
124
|
"escape",
|
|
116
125
|
"get_channel_boost_link",
|
|
117
126
|
"get_invite_chat_link",
|
|
@@ -119,12 +128,14 @@ __all__ = (
|
|
|
119
128
|
"get_resolve_domain_link",
|
|
120
129
|
"get_start_bot_link",
|
|
121
130
|
"get_start_group_link",
|
|
131
|
+
"impl",
|
|
122
132
|
"invite_chat_link",
|
|
123
133
|
"italic",
|
|
124
134
|
"link",
|
|
125
135
|
"magic_bundle",
|
|
126
136
|
"mention",
|
|
127
137
|
"pre_code",
|
|
138
|
+
"resolve_arg_names",
|
|
128
139
|
"resolve_domain",
|
|
129
140
|
"spoiler",
|
|
130
141
|
"start_bot_link",
|
|
@@ -132,11 +143,4 @@ __all__ = (
|
|
|
132
143
|
"strike",
|
|
133
144
|
"tg_emoji",
|
|
134
145
|
"underline",
|
|
135
|
-
"bold",
|
|
136
|
-
"channel_boost_link",
|
|
137
|
-
"code_inline",
|
|
138
|
-
"ctx_var",
|
|
139
|
-
"block_quote",
|
|
140
|
-
"impl",
|
|
141
|
-
"resolve_arg_names",
|
|
142
146
|
)
|
|
@@ -44,17 +44,16 @@ class DelayedTask(typing.Generic[CoroFunc]):
|
|
|
44
44
|
def is_cancelled(self) -> bool:
|
|
45
45
|
return self._cancelled
|
|
46
46
|
|
|
47
|
-
async def __call__(self, *args, **kwargs) -> None:
|
|
47
|
+
async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
|
48
48
|
while not self.is_cancelled:
|
|
49
49
|
await asyncio.sleep(self.seconds)
|
|
50
50
|
if self.is_cancelled:
|
|
51
51
|
break
|
|
52
52
|
try:
|
|
53
53
|
await self.handler(*args, **kwargs)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
break
|
|
54
|
+
finally:
|
|
55
|
+
if not self.repeat:
|
|
56
|
+
break
|
|
58
57
|
|
|
59
58
|
def cancel(self) -> None:
|
|
60
59
|
if not self._cancelled:
|
telegrinder/tools/magic.py
CHANGED
|
@@ -6,7 +6,7 @@ from functools import wraps
|
|
|
6
6
|
|
|
7
7
|
if typing.TYPE_CHECKING:
|
|
8
8
|
from telegrinder.bot.rules.abc import ABCRule
|
|
9
|
-
from telegrinder.node.
|
|
9
|
+
from telegrinder.node.polymorphic import Polymorphic
|
|
10
10
|
|
|
11
11
|
T = typing.TypeVar("T", bound=ABCRule)
|
|
12
12
|
F = typing.TypeVar(
|
|
@@ -23,7 +23,6 @@ IMPL_MARK: typing.Final[str] = "_is_impl"
|
|
|
23
23
|
|
|
24
24
|
def cache_magic_value(mark_key: str, /):
|
|
25
25
|
def inner(func: "F") -> "F":
|
|
26
|
-
|
|
27
26
|
@wraps(func)
|
|
28
27
|
def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
|
|
29
28
|
if mark_key not in args[0].__dict__:
|
|
@@ -31,6 +30,7 @@ def cache_magic_value(mark_key: str, /):
|
|
|
31
30
|
return args[0].__dict__[mark_key]
|
|
32
31
|
|
|
33
32
|
return wrapper # type: ignore
|
|
33
|
+
|
|
34
34
|
return inner
|
|
35
35
|
|
|
36
36
|
|
|
@@ -43,13 +43,13 @@ def get_default_args(func: FuncType) -> dict[str, typing.Any]:
|
|
|
43
43
|
fspec = inspect.getfullargspec(func)
|
|
44
44
|
if not fspec.defaults:
|
|
45
45
|
return {}
|
|
46
|
-
return dict(zip(fspec.args[-len(fspec.defaults):], fspec.defaults))
|
|
46
|
+
return dict(zip(fspec.args[-len(fspec.defaults) :], fspec.defaults))
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def get_annotations(func: FuncType, *, return_type: bool = False) -> dict[str, typing.Any]:
|
|
50
50
|
annotations = func.__annotations__
|
|
51
51
|
if not return_type and "return" in func.__annotations__:
|
|
52
|
-
|
|
52
|
+
annotations.pop("return")
|
|
53
53
|
return annotations
|
|
54
54
|
|
|
55
55
|
|
|
@@ -60,13 +60,11 @@ def to_str(s: str | enum.Enum) -> str:
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
@typing.overload
|
|
63
|
-
def magic_bundle(handler: FuncType, kw: dict[str, typing.Any]) -> dict[str, typing.Any]:
|
|
64
|
-
...
|
|
63
|
+
def magic_bundle(handler: FuncType, kw: dict[str, typing.Any]) -> dict[str, typing.Any]: ...
|
|
65
64
|
|
|
66
65
|
|
|
67
66
|
@typing.overload
|
|
68
|
-
def magic_bundle(handler: FuncType, kw: dict[enum.Enum, typing.Any]) -> dict[str, typing.Any]:
|
|
69
|
-
...
|
|
67
|
+
def magic_bundle(handler: FuncType, kw: dict[enum.Enum, typing.Any]) -> dict[str, typing.Any]: ...
|
|
70
68
|
|
|
71
69
|
|
|
72
70
|
@typing.overload
|
|
@@ -76,8 +74,7 @@ def magic_bundle(
|
|
|
76
74
|
*,
|
|
77
75
|
start_idx: int = 1,
|
|
78
76
|
bundle_ctx: bool = True,
|
|
79
|
-
) -> dict[str, typing.Any]:
|
|
80
|
-
...
|
|
77
|
+
) -> dict[str, typing.Any]: ...
|
|
81
78
|
|
|
82
79
|
|
|
83
80
|
@typing.overload
|
|
@@ -87,8 +84,7 @@ def magic_bundle(
|
|
|
87
84
|
*,
|
|
88
85
|
start_idx: int = 1,
|
|
89
86
|
bundle_ctx: bool = True,
|
|
90
|
-
) -> dict[str, typing.Any]:
|
|
91
|
-
...
|
|
87
|
+
) -> dict[str, typing.Any]: ...
|
|
92
88
|
|
|
93
89
|
|
|
94
90
|
@typing.overload
|
|
@@ -97,8 +93,7 @@ def magic_bundle(
|
|
|
97
93
|
kw: dict[type, typing.Any],
|
|
98
94
|
*,
|
|
99
95
|
typebundle: typing.Literal[True] = True,
|
|
100
|
-
) -> dict[str, typing.Any]:
|
|
101
|
-
...
|
|
96
|
+
) -> dict[str, typing.Any]: ...
|
|
102
97
|
|
|
103
98
|
|
|
104
99
|
def magic_bundle(
|
|
@@ -109,7 +104,6 @@ def magic_bundle(
|
|
|
109
104
|
bundle_ctx: bool = True,
|
|
110
105
|
typebundle: bool = False,
|
|
111
106
|
) -> dict[str, typing.Any]:
|
|
112
|
-
|
|
113
107
|
if typebundle:
|
|
114
108
|
types = get_annotations(handler, return_type=False)
|
|
115
109
|
bundle: dict[str, typing.Any] = {}
|
|
@@ -141,13 +135,17 @@ def impl(method: typing.Callable[..., typing.Any]):
|
|
|
141
135
|
return classmethod(method)
|
|
142
136
|
|
|
143
137
|
|
|
144
|
-
def get_impls(cls: type["
|
|
145
|
-
|
|
138
|
+
def get_impls(cls: type["Polymorphic"]) -> list[typing.Callable[..., typing.Any]]:
|
|
139
|
+
moprh_impls = getattr(cls, "__morph_impls__", None)
|
|
140
|
+
if moprh_impls is not None:
|
|
141
|
+
return moprh_impls
|
|
142
|
+
impls = [
|
|
146
143
|
func.__func__
|
|
147
144
|
for func in vars(cls).values()
|
|
148
145
|
if isinstance(func, classmethod) and getattr(func.__func__, IMPL_MARK, False)
|
|
149
146
|
]
|
|
150
|
-
|
|
147
|
+
setattr(cls, "__morph_impls__", impls)
|
|
148
|
+
return impls
|
|
151
149
|
|
|
152
150
|
|
|
153
151
|
__all__ = (
|
|
@@ -158,8 +156,8 @@ __all__ = (
|
|
|
158
156
|
"get_cached_translation",
|
|
159
157
|
"get_default_args",
|
|
160
158
|
"get_default_args",
|
|
161
|
-
"impl",
|
|
162
159
|
"get_impls",
|
|
160
|
+
"impl",
|
|
163
161
|
"magic_bundle",
|
|
164
162
|
"resolve_arg_names",
|
|
165
163
|
"to_str",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import enum
|
|
3
|
+
import typing
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from fntypes import Option
|
|
7
|
+
|
|
8
|
+
from telegrinder.bot.rules.state import State, StateMeta
|
|
9
|
+
|
|
10
|
+
Payload = typing.TypeVar("Payload")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class StateData(typing.Generic[Payload]):
|
|
15
|
+
key: str | enum.Enum
|
|
16
|
+
payload: Payload
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ABCStateStorage(abc.ABC, typing.Generic[Payload]):
|
|
20
|
+
@abc.abstractmethod
|
|
21
|
+
async def get(self, user_id: int) -> Option[StateData[Payload]]: ...
|
|
22
|
+
|
|
23
|
+
@abc.abstractmethod
|
|
24
|
+
async def delete(self, user_id: int) -> None: ...
|
|
25
|
+
|
|
26
|
+
@abc.abstractmethod
|
|
27
|
+
async def set(self, user_id: int, key: str | enum.Enum, payload: Payload) -> None: ...
|
|
28
|
+
|
|
29
|
+
def State(self, key: str | StateMeta | enum.Enum = StateMeta.ANY, /) -> State[Payload]: # noqa: N802
|
|
30
|
+
"""Can be used as a shortcut to get a state rule dependant on current storage."""
|
|
31
|
+
|
|
32
|
+
return State(storage=self, key=key)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = ("ABCStateStorage", "StateData")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from fntypes import Nothing, Option, Some
|
|
4
|
+
|
|
5
|
+
from telegrinder.tools.state_storage.abc import ABCStateStorage, StateData
|
|
6
|
+
|
|
7
|
+
Payload: typing.TypeAlias = dict[str, typing.Any]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MemoryStateStorage(ABCStateStorage[Payload]):
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self.storage: dict[int, StateData[Payload]] = {}
|
|
13
|
+
|
|
14
|
+
async def get(self, user_id: int) -> Option[StateData[Payload]]:
|
|
15
|
+
state = self.storage.get(user_id)
|
|
16
|
+
return Some(state) if state is not None else Nothing()
|
|
17
|
+
|
|
18
|
+
async def set(self, user_id: int, key: str, payload: Payload) -> None:
|
|
19
|
+
self.storage[user_id] = StateData(key, payload)
|
|
20
|
+
|
|
21
|
+
async def delete(self, user_id: int) -> None:
|
|
22
|
+
self.storage.pop(user_id)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = ("MemoryStateStorage",)
|