telegrinder 0.3.4.post1__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.
- telegrinder/__init__.py +30 -31
- telegrinder/api/__init__.py +2 -1
- telegrinder/api/api.py +28 -20
- telegrinder/api/error.py +8 -4
- telegrinder/api/response.py +2 -2
- telegrinder/api/token.py +2 -2
- telegrinder/bot/__init__.py +6 -0
- telegrinder/bot/bot.py +38 -31
- telegrinder/bot/cute_types/__init__.py +2 -0
- telegrinder/bot/cute_types/base.py +54 -128
- telegrinder/bot/cute_types/callback_query.py +76 -61
- telegrinder/bot/cute_types/chat_join_request.py +4 -3
- telegrinder/bot/cute_types/chat_member_updated.py +28 -31
- telegrinder/bot/cute_types/inline_query.py +5 -4
- telegrinder/bot/cute_types/message.py +555 -602
- telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
- telegrinder/bot/cute_types/update.py +20 -12
- telegrinder/bot/cute_types/utils.py +3 -36
- telegrinder/bot/dispatch/__init__.py +4 -0
- telegrinder/bot/dispatch/abc.py +8 -9
- telegrinder/bot/dispatch/context.py +5 -7
- telegrinder/bot/dispatch/dispatch.py +85 -33
- telegrinder/bot/dispatch/handler/abc.py +5 -6
- telegrinder/bot/dispatch/handler/audio_reply.py +2 -2
- telegrinder/bot/dispatch/handler/base.py +3 -3
- telegrinder/bot/dispatch/handler/document_reply.py +2 -2
- telegrinder/bot/dispatch/handler/func.py +36 -42
- telegrinder/bot/dispatch/handler/media_group_reply.py +5 -4
- telegrinder/bot/dispatch/handler/message_reply.py +2 -2
- telegrinder/bot/dispatch/handler/photo_reply.py +2 -2
- telegrinder/bot/dispatch/handler/sticker_reply.py +2 -2
- telegrinder/bot/dispatch/handler/video_reply.py +2 -2
- telegrinder/bot/dispatch/middleware/abc.py +83 -8
- telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
- telegrinder/bot/dispatch/process.py +44 -50
- telegrinder/bot/dispatch/return_manager/__init__.py +2 -0
- telegrinder/bot/dispatch/return_manager/abc.py +6 -10
- telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +20 -0
- telegrinder/bot/dispatch/view/__init__.py +2 -0
- telegrinder/bot/dispatch/view/abc.py +10 -6
- telegrinder/bot/dispatch/view/base.py +81 -50
- telegrinder/bot/dispatch/view/box.py +20 -9
- telegrinder/bot/dispatch/view/callback_query.py +3 -4
- telegrinder/bot/dispatch/view/chat_join_request.py +2 -7
- telegrinder/bot/dispatch/view/chat_member.py +3 -5
- telegrinder/bot/dispatch/view/inline_query.py +3 -4
- telegrinder/bot/dispatch/view/message.py +3 -4
- telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
- telegrinder/bot/dispatch/view/raw.py +42 -40
- telegrinder/bot/dispatch/waiter_machine/actions.py +5 -4
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +9 -7
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/state.py +3 -2
- telegrinder/bot/dispatch/waiter_machine/machine.py +113 -34
- telegrinder/bot/dispatch/waiter_machine/middleware.py +15 -10
- telegrinder/bot/dispatch/waiter_machine/short_state.py +7 -18
- telegrinder/bot/polling/polling.py +62 -54
- telegrinder/bot/rules/__init__.py +24 -1
- telegrinder/bot/rules/abc.py +17 -10
- telegrinder/bot/rules/callback_data.py +20 -61
- telegrinder/bot/rules/chat_join.py +6 -4
- telegrinder/bot/rules/command.py +4 -4
- telegrinder/bot/rules/enum_text.py +1 -4
- telegrinder/bot/rules/func.py +5 -3
- telegrinder/bot/rules/fuzzy.py +1 -1
- telegrinder/bot/rules/id.py +24 -0
- telegrinder/bot/rules/inline.py +6 -4
- telegrinder/bot/rules/integer.py +2 -1
- telegrinder/bot/rules/logic.py +18 -0
- telegrinder/bot/rules/markup.py +5 -6
- telegrinder/bot/rules/message.py +2 -4
- telegrinder/bot/rules/message_entities.py +1 -3
- telegrinder/bot/rules/node.py +15 -9
- telegrinder/bot/rules/payload.py +81 -0
- telegrinder/bot/rules/payment_invoice.py +29 -0
- telegrinder/bot/rules/regex.py +5 -6
- telegrinder/bot/rules/state.py +1 -3
- telegrinder/bot/rules/text.py +10 -5
- telegrinder/bot/rules/update.py +0 -0
- telegrinder/bot/scenario/abc.py +2 -4
- telegrinder/bot/scenario/checkbox.py +12 -14
- telegrinder/bot/scenario/choice.py +6 -9
- telegrinder/client/__init__.py +9 -1
- telegrinder/client/abc.py +35 -10
- telegrinder/client/aiohttp.py +28 -24
- telegrinder/client/form_data.py +31 -0
- telegrinder/client/sonic.py +212 -0
- telegrinder/model.py +38 -145
- telegrinder/modules.py +3 -1
- telegrinder/msgspec_utils.py +136 -68
- telegrinder/node/__init__.py +74 -13
- telegrinder/node/attachment.py +92 -16
- telegrinder/node/base.py +196 -68
- telegrinder/node/callback_query.py +17 -16
- telegrinder/node/command.py +3 -2
- telegrinder/node/composer.py +40 -75
- telegrinder/node/container.py +13 -7
- telegrinder/node/either.py +82 -0
- telegrinder/node/event.py +20 -31
- telegrinder/node/file.py +51 -0
- telegrinder/node/me.py +4 -5
- telegrinder/node/payload.py +78 -0
- telegrinder/node/polymorphic.py +27 -8
- telegrinder/node/rule.py +2 -6
- telegrinder/node/scope.py +4 -6
- telegrinder/node/source.py +37 -21
- telegrinder/node/text.py +20 -8
- telegrinder/node/tools/generator.py +7 -11
- telegrinder/py.typed +0 -0
- telegrinder/rules.py +0 -61
- telegrinder/tools/__init__.py +97 -38
- telegrinder/tools/adapter/__init__.py +19 -0
- telegrinder/tools/adapter/abc.py +49 -0
- telegrinder/tools/adapter/dataclass.py +56 -0
- telegrinder/{bot/rules → tools}/adapter/event.py +8 -10
- telegrinder/{bot/rules → tools}/adapter/node.py +8 -10
- telegrinder/{bot/rules → tools}/adapter/raw_event.py +2 -2
- telegrinder/{bot/rules → tools}/adapter/raw_update.py +2 -2
- telegrinder/tools/buttons.py +52 -26
- telegrinder/tools/callback_data_serilization/__init__.py +5 -0
- telegrinder/tools/callback_data_serilization/abc.py +51 -0
- telegrinder/tools/callback_data_serilization/json_ser.py +60 -0
- telegrinder/tools/callback_data_serilization/msgpack_ser.py +172 -0
- telegrinder/tools/error_handler/abc.py +4 -7
- telegrinder/tools/error_handler/error.py +0 -0
- telegrinder/tools/error_handler/error_handler.py +34 -48
- telegrinder/tools/formatting/__init__.py +57 -37
- telegrinder/tools/formatting/deep_links.py +541 -0
- telegrinder/tools/formatting/{html.py → html_formatter.py} +51 -79
- telegrinder/tools/formatting/spec_html_formats.py +14 -60
- telegrinder/tools/functional.py +1 -5
- telegrinder/tools/global_context/global_context.py +26 -51
- telegrinder/tools/global_context/telegrinder_ctx.py +3 -3
- telegrinder/tools/i18n/abc.py +0 -0
- telegrinder/tools/i18n/middleware/abc.py +3 -6
- telegrinder/tools/input_file_directory.py +30 -0
- telegrinder/tools/keyboard.py +9 -9
- telegrinder/tools/lifespan.py +105 -0
- telegrinder/tools/limited_dict.py +5 -10
- telegrinder/tools/loop_wrapper/abc.py +7 -2
- telegrinder/tools/loop_wrapper/loop_wrapper.py +40 -95
- telegrinder/tools/magic.py +184 -34
- telegrinder/tools/state_storage/__init__.py +0 -0
- telegrinder/tools/state_storage/abc.py +5 -9
- telegrinder/tools/state_storage/memory.py +1 -1
- telegrinder/tools/strings.py +13 -0
- telegrinder/types/__init__.py +8 -0
- telegrinder/types/enums.py +31 -21
- telegrinder/types/input_file.py +51 -0
- telegrinder/types/methods.py +531 -109
- telegrinder/types/objects.py +934 -826
- telegrinder/verification_utils.py +0 -2
- {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/LICENSE +2 -2
- telegrinder-0.4.0.dist-info/METADATA +144 -0
- telegrinder-0.4.0.dist-info/RECORD +182 -0
- {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/WHEEL +1 -1
- telegrinder/bot/rules/adapter/__init__.py +0 -17
- telegrinder/bot/rules/adapter/abc.py +0 -31
- telegrinder/node/message.py +0 -14
- telegrinder/node/update.py +0 -15
- telegrinder/tools/formatting/links.py +0 -38
- telegrinder/tools/kb_set/__init__.py +0 -4
- telegrinder/tools/kb_set/base.py +0 -15
- telegrinder/tools/kb_set/yaml.py +0 -63
- telegrinder-0.3.4.post1.dist-info/METADATA +0 -110
- telegrinder-0.3.4.post1.dist-info/RECORD +0 -165
- /telegrinder/{bot/rules → tools}/adapter/errors.py +0 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import re
|
|
2
|
-
import typing
|
|
3
2
|
|
|
4
3
|
import vbml
|
|
5
4
|
|
|
@@ -16,12 +15,13 @@ class TelegrinderContext(GlobalContext):
|
|
|
16
15
|
ctx1 = TelegrinderContext()
|
|
17
16
|
ctx2 = GlobalContext("telegrinder") # same, but without the type-hints
|
|
18
17
|
assert ctx1 == ctx2 # ok
|
|
19
|
-
```
|
|
18
|
+
```
|
|
19
|
+
"""
|
|
20
20
|
|
|
21
21
|
__ctx_name__ = "telegrinder"
|
|
22
22
|
|
|
23
23
|
vbml_pattern_flags: re.RegexFlag | None = None
|
|
24
|
-
vbml_patcher:
|
|
24
|
+
vbml_patcher: vbml.Patcher = ctx_var(default=vbml.Patcher(), frozen=True)
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
__all__ = ("TelegrinderContext",)
|
telegrinder/tools/i18n/abc.py
CHANGED
|
File without changes
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import typing
|
|
2
1
|
from abc import abstractmethod
|
|
3
2
|
|
|
4
3
|
from telegrinder.bot.cute_types.base import BaseCute
|
|
@@ -6,18 +5,16 @@ from telegrinder.bot.dispatch.context import Context
|
|
|
6
5
|
from telegrinder.bot.dispatch.middleware import ABCMiddleware
|
|
7
6
|
from telegrinder.tools.i18n import ABCI18n, I18nEnum
|
|
8
7
|
|
|
9
|
-
T = typing.TypeVar("T", bound=BaseCute)
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
class ABCTranslatorMiddleware(ABCMiddleware[T]):
|
|
9
|
+
class ABCTranslatorMiddleware[Event: BaseCute](ABCMiddleware[Event]):
|
|
13
10
|
def __init__(self, i18n: ABCI18n) -> None:
|
|
14
11
|
self.i18n = i18n
|
|
15
12
|
|
|
16
13
|
@abstractmethod
|
|
17
|
-
async def get_locale(self, event:
|
|
14
|
+
async def get_locale(self, event: Event) -> str:
|
|
18
15
|
pass
|
|
19
16
|
|
|
20
|
-
async def pre(self, event:
|
|
17
|
+
async def pre(self, event: Event, ctx: Context) -> bool:
|
|
21
18
|
ctx[I18nEnum.I18N] = self.i18n.get_translator_by_locale(await self.get_locale(event))
|
|
22
19
|
return True
|
|
23
20
|
|
|
@@ -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",)
|
telegrinder/tools/keyboard.py
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import typing
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
|
-
from types import NoneType
|
|
5
4
|
|
|
6
|
-
from fntypes.option import
|
|
5
|
+
from fntypes.option import Some
|
|
7
6
|
|
|
7
|
+
from telegrinder.model import is_none
|
|
8
8
|
from telegrinder.types.objects import (
|
|
9
9
|
InlineKeyboardMarkup,
|
|
10
10
|
ReplyKeyboardMarkup,
|
|
11
11
|
ReplyKeyboardRemove,
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
-
from .buttons import Button, InlineButton,
|
|
14
|
+
from .buttons import BaseButton, Button, InlineButton, RowButtons
|
|
15
15
|
|
|
16
|
-
DictStrAny
|
|
17
|
-
AnyMarkup
|
|
16
|
+
type DictStrAny = dict[str, typing.Any]
|
|
17
|
+
type AnyMarkup = InlineKeyboardMarkup | ReplyKeyboardMarkup
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def copy_keyboard(keyboard: list[list[DictStrAny]]) -> list[list[DictStrAny]]:
|
|
@@ -30,7 +30,7 @@ class KeyboardModel:
|
|
|
30
30
|
keyboard: list[list[DictStrAny]]
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
class ABCMarkup
|
|
33
|
+
class ABCMarkup[KeyboardButton: BaseButton](ABC):
|
|
34
34
|
BUTTON: type[KeyboardButton]
|
|
35
35
|
keyboard: list[list[DictStrAny]]
|
|
36
36
|
|
|
@@ -46,8 +46,8 @@ class ABCMarkup(ABC, typing.Generic[KeyboardButton]):
|
|
|
46
46
|
def get_empty_markup(cls) -> AnyMarkup:
|
|
47
47
|
return cls().get_markup()
|
|
48
48
|
|
|
49
|
-
def add(self, row_or_button: RowButtons[KeyboardButton] | KeyboardButton) -> typing.Self:
|
|
50
|
-
if not
|
|
49
|
+
def add(self, row_or_button: RowButtons[KeyboardButton] | KeyboardButton, /) -> typing.Self:
|
|
50
|
+
if not self.keyboard:
|
|
51
51
|
self.row()
|
|
52
52
|
|
|
53
53
|
if isinstance(row_or_button, RowButtons):
|
|
@@ -99,7 +99,7 @@ class Keyboard(ABCMarkup[Button], KeyboardModel):
|
|
|
99
99
|
return {
|
|
100
100
|
k: v.unwrap() if v and isinstance(v, Some) else v
|
|
101
101
|
for k, v in dataclasses.asdict(self).items()
|
|
102
|
-
if
|
|
102
|
+
if not is_none(v)
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
def get_markup(self) -> ReplyKeyboardMarkup:
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import dataclasses
|
|
3
|
+
import datetime
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
type CoroutineTask[T] = typing.Coroutine[typing.Any, typing.Any, T]
|
|
7
|
+
type CoroutineFunc[**P, T] = typing.Callable[P, CoroutineTask[T]]
|
|
8
|
+
type Task[**P, T] = CoroutineFunc[P, T] | CoroutineTask[T] | DelayedTask[typing.Callable[P, CoroutineTask[T]]]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_tasks(
|
|
12
|
+
tasks: list[CoroutineTask[typing.Any]],
|
|
13
|
+
/,
|
|
14
|
+
) -> None:
|
|
15
|
+
loop = asyncio.get_event_loop()
|
|
16
|
+
while tasks:
|
|
17
|
+
loop.run_until_complete(tasks.pop(0))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def to_coroutine_task[**P, T](task: Task[P, T], /) -> CoroutineTask[T]:
|
|
21
|
+
if asyncio.iscoroutinefunction(task) or isinstance(task, DelayedTask):
|
|
22
|
+
task = task()
|
|
23
|
+
elif not asyncio.iscoroutine(task):
|
|
24
|
+
raise TypeError("Task should be coroutine or coroutine function.")
|
|
25
|
+
return task
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclasses.dataclass(slots=True)
|
|
29
|
+
class DelayedTask[Handler: CoroutineFunc[..., typing.Any]]:
|
|
30
|
+
handler: Handler
|
|
31
|
+
seconds: float | datetime.timedelta
|
|
32
|
+
repeat: bool = dataclasses.field(default=False, kw_only=True)
|
|
33
|
+
_cancelled: bool = dataclasses.field(default=False, init=False, repr=False)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def is_cancelled(self) -> bool:
|
|
37
|
+
return self._cancelled
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def delay(self) -> float:
|
|
41
|
+
return self.seconds if isinstance(self.seconds, int | float) else self.seconds.total_seconds()
|
|
42
|
+
|
|
43
|
+
async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
|
|
44
|
+
while not self.is_cancelled:
|
|
45
|
+
await asyncio.sleep(self.delay)
|
|
46
|
+
if self.is_cancelled:
|
|
47
|
+
break
|
|
48
|
+
try:
|
|
49
|
+
await self.handler(*args, **kwargs)
|
|
50
|
+
finally:
|
|
51
|
+
if not self.repeat:
|
|
52
|
+
break
|
|
53
|
+
|
|
54
|
+
def cancel(self) -> None:
|
|
55
|
+
if not self._cancelled:
|
|
56
|
+
self._cancelled = True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
60
|
+
class Lifespan:
|
|
61
|
+
startup_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
|
|
62
|
+
shutdown_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
|
|
63
|
+
|
|
64
|
+
def on_startup[**P, T](self, task: Task[P, T], /) -> Task[P, T]:
|
|
65
|
+
self.startup_tasks.append(to_coroutine_task(task))
|
|
66
|
+
return task
|
|
67
|
+
|
|
68
|
+
def on_shutdown[**P, T](self, task: Task[P, T], /) -> Task[P, T]:
|
|
69
|
+
self.shutdown_tasks.append(to_coroutine_task(task))
|
|
70
|
+
return task
|
|
71
|
+
|
|
72
|
+
def start(self) -> None:
|
|
73
|
+
run_tasks(self.startup_tasks)
|
|
74
|
+
|
|
75
|
+
def shutdown(self) -> None:
|
|
76
|
+
run_tasks(self.shutdown_tasks)
|
|
77
|
+
|
|
78
|
+
def __enter__(self) -> None:
|
|
79
|
+
self.start()
|
|
80
|
+
|
|
81
|
+
def __exit__(self) -> None:
|
|
82
|
+
self.shutdown()
|
|
83
|
+
|
|
84
|
+
async def __aenter__(self) -> None:
|
|
85
|
+
for task in self.startup_tasks:
|
|
86
|
+
await task
|
|
87
|
+
|
|
88
|
+
async def __aexit__(self, *args) -> None:
|
|
89
|
+
for task in self.shutdown_tasks:
|
|
90
|
+
await task
|
|
91
|
+
|
|
92
|
+
def __add__(self, other: typing.Self, /) -> typing.Self:
|
|
93
|
+
return self.__class__(
|
|
94
|
+
startup_tasks=self.startup_tasks + other.startup_tasks,
|
|
95
|
+
shutdown_tasks=self.shutdown_tasks + other.shutdown_tasks,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
__all__ = (
|
|
100
|
+
"CoroutineTask",
|
|
101
|
+
"DelayedTask",
|
|
102
|
+
"Lifespan",
|
|
103
|
+
"run_tasks",
|
|
104
|
+
"to_coroutine_task",
|
|
105
|
+
)
|
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
import typing
|
|
2
1
|
from collections import UserDict, deque
|
|
3
2
|
|
|
4
|
-
KT = typing.TypeVar("KT")
|
|
5
|
-
VT = typing.TypeVar("VT")
|
|
6
3
|
|
|
7
|
-
|
|
8
|
-
class LimitedDict(UserDict[KT, VT]):
|
|
4
|
+
class LimitedDict[Key, Value](UserDict[Key, Value]):
|
|
9
5
|
def __init__(self, *, maxlimit: int = 1000) -> None:
|
|
10
6
|
super().__init__()
|
|
11
7
|
self.maxlimit = maxlimit
|
|
12
|
-
self.queue: deque[
|
|
8
|
+
self.queue: deque[Key] = deque(maxlen=maxlimit)
|
|
13
9
|
|
|
14
|
-
def set(self, key:
|
|
10
|
+
def set(self, key: Key, value: Value, /) -> Value | None:
|
|
15
11
|
"""Set item in the dictionary.
|
|
16
12
|
Returns a value that was deleted when the limit in the dictionary
|
|
17
13
|
was reached, otherwise None.
|
|
18
14
|
"""
|
|
19
|
-
|
|
20
15
|
deleted_item = None
|
|
21
16
|
if len(self.queue) >= self.maxlimit:
|
|
22
17
|
deleted_item = self.pop(self.queue.popleft(), None)
|
|
@@ -25,10 +20,10 @@ class LimitedDict(UserDict[KT, VT]):
|
|
|
25
20
|
super().__setitem__(key, value)
|
|
26
21
|
return deleted_item
|
|
27
22
|
|
|
28
|
-
def __setitem__(self, key:
|
|
23
|
+
def __setitem__(self, key: Key, value: Value, /) -> None:
|
|
29
24
|
self.set(key, value)
|
|
30
25
|
|
|
31
|
-
def __delitem__(self, key:
|
|
26
|
+
def __delitem__(self, key: Key) -> None:
|
|
32
27
|
if key in self.queue:
|
|
33
28
|
self.queue.remove(key)
|
|
34
29
|
return super().__delitem__(key)
|
|
@@ -3,13 +3,18 @@ from abc import ABC, abstractmethod
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class ABCLoopWrapper(ABC):
|
|
6
|
+
@property
|
|
6
7
|
@abstractmethod
|
|
7
|
-
def
|
|
8
|
+
def is_running(self) -> bool:
|
|
8
9
|
pass
|
|
9
10
|
|
|
10
11
|
@abstractmethod
|
|
11
|
-
def
|
|
12
|
+
def add_task(self, task: typing.Any, /) -> None:
|
|
12
13
|
pass
|
|
13
14
|
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def run_event_loop(self) -> typing.NoReturn:
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
14
19
|
|
|
15
20
|
__all__ = ("ABCLoopWrapper",)
|
|
@@ -1,84 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import contextlib
|
|
3
|
-
import dataclasses
|
|
4
3
|
import datetime
|
|
5
4
|
import typing
|
|
6
5
|
|
|
7
6
|
from telegrinder.modules import logger
|
|
8
|
-
from telegrinder.tools.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
CoroutineFunc: typing.TypeAlias = typing.Callable[P, CoroutineTask[T]]
|
|
16
|
-
Task: typing.TypeAlias = (
|
|
17
|
-
"CoroutineFunc[P, T] | CoroutineTask[T] | DelayedTask[typing.Callable[P, CoroutineTask[T]]]"
|
|
7
|
+
from telegrinder.tools.lifespan import (
|
|
8
|
+
CoroutineFunc,
|
|
9
|
+
CoroutineTask,
|
|
10
|
+
DelayedTask,
|
|
11
|
+
Lifespan,
|
|
12
|
+
Task,
|
|
13
|
+
to_coroutine_task,
|
|
18
14
|
)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def run_tasks(
|
|
22
|
-
tasks: list[CoroutineTask[typing.Any]],
|
|
23
|
-
loop: asyncio.AbstractEventLoop,
|
|
24
|
-
) -> None:
|
|
25
|
-
while tasks:
|
|
26
|
-
loop.run_until_complete(tasks.pop(0))
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def to_coroutine_task(task: Task) -> CoroutineTask[typing.Any]:
|
|
30
|
-
if asyncio.iscoroutinefunction(task) or isinstance(task, DelayedTask):
|
|
31
|
-
task = task()
|
|
32
|
-
elif not asyncio.iscoroutine(task):
|
|
33
|
-
raise TypeError("Task should be coroutine or coroutine function.")
|
|
34
|
-
return task
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@dataclasses.dataclass(slots=True)
|
|
38
|
-
class DelayedTask(typing.Generic[CoroFunc]):
|
|
39
|
-
handler: CoroFunc
|
|
40
|
-
seconds: float
|
|
41
|
-
repeat: bool = dataclasses.field(default=False, kw_only=True)
|
|
42
|
-
_cancelled: bool = dataclasses.field(default=False, init=False, repr=False)
|
|
43
|
-
|
|
44
|
-
@property
|
|
45
|
-
def is_cancelled(self) -> bool:
|
|
46
|
-
return self._cancelled
|
|
47
|
-
|
|
48
|
-
async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
|
49
|
-
while not self.is_cancelled:
|
|
50
|
-
await asyncio.sleep(self.seconds)
|
|
51
|
-
if self.is_cancelled:
|
|
52
|
-
break
|
|
53
|
-
try:
|
|
54
|
-
await self.handler(*args, **kwargs)
|
|
55
|
-
finally:
|
|
56
|
-
if not self.repeat:
|
|
57
|
-
break
|
|
58
|
-
|
|
59
|
-
def cancel(self) -> None:
|
|
60
|
-
if not self._cancelled:
|
|
61
|
-
self._cancelled = True
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
65
|
-
class Lifespan:
|
|
66
|
-
startup_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
|
|
67
|
-
shutdown_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
|
|
68
|
-
|
|
69
|
-
def on_startup(self, task: Task, /) -> Task:
|
|
70
|
-
self.startup_tasks.append(to_coroutine_task(task))
|
|
71
|
-
return task
|
|
72
|
-
|
|
73
|
-
def on_shutdown(self, task: Task, /) -> Task:
|
|
74
|
-
self.shutdown_tasks.append(to_coroutine_task(task))
|
|
75
|
-
return task
|
|
76
|
-
|
|
77
|
-
def start(self, loop: asyncio.AbstractEventLoop, /) -> None:
|
|
78
|
-
run_tasks(self.startup_tasks, loop)
|
|
79
|
-
|
|
80
|
-
def shutdown(self, loop: asyncio.AbstractEventLoop, /) -> None:
|
|
81
|
-
run_tasks(self.shutdown_tasks, loop)
|
|
15
|
+
from telegrinder.tools.loop_wrapper.abc import ABCLoopWrapper
|
|
16
|
+
from telegrinder.tools.magic import cancel_future
|
|
82
17
|
|
|
83
18
|
|
|
84
19
|
class LoopWrapper(ABCLoopWrapper):
|
|
@@ -87,15 +22,20 @@ class LoopWrapper(ABCLoopWrapper):
|
|
|
87
22
|
*,
|
|
88
23
|
tasks: list[CoroutineTask[typing.Any]] | None = None,
|
|
89
24
|
lifespan: Lifespan | None = None,
|
|
90
|
-
event_loop: asyncio.AbstractEventLoop | None = None,
|
|
91
25
|
) -> None:
|
|
92
26
|
self.tasks: list[CoroutineTask[typing.Any]] = tasks or []
|
|
93
27
|
self.lifespan = lifespan or Lifespan()
|
|
94
|
-
self._loop =
|
|
28
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def is_running(self) -> bool:
|
|
32
|
+
if self._loop is None:
|
|
33
|
+
return False
|
|
34
|
+
return self._loop.is_running()
|
|
95
35
|
|
|
96
36
|
@property
|
|
97
37
|
def loop(self) -> asyncio.AbstractEventLoop:
|
|
98
|
-
assert self._loop is not None
|
|
38
|
+
assert self._loop is not None, "Loop is not set."
|
|
99
39
|
return self._loop
|
|
100
40
|
|
|
101
41
|
def __repr__(self) -> str:
|
|
@@ -106,15 +46,22 @@ class LoopWrapper(ABCLoopWrapper):
|
|
|
106
46
|
self.lifespan,
|
|
107
47
|
)
|
|
108
48
|
|
|
109
|
-
def
|
|
49
|
+
async def _run_tasks(self) -> None:
|
|
50
|
+
async with asyncio.TaskGroup() as tg:
|
|
51
|
+
while self.tasks:
|
|
52
|
+
tg.create_task(self.tasks.pop(0))
|
|
53
|
+
|
|
54
|
+
def run_event_loop(self) -> typing.NoReturn: # type: ignore
|
|
110
55
|
if not self.tasks:
|
|
111
|
-
logger.warning("
|
|
56
|
+
logger.warning("Run loop without tasks!")
|
|
112
57
|
|
|
113
|
-
|
|
114
|
-
|
|
58
|
+
try:
|
|
59
|
+
self._loop = asyncio.get_running_loop()
|
|
60
|
+
except RuntimeError:
|
|
61
|
+
self._loop = asyncio.get_event_loop()
|
|
115
62
|
|
|
116
|
-
|
|
117
|
-
|
|
63
|
+
self.lifespan.start()
|
|
64
|
+
self._loop.create_task(self._run_tasks())
|
|
118
65
|
|
|
119
66
|
tasks = asyncio.all_tasks(self._loop)
|
|
120
67
|
try:
|
|
@@ -125,19 +72,19 @@ class LoopWrapper(ABCLoopWrapper):
|
|
|
125
72
|
for task_result in tasks_results:
|
|
126
73
|
try:
|
|
127
74
|
task_result.result()
|
|
128
|
-
except BaseException
|
|
129
|
-
logger.exception(
|
|
75
|
+
except BaseException:
|
|
76
|
+
logger.exception("Traceback message below:")
|
|
130
77
|
tasks = asyncio.all_tasks(self._loop)
|
|
131
78
|
except KeyboardInterrupt:
|
|
132
79
|
print() # blank print for ^C
|
|
133
80
|
logger.info("Caught KeyboardInterrupt, cancellation...")
|
|
134
81
|
self.complete_tasks(tasks)
|
|
135
82
|
finally:
|
|
136
|
-
self.lifespan.shutdown(
|
|
83
|
+
self.lifespan.shutdown()
|
|
137
84
|
if self._loop.is_running():
|
|
138
85
|
self._loop.close()
|
|
139
86
|
|
|
140
|
-
def add_task(self, task: Task) -> None:
|
|
87
|
+
def add_task(self, task: Task[..., typing.Any], /) -> None:
|
|
141
88
|
task = to_coroutine_task(task)
|
|
142
89
|
|
|
143
90
|
if self._loop is not None and self._loop.is_running():
|
|
@@ -145,12 +92,10 @@ class LoopWrapper(ABCLoopWrapper):
|
|
|
145
92
|
else:
|
|
146
93
|
self.tasks.append(task)
|
|
147
94
|
|
|
148
|
-
def complete_tasks(self, tasks: set[asyncio.Task[typing.Any]]) -> None:
|
|
95
|
+
def complete_tasks(self, tasks: set[asyncio.Task[typing.Any]], /) -> None:
|
|
149
96
|
tasks = tasks | asyncio.all_tasks(self.loop)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
with contextlib.suppress(asyncio.CancelledError):
|
|
153
|
-
self.loop.run_until_complete(task_to_cancel)
|
|
97
|
+
with contextlib.suppress(asyncio.CancelledError, asyncio.InvalidStateError):
|
|
98
|
+
self.loop.run_until_complete(cancel_future(asyncio.gather(*tasks, return_exceptions=True)))
|
|
154
99
|
|
|
155
100
|
@typing.overload
|
|
156
101
|
def timer(self, *, seconds: datetime.timedelta) -> typing.Callable[..., typing.Any]: ...
|
|
@@ -180,7 +125,7 @@ class LoopWrapper(ABCLoopWrapper):
|
|
|
180
125
|
seconds += hours * 60 * 60
|
|
181
126
|
seconds += days * 24 * 60 * 60
|
|
182
127
|
|
|
183
|
-
def decorator(func:
|
|
128
|
+
def decorator[Func: CoroutineFunc[..., typing.Any]](func: Func) -> Func:
|
|
184
129
|
self.add_task(DelayedTask(func, seconds, repeat=False))
|
|
185
130
|
return func
|
|
186
131
|
|
|
@@ -214,11 +159,11 @@ class LoopWrapper(ABCLoopWrapper):
|
|
|
214
159
|
seconds += hours * 60 * 60
|
|
215
160
|
seconds += days * 24 * 60 * 60
|
|
216
161
|
|
|
217
|
-
def decorator(func:
|
|
162
|
+
def decorator[Func: CoroutineFunc[..., typing.Any]](func: Func) -> Func:
|
|
218
163
|
self.add_task(DelayedTask(func, seconds, repeat=True))
|
|
219
164
|
return func
|
|
220
165
|
|
|
221
166
|
return decorator
|
|
222
167
|
|
|
223
168
|
|
|
224
|
-
__all__ = ("DelayedTask", "
|
|
169
|
+
__all__ = ("DelayedTask", "LoopWrapper", "to_coroutine_task")
|