telegrinder 0.3.0.post2__py3-none-any.whl → 0.3.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 +144 -144
- telegrinder/api/__init__.py +8 -8
- telegrinder/api/api.py +93 -93
- telegrinder/api/error.py +16 -16
- telegrinder/api/response.py +20 -20
- telegrinder/api/token.py +36 -36
- telegrinder/bot/__init__.py +66 -66
- telegrinder/bot/bot.py +76 -76
- telegrinder/bot/cute_types/__init__.py +11 -11
- telegrinder/bot/cute_types/base.py +258 -234
- telegrinder/bot/cute_types/callback_query.py +382 -382
- telegrinder/bot/cute_types/chat_join_request.py +61 -61
- telegrinder/bot/cute_types/chat_member_updated.py +160 -160
- telegrinder/bot/cute_types/inline_query.py +53 -53
- telegrinder/bot/cute_types/message.py +2631 -2631
- telegrinder/bot/cute_types/update.py +75 -75
- telegrinder/bot/cute_types/utils.py +92 -92
- telegrinder/bot/dispatch/__init__.py +55 -55
- telegrinder/bot/dispatch/abc.py +77 -77
- telegrinder/bot/dispatch/context.py +92 -92
- telegrinder/bot/dispatch/dispatch.py +202 -201
- telegrinder/bot/dispatch/handler/__init__.py +13 -13
- telegrinder/bot/dispatch/handler/abc.py +24 -24
- telegrinder/bot/dispatch/handler/audio_reply.py +44 -44
- telegrinder/bot/dispatch/handler/base.py +57 -57
- telegrinder/bot/dispatch/handler/document_reply.py +44 -44
- telegrinder/bot/dispatch/handler/func.py +128 -123
- telegrinder/bot/dispatch/handler/media_group_reply.py +43 -43
- telegrinder/bot/dispatch/handler/message_reply.py +36 -36
- telegrinder/bot/dispatch/handler/photo_reply.py +44 -44
- telegrinder/bot/dispatch/handler/sticker_reply.py +37 -37
- telegrinder/bot/dispatch/handler/video_reply.py +44 -44
- telegrinder/bot/dispatch/middleware/__init__.py +3 -3
- telegrinder/bot/dispatch/middleware/abc.py +16 -16
- telegrinder/bot/dispatch/process.py +132 -132
- telegrinder/bot/dispatch/return_manager/__init__.py +13 -13
- telegrinder/bot/dispatch/return_manager/abc.py +108 -108
- telegrinder/bot/dispatch/return_manager/callback_query.py +20 -20
- telegrinder/bot/dispatch/return_manager/inline_query.py +15 -15
- telegrinder/bot/dispatch/return_manager/message.py +36 -36
- telegrinder/bot/dispatch/view/__init__.py +13 -13
- telegrinder/bot/dispatch/view/abc.py +41 -41
- telegrinder/bot/dispatch/view/base.py +200 -211
- telegrinder/bot/dispatch/view/box.py +129 -129
- telegrinder/bot/dispatch/view/callback_query.py +17 -17
- telegrinder/bot/dispatch/view/chat_join_request.py +16 -16
- telegrinder/bot/dispatch/view/chat_member.py +39 -39
- telegrinder/bot/dispatch/view/inline_query.py +17 -17
- telegrinder/bot/dispatch/view/message.py +44 -44
- telegrinder/bot/dispatch/view/raw.py +114 -118
- telegrinder/bot/dispatch/waiter_machine/__init__.py +17 -17
- telegrinder/bot/dispatch/waiter_machine/actions.py +13 -13
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +8 -8
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +57 -57
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +57 -57
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +53 -53
- telegrinder/bot/dispatch/waiter_machine/hasher/state.py +19 -19
- telegrinder/bot/dispatch/waiter_machine/machine.py +168 -170
- telegrinder/bot/dispatch/waiter_machine/middleware.py +89 -89
- telegrinder/bot/dispatch/waiter_machine/short_state.py +65 -65
- telegrinder/bot/polling/__init__.py +4 -4
- telegrinder/bot/polling/abc.py +25 -25
- telegrinder/bot/polling/polling.py +131 -131
- telegrinder/bot/rules/__init__.py +62 -62
- telegrinder/bot/rules/abc.py +238 -233
- telegrinder/bot/rules/adapter/__init__.py +9 -9
- telegrinder/bot/rules/adapter/abc.py +29 -29
- telegrinder/bot/rules/adapter/errors.py +5 -5
- telegrinder/bot/rules/adapter/event.py +76 -76
- telegrinder/bot/rules/adapter/node.py +48 -48
- telegrinder/bot/rules/adapter/raw_update.py +30 -30
- telegrinder/bot/rules/callback_data.py +171 -171
- telegrinder/bot/rules/chat_join.py +48 -48
- telegrinder/bot/rules/command.py +126 -126
- telegrinder/bot/rules/enum_text.py +36 -33
- telegrinder/bot/rules/func.py +26 -26
- telegrinder/bot/rules/fuzzy.py +24 -24
- telegrinder/bot/rules/inline.py +60 -60
- telegrinder/bot/rules/integer.py +20 -20
- telegrinder/bot/rules/is_from.py +146 -146
- telegrinder/bot/rules/markup.py +43 -43
- telegrinder/bot/rules/mention.py +14 -14
- telegrinder/bot/rules/message.py +17 -17
- telegrinder/bot/rules/message_entities.py +35 -35
- telegrinder/bot/rules/node.py +27 -27
- telegrinder/bot/rules/regex.py +37 -37
- telegrinder/bot/rules/rule_enum.py +72 -72
- telegrinder/bot/rules/start.py +42 -42
- telegrinder/bot/rules/state.py +37 -37
- telegrinder/bot/rules/text.py +33 -33
- telegrinder/bot/rules/update.py +15 -15
- telegrinder/bot/scenario/__init__.py +5 -5
- telegrinder/bot/scenario/abc.py +19 -19
- telegrinder/bot/scenario/checkbox.py +167 -139
- telegrinder/bot/scenario/choice.py +46 -44
- telegrinder/client/__init__.py +4 -4
- telegrinder/client/abc.py +75 -75
- telegrinder/client/aiohttp.py +130 -130
- telegrinder/model.py +244 -244
- telegrinder/modules.py +237 -237
- telegrinder/msgspec_json.py +14 -14
- telegrinder/msgspec_utils.py +410 -410
- telegrinder/node/__init__.py +20 -20
- telegrinder/node/attachment.py +92 -92
- telegrinder/node/base.py +143 -144
- telegrinder/node/callback_query.py +14 -14
- telegrinder/node/command.py +33 -33
- telegrinder/node/composer.py +196 -184
- telegrinder/node/container.py +27 -27
- telegrinder/node/event.py +71 -73
- telegrinder/node/me.py +16 -16
- telegrinder/node/message.py +14 -14
- telegrinder/node/polymorphic.py +48 -52
- telegrinder/node/rule.py +76 -76
- telegrinder/node/scope.py +38 -38
- telegrinder/node/source.py +71 -71
- telegrinder/node/text.py +21 -21
- telegrinder/node/tools/__init__.py +3 -3
- telegrinder/node/tools/generator.py +40 -40
- telegrinder/node/update.py +15 -15
- telegrinder/rules.py +0 -0
- telegrinder/tools/__init__.py +74 -74
- telegrinder/tools/buttons.py +79 -79
- telegrinder/tools/error_handler/__init__.py +7 -7
- telegrinder/tools/error_handler/abc.py +33 -33
- telegrinder/tools/error_handler/error.py +9 -9
- telegrinder/tools/error_handler/error_handler.py +193 -193
- telegrinder/tools/formatting/__init__.py +46 -46
- telegrinder/tools/formatting/html.py +308 -308
- telegrinder/tools/formatting/links.py +33 -33
- telegrinder/tools/formatting/spec_html_formats.py +111 -111
- telegrinder/tools/functional.py +12 -12
- telegrinder/tools/global_context/__init__.py +7 -7
- telegrinder/tools/global_context/abc.py +63 -63
- telegrinder/tools/global_context/global_context.py +412 -412
- telegrinder/tools/global_context/telegrinder_ctx.py +27 -27
- telegrinder/tools/i18n/__init__.py +12 -12
- telegrinder/tools/i18n/abc.py +32 -32
- telegrinder/tools/i18n/middleware/__init__.py +3 -3
- telegrinder/tools/i18n/middleware/abc.py +25 -25
- telegrinder/tools/i18n/simple.py +43 -43
- telegrinder/tools/kb_set/__init__.py +4 -4
- telegrinder/tools/kb_set/base.py +15 -15
- telegrinder/tools/kb_set/yaml.py +63 -63
- telegrinder/tools/keyboard.py +128 -128
- telegrinder/tools/limited_dict.py +37 -37
- telegrinder/tools/loop_wrapper/__init__.py +4 -4
- telegrinder/tools/loop_wrapper/abc.py +15 -15
- telegrinder/tools/loop_wrapper/loop_wrapper.py +216 -216
- telegrinder/tools/magic.py +168 -164
- telegrinder/tools/parse_mode.py +6 -6
- telegrinder/tools/state_storage/__init__.py +4 -4
- telegrinder/tools/state_storage/abc.py +35 -35
- telegrinder/tools/state_storage/memory.py +25 -25
- telegrinder/types/__init__.py +6 -6
- telegrinder/types/enums.py +672 -672
- telegrinder/types/methods.py +4633 -4633
- telegrinder/types/objects.py +6317 -6317
- telegrinder/verification_utils.py +32 -32
- {telegrinder-0.3.0.post2.dist-info → telegrinder-0.3.2.dist-info}/LICENSE +22 -22
- {telegrinder-0.3.0.post2.dist-info → telegrinder-0.3.2.dist-info}/METADATA +1 -1
- telegrinder-0.3.2.dist-info/RECORD +164 -0
- telegrinder-0.3.0.post2.dist-info/RECORD +0 -164
- {telegrinder-0.3.0.post2.dist-info → telegrinder-0.3.2.dist-info}/WHEEL +0 -0
|
@@ -1,216 +1,216 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import contextlib
|
|
3
|
-
import dataclasses
|
|
4
|
-
import datetime
|
|
5
|
-
import typing
|
|
6
|
-
|
|
7
|
-
from telegrinder.modules import logger
|
|
8
|
-
|
|
9
|
-
from .abc import ABCLoopWrapper
|
|
10
|
-
|
|
11
|
-
T = typing.TypeVar("T")
|
|
12
|
-
P = typing.ParamSpec("P")
|
|
13
|
-
CoroFunc = typing.TypeVar("CoroFunc", bound="CoroutineFunc")
|
|
14
|
-
|
|
15
|
-
CoroutineTask: typing.TypeAlias = typing.Coroutine[typing.Any, typing.Any, T]
|
|
16
|
-
CoroutineFunc: typing.TypeAlias = typing.Callable[P, CoroutineTask[T]]
|
|
17
|
-
Task: typing.TypeAlias = typing.Union[CoroutineFunc, CoroutineTask, "DelayedTask"]
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def run_tasks(
|
|
21
|
-
tasks: list[CoroutineTask[typing.Any]],
|
|
22
|
-
loop: asyncio.AbstractEventLoop,
|
|
23
|
-
) -> None:
|
|
24
|
-
while tasks:
|
|
25
|
-
loop.run_until_complete(tasks.pop(0))
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def to_coroutine_task(task: Task) -> CoroutineTask[typing.Any]:
|
|
29
|
-
if asyncio.iscoroutinefunction(task) or isinstance(task, DelayedTask):
|
|
30
|
-
task = task()
|
|
31
|
-
elif not asyncio.iscoroutine(task):
|
|
32
|
-
raise TypeError("Task should be coroutine or coroutine function.")
|
|
33
|
-
return task
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@dataclasses.dataclass(slots=True)
|
|
37
|
-
class DelayedTask(typing.Generic[CoroFunc]):
|
|
38
|
-
handler: CoroFunc
|
|
39
|
-
seconds: float
|
|
40
|
-
repeat: bool = dataclasses.field(default=False, kw_only=True)
|
|
41
|
-
_cancelled: bool = dataclasses.field(default=False, init=False, repr=False)
|
|
42
|
-
|
|
43
|
-
@property
|
|
44
|
-
def is_cancelled(self) -> bool:
|
|
45
|
-
return self._cancelled
|
|
46
|
-
|
|
47
|
-
async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
|
48
|
-
while not self.is_cancelled:
|
|
49
|
-
await asyncio.sleep(self.seconds)
|
|
50
|
-
if self.is_cancelled:
|
|
51
|
-
break
|
|
52
|
-
try:
|
|
53
|
-
await self.handler(*args, **kwargs)
|
|
54
|
-
finally:
|
|
55
|
-
if not self.repeat:
|
|
56
|
-
break
|
|
57
|
-
|
|
58
|
-
def cancel(self) -> None:
|
|
59
|
-
if not self._cancelled:
|
|
60
|
-
self._cancelled = True
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
64
|
-
class Lifespan:
|
|
65
|
-
startup_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
|
|
66
|
-
shutdown_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
|
|
67
|
-
|
|
68
|
-
def on_startup(self, task: Task, /) -> Task:
|
|
69
|
-
self.startup_tasks.append(to_coroutine_task(task))
|
|
70
|
-
return task
|
|
71
|
-
|
|
72
|
-
def on_shutdown(self, task: Task, /) -> Task:
|
|
73
|
-
self.shutdown_tasks.append(to_coroutine_task(task))
|
|
74
|
-
return task
|
|
75
|
-
|
|
76
|
-
def start(self, loop: asyncio.AbstractEventLoop, /) -> None:
|
|
77
|
-
run_tasks(self.startup_tasks, loop)
|
|
78
|
-
|
|
79
|
-
def shutdown(self, loop: asyncio.AbstractEventLoop, /) -> None:
|
|
80
|
-
run_tasks(self.shutdown_tasks, loop)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class LoopWrapper(ABCLoopWrapper):
|
|
84
|
-
def __init__(
|
|
85
|
-
self,
|
|
86
|
-
*,
|
|
87
|
-
tasks: list[CoroutineTask[typing.Any]] | None = None,
|
|
88
|
-
lifespan: Lifespan | None = None,
|
|
89
|
-
event_loop: asyncio.AbstractEventLoop | None = None,
|
|
90
|
-
) -> None:
|
|
91
|
-
self.tasks: list[CoroutineTask[typing.Any]] = tasks or []
|
|
92
|
-
self.lifespan = lifespan or Lifespan()
|
|
93
|
-
self._loop = event_loop or asyncio.new_event_loop()
|
|
94
|
-
|
|
95
|
-
def __repr__(self) -> str:
|
|
96
|
-
return "<{}: loop={!r} with tasks={!r}, lifespan={!r}>".format(
|
|
97
|
-
self.__class__.__name__,
|
|
98
|
-
self._loop,
|
|
99
|
-
self.tasks,
|
|
100
|
-
self.lifespan,
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
def run_event_loop(self) -> None:
|
|
104
|
-
if not self.tasks:
|
|
105
|
-
logger.warning("You run loop with 0 tasks!")
|
|
106
|
-
|
|
107
|
-
self.lifespan.start(self._loop)
|
|
108
|
-
while self.tasks:
|
|
109
|
-
self._loop.create_task(self.tasks.pop(0))
|
|
110
|
-
tasks = asyncio.all_tasks(self._loop)
|
|
111
|
-
|
|
112
|
-
try:
|
|
113
|
-
while tasks:
|
|
114
|
-
tasks_results, _ = self._loop.run_until_complete(
|
|
115
|
-
asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION),
|
|
116
|
-
)
|
|
117
|
-
for task_result in tasks_results:
|
|
118
|
-
try:
|
|
119
|
-
task_result.result()
|
|
120
|
-
except BaseException as ex:
|
|
121
|
-
logger.exception(ex)
|
|
122
|
-
tasks = asyncio.all_tasks(self._loop)
|
|
123
|
-
except KeyboardInterrupt:
|
|
124
|
-
print() # blank print for ^C
|
|
125
|
-
logger.info("Caught KeyboardInterrupt, cancellation...")
|
|
126
|
-
self.complete_tasks(tasks)
|
|
127
|
-
finally:
|
|
128
|
-
self.lifespan.shutdown(self._loop)
|
|
129
|
-
if self._loop.is_running():
|
|
130
|
-
self._loop.close()
|
|
131
|
-
|
|
132
|
-
def add_task(self, task: Task) -> None:
|
|
133
|
-
task = to_coroutine_task(task)
|
|
134
|
-
|
|
135
|
-
if self._loop and self._loop.is_running():
|
|
136
|
-
self._loop.create_task(task)
|
|
137
|
-
else:
|
|
138
|
-
self.tasks.append(task)
|
|
139
|
-
|
|
140
|
-
def complete_tasks(self, tasks: set[asyncio.Task[typing.Any]]) -> None:
|
|
141
|
-
tasks = tasks | asyncio.all_tasks(self._loop)
|
|
142
|
-
task_to_cancel = asyncio.gather(*tasks, return_exceptions=True)
|
|
143
|
-
task_to_cancel.cancel()
|
|
144
|
-
with contextlib.suppress(asyncio.CancelledError):
|
|
145
|
-
self._loop.run_until_complete(task_to_cancel)
|
|
146
|
-
|
|
147
|
-
@typing.overload
|
|
148
|
-
def timer(self, *, seconds: datetime.timedelta) -> typing.Callable[..., typing.Any]: ...
|
|
149
|
-
|
|
150
|
-
@typing.overload
|
|
151
|
-
def timer(
|
|
152
|
-
self,
|
|
153
|
-
*,
|
|
154
|
-
days: int = 0,
|
|
155
|
-
hours: int = 0,
|
|
156
|
-
minutes: int = 0,
|
|
157
|
-
seconds: float = 0,
|
|
158
|
-
) -> typing.Callable[..., typing.Any]: ...
|
|
159
|
-
|
|
160
|
-
def timer(
|
|
161
|
-
self,
|
|
162
|
-
*,
|
|
163
|
-
days: int = 0,
|
|
164
|
-
hours: int = 0,
|
|
165
|
-
minutes: int = 0,
|
|
166
|
-
seconds: float | datetime.timedelta = 0,
|
|
167
|
-
) -> typing.Callable[..., typing.Any]:
|
|
168
|
-
if isinstance(seconds, datetime.timedelta):
|
|
169
|
-
seconds = seconds.total_seconds()
|
|
170
|
-
|
|
171
|
-
seconds += minutes * 60
|
|
172
|
-
seconds += hours * 60 * 60
|
|
173
|
-
seconds += days * 24 * 60 * 60
|
|
174
|
-
|
|
175
|
-
def decorator(func: CoroFunc) -> CoroFunc:
|
|
176
|
-
self.add_task(DelayedTask(func, seconds, repeat=False))
|
|
177
|
-
return func
|
|
178
|
-
|
|
179
|
-
return decorator
|
|
180
|
-
|
|
181
|
-
@typing.overload
|
|
182
|
-
def interval(self, *, seconds: datetime.timedelta) -> typing.Callable[..., typing.Any]: ...
|
|
183
|
-
|
|
184
|
-
@typing.overload
|
|
185
|
-
def interval(
|
|
186
|
-
self,
|
|
187
|
-
*,
|
|
188
|
-
days: int = 0,
|
|
189
|
-
hours: int = 0,
|
|
190
|
-
minutes: int = 0,
|
|
191
|
-
seconds: float = 0,
|
|
192
|
-
) -> typing.Callable[..., typing.Any]: ...
|
|
193
|
-
|
|
194
|
-
def interval(
|
|
195
|
-
self,
|
|
196
|
-
*,
|
|
197
|
-
days: int = 0,
|
|
198
|
-
hours: int = 0,
|
|
199
|
-
minutes: int = 0,
|
|
200
|
-
seconds: float | datetime.timedelta = 0,
|
|
201
|
-
) -> typing.Callable[..., typing.Any]:
|
|
202
|
-
if isinstance(seconds, datetime.timedelta):
|
|
203
|
-
seconds = seconds.total_seconds()
|
|
204
|
-
|
|
205
|
-
seconds += minutes * 60
|
|
206
|
-
seconds += hours * 60 * 60
|
|
207
|
-
seconds += days * 24 * 60 * 60
|
|
208
|
-
|
|
209
|
-
def decorator(func: CoroFunc) -> CoroFunc:
|
|
210
|
-
self.add_task(DelayedTask(func, seconds, repeat=True))
|
|
211
|
-
return func
|
|
212
|
-
|
|
213
|
-
return decorator
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
__all__ = ("DelayedTask", "Lifespan", "LoopWrapper", "to_coroutine_task")
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import dataclasses
|
|
4
|
+
import datetime
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
from telegrinder.modules import logger
|
|
8
|
+
|
|
9
|
+
from .abc import ABCLoopWrapper
|
|
10
|
+
|
|
11
|
+
T = typing.TypeVar("T")
|
|
12
|
+
P = typing.ParamSpec("P")
|
|
13
|
+
CoroFunc = typing.TypeVar("CoroFunc", bound="CoroutineFunc")
|
|
14
|
+
|
|
15
|
+
CoroutineTask: typing.TypeAlias = typing.Coroutine[typing.Any, typing.Any, T]
|
|
16
|
+
CoroutineFunc: typing.TypeAlias = typing.Callable[P, CoroutineTask[T]]
|
|
17
|
+
Task: typing.TypeAlias = typing.Union[CoroutineFunc, CoroutineTask, "DelayedTask"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_tasks(
|
|
21
|
+
tasks: list[CoroutineTask[typing.Any]],
|
|
22
|
+
loop: asyncio.AbstractEventLoop,
|
|
23
|
+
) -> None:
|
|
24
|
+
while tasks:
|
|
25
|
+
loop.run_until_complete(tasks.pop(0))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def to_coroutine_task(task: Task) -> CoroutineTask[typing.Any]:
|
|
29
|
+
if asyncio.iscoroutinefunction(task) or isinstance(task, DelayedTask):
|
|
30
|
+
task = task()
|
|
31
|
+
elif not asyncio.iscoroutine(task):
|
|
32
|
+
raise TypeError("Task should be coroutine or coroutine function.")
|
|
33
|
+
return task
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclasses.dataclass(slots=True)
|
|
37
|
+
class DelayedTask(typing.Generic[CoroFunc]):
|
|
38
|
+
handler: CoroFunc
|
|
39
|
+
seconds: float
|
|
40
|
+
repeat: bool = dataclasses.field(default=False, kw_only=True)
|
|
41
|
+
_cancelled: bool = dataclasses.field(default=False, init=False, repr=False)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def is_cancelled(self) -> bool:
|
|
45
|
+
return self._cancelled
|
|
46
|
+
|
|
47
|
+
async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
|
48
|
+
while not self.is_cancelled:
|
|
49
|
+
await asyncio.sleep(self.seconds)
|
|
50
|
+
if self.is_cancelled:
|
|
51
|
+
break
|
|
52
|
+
try:
|
|
53
|
+
await self.handler(*args, **kwargs)
|
|
54
|
+
finally:
|
|
55
|
+
if not self.repeat:
|
|
56
|
+
break
|
|
57
|
+
|
|
58
|
+
def cancel(self) -> None:
|
|
59
|
+
if not self._cancelled:
|
|
60
|
+
self._cancelled = True
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
64
|
+
class Lifespan:
|
|
65
|
+
startup_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
|
|
66
|
+
shutdown_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
|
|
67
|
+
|
|
68
|
+
def on_startup(self, task: Task, /) -> Task:
|
|
69
|
+
self.startup_tasks.append(to_coroutine_task(task))
|
|
70
|
+
return task
|
|
71
|
+
|
|
72
|
+
def on_shutdown(self, task: Task, /) -> Task:
|
|
73
|
+
self.shutdown_tasks.append(to_coroutine_task(task))
|
|
74
|
+
return task
|
|
75
|
+
|
|
76
|
+
def start(self, loop: asyncio.AbstractEventLoop, /) -> None:
|
|
77
|
+
run_tasks(self.startup_tasks, loop)
|
|
78
|
+
|
|
79
|
+
def shutdown(self, loop: asyncio.AbstractEventLoop, /) -> None:
|
|
80
|
+
run_tasks(self.shutdown_tasks, loop)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class LoopWrapper(ABCLoopWrapper):
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
*,
|
|
87
|
+
tasks: list[CoroutineTask[typing.Any]] | None = None,
|
|
88
|
+
lifespan: Lifespan | None = None,
|
|
89
|
+
event_loop: asyncio.AbstractEventLoop | None = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
self.tasks: list[CoroutineTask[typing.Any]] = tasks or []
|
|
92
|
+
self.lifespan = lifespan or Lifespan()
|
|
93
|
+
self._loop = event_loop or asyncio.new_event_loop()
|
|
94
|
+
|
|
95
|
+
def __repr__(self) -> str:
|
|
96
|
+
return "<{}: loop={!r} with tasks={!r}, lifespan={!r}>".format(
|
|
97
|
+
self.__class__.__name__,
|
|
98
|
+
self._loop,
|
|
99
|
+
self.tasks,
|
|
100
|
+
self.lifespan,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def run_event_loop(self) -> None:
|
|
104
|
+
if not self.tasks:
|
|
105
|
+
logger.warning("You run loop with 0 tasks!")
|
|
106
|
+
|
|
107
|
+
self.lifespan.start(self._loop)
|
|
108
|
+
while self.tasks:
|
|
109
|
+
self._loop.create_task(self.tasks.pop(0))
|
|
110
|
+
tasks = asyncio.all_tasks(self._loop)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
while tasks:
|
|
114
|
+
tasks_results, _ = self._loop.run_until_complete(
|
|
115
|
+
asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION),
|
|
116
|
+
)
|
|
117
|
+
for task_result in tasks_results:
|
|
118
|
+
try:
|
|
119
|
+
task_result.result()
|
|
120
|
+
except BaseException as ex:
|
|
121
|
+
logger.exception(ex)
|
|
122
|
+
tasks = asyncio.all_tasks(self._loop)
|
|
123
|
+
except KeyboardInterrupt:
|
|
124
|
+
print() # blank print for ^C
|
|
125
|
+
logger.info("Caught KeyboardInterrupt, cancellation...")
|
|
126
|
+
self.complete_tasks(tasks)
|
|
127
|
+
finally:
|
|
128
|
+
self.lifespan.shutdown(self._loop)
|
|
129
|
+
if self._loop.is_running():
|
|
130
|
+
self._loop.close()
|
|
131
|
+
|
|
132
|
+
def add_task(self, task: Task) -> None:
|
|
133
|
+
task = to_coroutine_task(task)
|
|
134
|
+
|
|
135
|
+
if self._loop and self._loop.is_running():
|
|
136
|
+
self._loop.create_task(task)
|
|
137
|
+
else:
|
|
138
|
+
self.tasks.append(task)
|
|
139
|
+
|
|
140
|
+
def complete_tasks(self, tasks: set[asyncio.Task[typing.Any]]) -> None:
|
|
141
|
+
tasks = tasks | asyncio.all_tasks(self._loop)
|
|
142
|
+
task_to_cancel = asyncio.gather(*tasks, return_exceptions=True)
|
|
143
|
+
task_to_cancel.cancel()
|
|
144
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
145
|
+
self._loop.run_until_complete(task_to_cancel)
|
|
146
|
+
|
|
147
|
+
@typing.overload
|
|
148
|
+
def timer(self, *, seconds: datetime.timedelta) -> typing.Callable[..., typing.Any]: ...
|
|
149
|
+
|
|
150
|
+
@typing.overload
|
|
151
|
+
def timer(
|
|
152
|
+
self,
|
|
153
|
+
*,
|
|
154
|
+
days: int = 0,
|
|
155
|
+
hours: int = 0,
|
|
156
|
+
minutes: int = 0,
|
|
157
|
+
seconds: float = 0,
|
|
158
|
+
) -> typing.Callable[..., typing.Any]: ...
|
|
159
|
+
|
|
160
|
+
def timer(
|
|
161
|
+
self,
|
|
162
|
+
*,
|
|
163
|
+
days: int = 0,
|
|
164
|
+
hours: int = 0,
|
|
165
|
+
minutes: int = 0,
|
|
166
|
+
seconds: float | datetime.timedelta = 0,
|
|
167
|
+
) -> typing.Callable[..., typing.Any]:
|
|
168
|
+
if isinstance(seconds, datetime.timedelta):
|
|
169
|
+
seconds = seconds.total_seconds()
|
|
170
|
+
|
|
171
|
+
seconds += minutes * 60
|
|
172
|
+
seconds += hours * 60 * 60
|
|
173
|
+
seconds += days * 24 * 60 * 60
|
|
174
|
+
|
|
175
|
+
def decorator(func: CoroFunc) -> CoroFunc:
|
|
176
|
+
self.add_task(DelayedTask(func, seconds, repeat=False))
|
|
177
|
+
return func
|
|
178
|
+
|
|
179
|
+
return decorator
|
|
180
|
+
|
|
181
|
+
@typing.overload
|
|
182
|
+
def interval(self, *, seconds: datetime.timedelta) -> typing.Callable[..., typing.Any]: ...
|
|
183
|
+
|
|
184
|
+
@typing.overload
|
|
185
|
+
def interval(
|
|
186
|
+
self,
|
|
187
|
+
*,
|
|
188
|
+
days: int = 0,
|
|
189
|
+
hours: int = 0,
|
|
190
|
+
minutes: int = 0,
|
|
191
|
+
seconds: float = 0,
|
|
192
|
+
) -> typing.Callable[..., typing.Any]: ...
|
|
193
|
+
|
|
194
|
+
def interval(
|
|
195
|
+
self,
|
|
196
|
+
*,
|
|
197
|
+
days: int = 0,
|
|
198
|
+
hours: int = 0,
|
|
199
|
+
minutes: int = 0,
|
|
200
|
+
seconds: float | datetime.timedelta = 0,
|
|
201
|
+
) -> typing.Callable[..., typing.Any]:
|
|
202
|
+
if isinstance(seconds, datetime.timedelta):
|
|
203
|
+
seconds = seconds.total_seconds()
|
|
204
|
+
|
|
205
|
+
seconds += minutes * 60
|
|
206
|
+
seconds += hours * 60 * 60
|
|
207
|
+
seconds += days * 24 * 60 * 60
|
|
208
|
+
|
|
209
|
+
def decorator(func: CoroFunc) -> CoroFunc:
|
|
210
|
+
self.add_task(DelayedTask(func, seconds, repeat=True))
|
|
211
|
+
return func
|
|
212
|
+
|
|
213
|
+
return decorator
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
__all__ = ("DelayedTask", "Lifespan", "LoopWrapper", "to_coroutine_task")
|