telegrinder 0.1.dev19__py3-none-any.whl → 0.1.dev158__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 +129 -22
- telegrinder/api/__init__.py +11 -2
- telegrinder/api/abc.py +25 -9
- telegrinder/api/api.py +29 -24
- telegrinder/api/error.py +14 -4
- telegrinder/api/response.py +11 -7
- telegrinder/bot/__init__.py +68 -7
- telegrinder/bot/bot.py +30 -24
- telegrinder/bot/cute_types/__init__.py +11 -1
- telegrinder/bot/cute_types/base.py +47 -0
- telegrinder/bot/cute_types/callback_query.py +64 -14
- telegrinder/bot/cute_types/inline_query.py +22 -16
- telegrinder/bot/cute_types/message.py +163 -43
- telegrinder/bot/cute_types/update.py +23 -0
- telegrinder/bot/dispatch/__init__.py +56 -3
- telegrinder/bot/dispatch/abc.py +9 -7
- telegrinder/bot/dispatch/composition.py +74 -0
- telegrinder/bot/dispatch/context.py +71 -0
- telegrinder/bot/dispatch/dispatch.py +86 -49
- telegrinder/bot/dispatch/handler/__init__.py +3 -0
- telegrinder/bot/dispatch/handler/abc.py +11 -5
- telegrinder/bot/dispatch/handler/func.py +41 -32
- telegrinder/bot/dispatch/handler/message_reply.py +46 -0
- telegrinder/bot/dispatch/middleware/__init__.py +2 -0
- telegrinder/bot/dispatch/middleware/abc.py +10 -4
- telegrinder/bot/dispatch/process.py +53 -49
- telegrinder/bot/dispatch/return_manager/__init__.py +19 -0
- telegrinder/bot/dispatch/return_manager/abc.py +95 -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 +25 -0
- telegrinder/bot/dispatch/view/__init__.py +14 -2
- telegrinder/bot/dispatch/view/abc.py +121 -2
- telegrinder/bot/dispatch/view/box.py +38 -0
- telegrinder/bot/dispatch/view/callback_query.py +13 -38
- telegrinder/bot/dispatch/view/inline_query.py +11 -38
- telegrinder/bot/dispatch/view/message.py +11 -46
- telegrinder/bot/dispatch/waiter_machine/__init__.py +9 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +116 -0
- telegrinder/bot/dispatch/waiter_machine/middleware.py +76 -0
- telegrinder/bot/dispatch/waiter_machine/short_state.py +37 -0
- telegrinder/bot/polling/__init__.py +2 -0
- telegrinder/bot/polling/abc.py +11 -4
- telegrinder/bot/polling/polling.py +89 -40
- telegrinder/bot/rules/__init__.py +92 -5
- telegrinder/bot/rules/abc.py +81 -63
- telegrinder/bot/rules/adapter/__init__.py +11 -0
- telegrinder/bot/rules/adapter/abc.py +21 -0
- telegrinder/bot/rules/adapter/errors.py +5 -0
- telegrinder/bot/rules/adapter/event.py +43 -0
- telegrinder/bot/rules/adapter/raw_update.py +24 -0
- telegrinder/bot/rules/callback_data.py +159 -38
- telegrinder/bot/rules/command.py +116 -0
- telegrinder/bot/rules/enum_text.py +28 -0
- telegrinder/bot/rules/func.py +17 -17
- telegrinder/bot/rules/fuzzy.py +13 -10
- telegrinder/bot/rules/inline.py +61 -0
- telegrinder/bot/rules/integer.py +12 -7
- telegrinder/bot/rules/is_from.py +148 -7
- telegrinder/bot/rules/markup.py +21 -18
- telegrinder/bot/rules/mention.py +17 -0
- telegrinder/bot/rules/message_entities.py +33 -0
- telegrinder/bot/rules/regex.py +27 -19
- telegrinder/bot/rules/rule_enum.py +74 -0
- telegrinder/bot/rules/start.py +42 -0
- telegrinder/bot/rules/text.py +23 -14
- telegrinder/bot/scenario/__init__.py +2 -0
- telegrinder/bot/scenario/abc.py +12 -5
- telegrinder/bot/scenario/checkbox.py +48 -30
- telegrinder/bot/scenario/choice.py +16 -10
- telegrinder/client/__init__.py +2 -0
- telegrinder/client/abc.py +8 -21
- telegrinder/client/aiohttp.py +30 -21
- telegrinder/model.py +68 -37
- telegrinder/modules.py +189 -21
- telegrinder/msgspec_json.py +14 -0
- telegrinder/msgspec_utils.py +207 -0
- telegrinder/node/__init__.py +31 -0
- telegrinder/node/attachment.py +71 -0
- telegrinder/node/base.py +93 -0
- telegrinder/node/composer.py +71 -0
- telegrinder/node/container.py +22 -0
- telegrinder/node/message.py +18 -0
- telegrinder/node/rule.py +56 -0
- telegrinder/node/source.py +31 -0
- telegrinder/node/text.py +13 -0
- telegrinder/node/tools/__init__.py +3 -0
- telegrinder/node/tools/generator.py +40 -0
- telegrinder/node/update.py +12 -0
- telegrinder/rules.py +1 -1
- telegrinder/tools/__init__.py +165 -4
- telegrinder/tools/buttons.py +75 -51
- telegrinder/tools/error_handler/__init__.py +8 -0
- telegrinder/tools/error_handler/abc.py +30 -0
- telegrinder/tools/error_handler/error_handler.py +156 -0
- telegrinder/tools/formatting/__init__.py +81 -3
- telegrinder/tools/formatting/html.py +283 -37
- telegrinder/tools/formatting/links.py +32 -0
- telegrinder/tools/formatting/spec_html_formats.py +121 -0
- telegrinder/tools/global_context/__init__.py +12 -0
- telegrinder/tools/global_context/abc.py +66 -0
- telegrinder/tools/global_context/global_context.py +451 -0
- telegrinder/tools/global_context/telegrinder_ctx.py +25 -0
- telegrinder/tools/i18n/__init__.py +12 -0
- telegrinder/tools/i18n/base.py +31 -0
- telegrinder/tools/i18n/middleware/__init__.py +3 -0
- telegrinder/tools/i18n/middleware/base.py +26 -0
- telegrinder/tools/i18n/simple.py +48 -0
- telegrinder/tools/inline_query.py +684 -0
- telegrinder/tools/kb_set/__init__.py +2 -0
- telegrinder/tools/kb_set/base.py +3 -0
- telegrinder/tools/kb_set/yaml.py +28 -17
- telegrinder/tools/keyboard.py +84 -62
- telegrinder/tools/loop_wrapper/__init__.py +4 -0
- telegrinder/tools/loop_wrapper/abc.py +18 -0
- telegrinder/tools/loop_wrapper/loop_wrapper.py +132 -0
- telegrinder/tools/magic.py +48 -23
- telegrinder/tools/parse_mode.py +1 -2
- telegrinder/types/__init__.py +1 -0
- telegrinder/types/enums.py +651 -0
- telegrinder/types/methods.py +3933 -1128
- telegrinder/types/objects.py +4755 -1633
- {telegrinder-0.1.dev19.dist-info → telegrinder-0.1.dev158.dist-info}/LICENSE +2 -1
- telegrinder-0.1.dev158.dist-info/METADATA +108 -0
- telegrinder-0.1.dev158.dist-info/RECORD +126 -0
- {telegrinder-0.1.dev19.dist-info → telegrinder-0.1.dev158.dist-info}/WHEEL +1 -1
- telegrinder/bot/dispatch/waiter.py +0 -37
- telegrinder/result.py +0 -38
- telegrinder/tools/formatting/abc.py +0 -52
- telegrinder/tools/formatting/markdown.py +0 -57
- telegrinder/typegen/__init__.py +0 -1
- telegrinder/typegen/__main__.py +0 -3
- telegrinder/typegen/nicification.py +0 -20
- telegrinder/typegen/schema_generator.py +0 -259
- telegrinder-0.1.dev19.dist-info/METADATA +0 -22
- telegrinder-0.1.dev19.dist-info/RECORD +0 -74
telegrinder/tools/kb_set/yaml.py
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
|
-
from .base import KeyboardSetBase, KeyboardSetError
|
|
2
|
-
from telegrinder.tools.keyboard import Keyboard, InlineKeyboard
|
|
3
|
-
import typing
|
|
4
|
-
import re
|
|
5
1
|
import os
|
|
2
|
+
import re
|
|
3
|
+
import typing
|
|
4
|
+
|
|
6
5
|
import yaml
|
|
7
6
|
|
|
7
|
+
from telegrinder.tools.keyboard import InlineKeyboard, Keyboard
|
|
8
|
+
|
|
9
|
+
from .base import KeyboardSetBase, KeyboardSetError
|
|
10
|
+
|
|
11
|
+
PathLike = str | os.PathLike[str]
|
|
12
|
+
|
|
8
13
|
|
|
9
14
|
class KeyboardSetYAML(KeyboardSetBase):
|
|
10
|
-
__config__:
|
|
15
|
+
__config__: PathLike
|
|
11
16
|
|
|
12
17
|
@classmethod
|
|
13
18
|
def load(cls) -> None:
|
|
14
19
|
config_path = getattr(cls, "__config__", "keyboards.yaml")
|
|
15
20
|
if not os.path.exists(config_path):
|
|
16
|
-
raise FileNotFoundError(f"Config file for {cls.__name__} is undefined")
|
|
21
|
+
raise FileNotFoundError(f"Config file for {cls.__name__!r} is undefined.")
|
|
17
22
|
|
|
18
|
-
config = yaml.load(
|
|
23
|
+
config = yaml.load(
|
|
24
|
+
open( # noqa: SIM115
|
|
25
|
+
str(config_path),
|
|
26
|
+
mode="r",
|
|
27
|
+
encoding="UTF-8",
|
|
28
|
+
),
|
|
29
|
+
yaml.Loader,
|
|
30
|
+
)
|
|
19
31
|
for name, hint in typing.get_type_hints(cls).items():
|
|
20
32
|
g = re.match(r"(?:kb_|keyboard_)(.+)", name.lower())
|
|
21
33
|
if not g:
|
|
@@ -23,12 +35,9 @@ class KeyboardSetYAML(KeyboardSetBase):
|
|
|
23
35
|
|
|
24
36
|
short_name = g.group(1)
|
|
25
37
|
if short_name not in config:
|
|
26
|
-
raise KeyboardSetError(
|
|
27
|
-
f"Keyboard {short_name!r} is undefined in config"
|
|
28
|
-
)
|
|
38
|
+
raise KeyboardSetError(f"Keyboard {short_name!r} is undefined in config.")
|
|
29
39
|
|
|
30
40
|
kb_config = config[short_name]
|
|
31
|
-
|
|
32
41
|
if (
|
|
33
42
|
not isinstance(kb_config, dict)
|
|
34
43
|
or "buttons" not in kb_config
|
|
@@ -36,18 +45,20 @@ class KeyboardSetYAML(KeyboardSetBase):
|
|
|
36
45
|
):
|
|
37
46
|
raise KeyboardSetError(
|
|
38
47
|
"Keyboard should be dict with field buttons which must be a list, "
|
|
39
|
-
"check documentation"
|
|
48
|
+
"check documentation."
|
|
40
49
|
)
|
|
41
|
-
|
|
50
|
+
|
|
42
51
|
buttons = kb_config.pop("buttons")
|
|
43
|
-
new_keyboard:
|
|
44
|
-
|
|
52
|
+
new_keyboard: Keyboard | InlineKeyboard = hint(**kb_config)
|
|
45
53
|
for button in buttons:
|
|
46
54
|
if not button:
|
|
47
55
|
new_keyboard.row()
|
|
48
56
|
continue
|
|
49
57
|
if "text" not in button:
|
|
50
|
-
raise KeyboardSetError("
|
|
51
|
-
new_keyboard.add(new_keyboard.BUTTON(**button))
|
|
58
|
+
raise KeyboardSetError("Text is required in button.")
|
|
59
|
+
new_keyboard.add(new_keyboard.BUTTON(**button)) # type: ignore
|
|
52
60
|
|
|
53
61
|
setattr(cls, name, new_keyboard)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
__all__ = ("KeyboardSetYAML",)
|
telegrinder/tools/keyboard.py
CHANGED
|
@@ -1,47 +1,45 @@
|
|
|
1
|
+
import dataclasses
|
|
1
2
|
import typing
|
|
2
|
-
from typing import List, Optional
|
|
3
|
-
from dataclasses import dataclass
|
|
4
3
|
from abc import ABC, abstractmethod
|
|
5
|
-
from
|
|
4
|
+
from types import NoneType
|
|
6
5
|
|
|
7
|
-
from .
|
|
6
|
+
from fntypes.option import Nothing, Some
|
|
8
7
|
|
|
8
|
+
from telegrinder.msgspec_utils import Option
|
|
9
|
+
from telegrinder.types.objects import (
|
|
10
|
+
InlineKeyboardMarkup,
|
|
11
|
+
ReplyKeyboardMarkup,
|
|
12
|
+
ReplyKeyboardRemove,
|
|
13
|
+
)
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
from .buttons import Button, ButtonT, InlineButton, RowButtons
|
|
11
16
|
|
|
17
|
+
DictStrAny = dict[str, typing.Any]
|
|
18
|
+
AnyMarkup = InlineKeyboardMarkup | ReplyKeyboardMarkup
|
|
12
19
|
|
|
13
|
-
@dataclass
|
|
14
|
-
class KeyboardModel:
|
|
15
|
-
resize_keyboard: bool
|
|
16
|
-
one_time_keyboard: bool
|
|
17
|
-
selective: bool
|
|
18
|
-
keyboard: List[List[dict]]
|
|
19
20
|
|
|
21
|
+
def keyboard_remove(*, selective: bool | None = None) -> ReplyKeyboardRemove:
|
|
22
|
+
return ReplyKeyboardRemove(
|
|
23
|
+
remove_keyboard=True,
|
|
24
|
+
selective=Nothing() if selective is None else Some(selective),
|
|
25
|
+
)
|
|
20
26
|
|
|
21
|
-
class ABCMarkup(ABC, KeyboardModel):
|
|
22
|
-
BUTTON: typing.Type[ABCButton]
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
self.resize_keyboard = resize_keyboard
|
|
32
|
-
self.one_time_keyboard = one_time_keyboard
|
|
33
|
-
self.selective = selective
|
|
28
|
+
@dataclasses.dataclass
|
|
29
|
+
class KeyboardModel:
|
|
30
|
+
resize_keyboard: bool | Option[bool]
|
|
31
|
+
one_time_keyboard: bool | Option[bool]
|
|
32
|
+
selective: bool | Option[bool]
|
|
33
|
+
is_persistent: bool | Option[bool]
|
|
34
|
+
keyboard: list[list[dict[str, typing.Any]]]
|
|
34
35
|
|
|
35
|
-
@abstractmethod
|
|
36
|
-
def add(self, button) -> "ABCMarkup":
|
|
37
|
-
pass
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
class ABCMarkup(ABC, typing.Generic[ButtonT]):
|
|
38
|
+
BUTTON: type[ButtonT]
|
|
39
|
+
keyboard: list[list[dict[str, typing.Any]]]
|
|
42
40
|
|
|
43
41
|
@abstractmethod
|
|
44
|
-
def dict(self) ->
|
|
42
|
+
def dict(self) -> DictStrAny:
|
|
45
43
|
pass
|
|
46
44
|
|
|
47
45
|
@abstractmethod
|
|
@@ -52,6 +50,26 @@ class ABCMarkup(ABC, KeyboardModel):
|
|
|
52
50
|
def empty(cls) -> AnyMarkup:
|
|
53
51
|
return cls().get_markup()
|
|
54
52
|
|
|
53
|
+
def add(self, row_or_button: RowButtons[ButtonT] | ButtonT) -> typing.Self:
|
|
54
|
+
if not len(self.keyboard):
|
|
55
|
+
self.row()
|
|
56
|
+
|
|
57
|
+
if isinstance(row_or_button, RowButtons):
|
|
58
|
+
self.keyboard[-1].extend(row_or_button.get_data())
|
|
59
|
+
if row_or_button.auto_row:
|
|
60
|
+
self.row()
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
self.keyboard[-1].append(row_or_button.get_data())
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def row(self) -> typing.Self:
|
|
67
|
+
if len(self.keyboard) and not len(self.keyboard[-1]):
|
|
68
|
+
raise RuntimeError("Last row is empty!")
|
|
69
|
+
|
|
70
|
+
self.keyboard.append([])
|
|
71
|
+
return self
|
|
72
|
+
|
|
55
73
|
def format(self, **format_data: typing.Dict[str, str]) -> "ABCMarkup":
|
|
56
74
|
copy_keyboard = self.__class__()
|
|
57
75
|
for row in self.keyboard:
|
|
@@ -62,54 +80,58 @@ class ABCMarkup(ABC, KeyboardModel):
|
|
|
62
80
|
copy_keyboard.row()
|
|
63
81
|
return copy_keyboard
|
|
64
82
|
|
|
65
|
-
def merge(self, other:
|
|
83
|
+
def merge(self, other: typing.Self) -> typing.Self:
|
|
66
84
|
self.keyboard.extend(other.keyboard)
|
|
67
85
|
return self
|
|
68
86
|
|
|
69
87
|
|
|
70
|
-
class Keyboard(ABCMarkup):
|
|
88
|
+
class Keyboard(ABCMarkup[Button], KeyboardModel):
|
|
71
89
|
BUTTON = Button
|
|
72
90
|
|
|
73
|
-
def
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
self.
|
|
85
|
-
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
*,
|
|
94
|
+
resize_keyboard: bool = True,
|
|
95
|
+
one_time_keyboard: bool = False,
|
|
96
|
+
selective: bool = False,
|
|
97
|
+
is_persistent: bool = False,
|
|
98
|
+
):
|
|
99
|
+
self.keyboard = [[]]
|
|
100
|
+
self.resize_keyboard = resize_keyboard
|
|
101
|
+
self.one_time_keyboard = one_time_keyboard
|
|
102
|
+
self.selective = selective
|
|
103
|
+
self.is_persistent = is_persistent
|
|
86
104
|
|
|
87
|
-
def dict(self) ->
|
|
88
|
-
|
|
105
|
+
def dict(self) -> DictStrAny:
|
|
106
|
+
self.keyboard = [row for row in self.keyboard if row]
|
|
107
|
+
return {
|
|
108
|
+
k: v.unwrap() if v and isinstance(v, Some) else v
|
|
109
|
+
for k, v in self.__dict__.items()
|
|
110
|
+
if type(v) not in (NoneType, Nothing)
|
|
111
|
+
}
|
|
89
112
|
|
|
90
113
|
def get_markup(self) -> ReplyKeyboardMarkup:
|
|
91
114
|
return ReplyKeyboardMarkup(**self.dict())
|
|
92
115
|
|
|
93
116
|
|
|
94
|
-
class InlineKeyboard(ABCMarkup):
|
|
117
|
+
class InlineKeyboard(ABCMarkup[InlineButton]):
|
|
95
118
|
BUTTON = InlineButton
|
|
96
119
|
|
|
97
|
-
def
|
|
98
|
-
|
|
99
|
-
raise RuntimeError("Last row is empty!")
|
|
100
|
-
|
|
101
|
-
self.keyboard.append([])
|
|
102
|
-
return self
|
|
103
|
-
|
|
104
|
-
def add(self, button: InlineButton) -> "InlineKeyboard":
|
|
105
|
-
if not len(self.keyboard):
|
|
106
|
-
self.row()
|
|
107
|
-
|
|
108
|
-
self.keyboard[-1].append(button.get_data())
|
|
109
|
-
return self
|
|
120
|
+
def __init__(self) -> None:
|
|
121
|
+
self.keyboard = [[]]
|
|
110
122
|
|
|
111
|
-
def dict(self) ->
|
|
123
|
+
def dict(self) -> DictStrAny:
|
|
124
|
+
self.keyboard = [row for row in self.keyboard if row]
|
|
112
125
|
return dict(inline_keyboard=self.keyboard)
|
|
113
126
|
|
|
114
127
|
def get_markup(self) -> InlineKeyboardMarkup:
|
|
115
128
|
return InlineKeyboardMarkup(**self.dict())
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
__all__ = (
|
|
132
|
+
"ABCMarkup",
|
|
133
|
+
"InlineKeyboard",
|
|
134
|
+
"Keyboard",
|
|
135
|
+
"KeyboardModel",
|
|
136
|
+
"keyboard_remove",
|
|
137
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
CoroutineTask = typing.Coroutine[typing.Any, typing.Any, typing.Any]
|
|
5
|
+
CoroutineFunc = typing.Callable[..., CoroutineTask]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ABCLoopWrapper(ABC):
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def add_task(self, task: CoroutineFunc | CoroutineTask) -> None:
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def run_event_loop(self) -> None:
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ("ABCLoopWrapper",)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import dataclasses
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
from telegrinder.modules import logger
|
|
7
|
+
|
|
8
|
+
from .abc import ABCLoopWrapper, CoroutineFunc, CoroutineTask
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclasses.dataclass
|
|
12
|
+
class DelayedTask:
|
|
13
|
+
handler: CoroutineFunc
|
|
14
|
+
seconds: float
|
|
15
|
+
repeat: bool = dataclasses.field(default=False, kw_only=True)
|
|
16
|
+
_cancelled: bool = dataclasses.field(default=False, init=False, repr=False)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def is_cancelled(self) -> bool:
|
|
20
|
+
return self._cancelled
|
|
21
|
+
|
|
22
|
+
async def __call__(self, *args, **kwargs) -> None:
|
|
23
|
+
while not self.is_cancelled:
|
|
24
|
+
await asyncio.sleep(self.seconds)
|
|
25
|
+
if self.is_cancelled:
|
|
26
|
+
break
|
|
27
|
+
await self.handler(*args, **kwargs)
|
|
28
|
+
if not self.repeat:
|
|
29
|
+
break
|
|
30
|
+
|
|
31
|
+
def cancel(self) -> None:
|
|
32
|
+
if not self._cancelled:
|
|
33
|
+
self._cancelled = True
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LoopWrapper(ABCLoopWrapper):
|
|
37
|
+
def __init__(self, tasks: list[CoroutineTask] | None = None):
|
|
38
|
+
self.on_startup: list[CoroutineTask] = []
|
|
39
|
+
self.on_shutdown: list[CoroutineTask] = []
|
|
40
|
+
self.tasks = tasks or []
|
|
41
|
+
self._loop = asyncio.new_event_loop()
|
|
42
|
+
|
|
43
|
+
def run_event_loop(self) -> None:
|
|
44
|
+
if not self.tasks:
|
|
45
|
+
logger.warning("You run loop with 0 tasks!")
|
|
46
|
+
|
|
47
|
+
for startup_task in self.on_startup:
|
|
48
|
+
self._loop.run_until_complete(startup_task)
|
|
49
|
+
for task in self.tasks:
|
|
50
|
+
self._loop.create_task(task)
|
|
51
|
+
|
|
52
|
+
self.tasks.clear()
|
|
53
|
+
tasks = asyncio.all_tasks(self._loop)
|
|
54
|
+
try:
|
|
55
|
+
while tasks:
|
|
56
|
+
tasks_results, _ = self._loop.run_until_complete(
|
|
57
|
+
asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
|
|
58
|
+
)
|
|
59
|
+
for task_result in tasks_results:
|
|
60
|
+
try:
|
|
61
|
+
task_result.result()
|
|
62
|
+
except BaseException as ex:
|
|
63
|
+
logger.exception(ex)
|
|
64
|
+
tasks = asyncio.all_tasks(self._loop)
|
|
65
|
+
except KeyboardInterrupt:
|
|
66
|
+
print() # blank print for ^C
|
|
67
|
+
logger.info("KeyboardInterrupt")
|
|
68
|
+
self.complete_tasks(tasks)
|
|
69
|
+
finally:
|
|
70
|
+
for shutdown_task in self.on_shutdown:
|
|
71
|
+
self._loop.run_until_complete(shutdown_task)
|
|
72
|
+
if self._loop.is_running():
|
|
73
|
+
self._loop.close()
|
|
74
|
+
|
|
75
|
+
def add_task(self, task: CoroutineFunc | CoroutineTask | DelayedTask):
|
|
76
|
+
if asyncio.iscoroutinefunction(task) or isinstance(task, DelayedTask):
|
|
77
|
+
task = task()
|
|
78
|
+
elif not asyncio.iscoroutine(task):
|
|
79
|
+
raise TypeError("Task should be coroutine or coroutine function.")
|
|
80
|
+
|
|
81
|
+
if self._loop and self._loop.is_running():
|
|
82
|
+
self._loop.create_task(task)
|
|
83
|
+
else:
|
|
84
|
+
self.tasks.append(task)
|
|
85
|
+
|
|
86
|
+
def complete_tasks(self, tasks: set[asyncio.Task[typing.Any]]) -> None:
|
|
87
|
+
tasks = tasks | asyncio.all_tasks(self._loop)
|
|
88
|
+
task_to_cancel = asyncio.gather(*tasks, return_exceptions=True)
|
|
89
|
+
task_to_cancel.cancel()
|
|
90
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
91
|
+
self._loop.run_until_complete(task_to_cancel)
|
|
92
|
+
|
|
93
|
+
def timer(
|
|
94
|
+
self,
|
|
95
|
+
*,
|
|
96
|
+
days: int = 0,
|
|
97
|
+
hours: int = 0,
|
|
98
|
+
minutes: int = 0,
|
|
99
|
+
seconds: float = 0,
|
|
100
|
+
) -> typing.Callable[[typing.Callable], DelayedTask]:
|
|
101
|
+
seconds += minutes * 60
|
|
102
|
+
seconds += hours * 60 * 60
|
|
103
|
+
seconds += days * 24 * 60 * 60
|
|
104
|
+
|
|
105
|
+
def decorator(func: typing.Callable) -> DelayedTask:
|
|
106
|
+
delayed_task = DelayedTask(func, seconds, repeat=False)
|
|
107
|
+
self.add_task(delayed_task)
|
|
108
|
+
return delayed_task
|
|
109
|
+
|
|
110
|
+
return decorator
|
|
111
|
+
|
|
112
|
+
def interval(
|
|
113
|
+
self,
|
|
114
|
+
*,
|
|
115
|
+
days: int = 0,
|
|
116
|
+
hours: int = 0,
|
|
117
|
+
minutes: int = 0,
|
|
118
|
+
seconds: float = 0,
|
|
119
|
+
) -> typing.Callable[[typing.Callable], DelayedTask]:
|
|
120
|
+
seconds += minutes * 60
|
|
121
|
+
seconds += hours * 60 * 60
|
|
122
|
+
seconds += days * 24 * 60 * 60
|
|
123
|
+
|
|
124
|
+
def decorator(func: typing.Callable) -> DelayedTask:
|
|
125
|
+
delayed_task = DelayedTask(func, seconds, repeat=True)
|
|
126
|
+
self.add_task(delayed_task)
|
|
127
|
+
return delayed_task
|
|
128
|
+
|
|
129
|
+
return decorator
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
__all__ = ("DelayedTask", "LoopWrapper")
|
telegrinder/tools/magic.py
CHANGED
|
@@ -1,41 +1,66 @@
|
|
|
1
|
+
import enum
|
|
1
2
|
import inspect
|
|
2
3
|
import types
|
|
3
4
|
import typing
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
if typing.TYPE_CHECKING:
|
|
7
|
+
from telegrinder.bot.rules.abc import ABCRule
|
|
6
8
|
|
|
9
|
+
T = typing.TypeVar("T", bound=ABCRule)
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
self.name = name
|
|
11
|
-
self.t = t
|
|
12
|
-
self.handler = handler
|
|
11
|
+
FuncType = types.FunctionType | typing.Callable
|
|
12
|
+
TRANSLATIONS_KEY = "_translations"
|
|
13
13
|
|
|
14
|
-
def __repr__(self):
|
|
15
|
-
return (
|
|
16
|
-
f"Handler {self.handler.__name__} requires variable {self.name} of type {self.t} "
|
|
17
|
-
"which was not set in the context"
|
|
18
|
-
)
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
def resolve_arg_names(func: FuncType, start_idx: int = 1) -> tuple[str, ...]:
|
|
16
|
+
return func.__code__.co_varnames[start_idx : func.__code__.co_argcount]
|
|
22
17
|
|
|
23
18
|
|
|
24
|
-
def
|
|
25
|
-
return func.__code__.co_varnames[1 : func.__code__.co_argcount]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def get_default_args(func: types.FunctionType) -> typing.Dict[str, typing.Any]:
|
|
19
|
+
def get_default_args(func: FuncType) -> dict[str, typing.Any]:
|
|
29
20
|
fspec = inspect.getfullargspec(func)
|
|
30
21
|
return dict(zip(fspec.args[::-1], (fspec.defaults or ())[::-1]))
|
|
31
22
|
|
|
32
23
|
|
|
24
|
+
def to_str(s: str | enum.Enum) -> str:
|
|
25
|
+
if isinstance(s, enum.Enum):
|
|
26
|
+
return str(s.value)
|
|
27
|
+
return s
|
|
28
|
+
|
|
29
|
+
|
|
33
30
|
def magic_bundle(
|
|
34
|
-
handler:
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
handler: FuncType,
|
|
32
|
+
kw: dict[str | enum.Enum, typing.Any],
|
|
33
|
+
*,
|
|
34
|
+
start_idx: int = 1,
|
|
35
|
+
bundle_ctx: bool = True,
|
|
36
|
+
) -> dict[str, typing.Any]:
|
|
37
|
+
names = resolve_arg_names(handler, start_idx=start_idx)
|
|
37
38
|
args = get_default_args(handler)
|
|
38
|
-
args.update({k: v for k, v in kw.items() if k in names})
|
|
39
|
-
if "ctx" in names:
|
|
39
|
+
args.update({to_str(k): v for k, v in kw.items() if to_str(k) in names})
|
|
40
|
+
if "ctx" in names and bundle_ctx:
|
|
40
41
|
args["ctx"] = kw
|
|
41
42
|
return args
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_cached_translation(rule: "T", locale: str) -> typing.Optional["T"]:
|
|
46
|
+
translations = getattr(rule, TRANSLATIONS_KEY, {})
|
|
47
|
+
if not translations or locale not in translations:
|
|
48
|
+
return None
|
|
49
|
+
return translations[locale]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cache_translation(base_rule: "T", locale: str, translated_rule: "T") -> None:
|
|
53
|
+
translations = getattr(base_rule, TRANSLATIONS_KEY, {})
|
|
54
|
+
setattr(base_rule, TRANSLATIONS_KEY, {locale: translated_rule, **translations})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = (
|
|
58
|
+
"TRANSLATIONS_KEY",
|
|
59
|
+
"cache_translation",
|
|
60
|
+
"get_cached_translation",
|
|
61
|
+
"get_default_args",
|
|
62
|
+
"get_default_args",
|
|
63
|
+
"magic_bundle",
|
|
64
|
+
"resolve_arg_names",
|
|
65
|
+
"to_str",
|
|
66
|
+
)
|
telegrinder/tools/parse_mode.py
CHANGED
telegrinder/types/__init__.py
CHANGED