classic-scheduler 1.0.0__tar.gz
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.
- classic-scheduler-1.0.0/LICENSE +21 -0
- classic-scheduler-1.0.0/PKG-INFO +103 -0
- classic-scheduler-1.0.0/README.md +86 -0
- classic-scheduler-1.0.0/pyproject.toml +6 -0
- classic-scheduler-1.0.0/setup.cfg +39 -0
- classic-scheduler-1.0.0/setup.py +3 -0
- classic-scheduler-1.0.0/sources/classic/scheduler/__init__.py +1 -0
- classic-scheduler-1.0.0/sources/classic/scheduler/scheduler.py +245 -0
- classic-scheduler-1.0.0/sources/classic/scheduler/task.py +128 -0
- classic-scheduler-1.0.0/sources/classic_scheduler.egg-info/PKG-INFO +103 -0
- classic-scheduler-1.0.0/sources/classic_scheduler.egg-info/SOURCES.txt +13 -0
- classic-scheduler-1.0.0/sources/classic_scheduler.egg-info/dependency_links.txt +1 -0
- classic-scheduler-1.0.0/sources/classic_scheduler.egg-info/requires.txt +7 -0
- classic-scheduler-1.0.0/sources/classic_scheduler.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Sergei Variasov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: classic-scheduler
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Provides scheduler
|
|
5
|
+
Home-page: https://github.com/variasov/classic-scheduler
|
|
6
|
+
Author: Sergei Variasov, Vasiliy Pichugin
|
|
7
|
+
Author-email: variasov@gmail.com, pichugin_v_i@mail.ru
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/variasov/classic-scheduler/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
|
|
18
|
+
# Classic Scheduler
|
|
19
|
+
|
|
20
|
+
## Планировщик
|
|
21
|
+
|
|
22
|
+
Данная библиотека является продолжением библиотеки акторов `classic actors`. И, как следствие, она зависит от библиотеки акторов.
|
|
23
|
+
|
|
24
|
+
Планировщик - это актор, который вызывает функции по расписанию.
|
|
25
|
+
|
|
26
|
+
Рассмотрим пример использования библиотеки:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
def func():
|
|
30
|
+
print('Hello world!')
|
|
31
|
+
|
|
32
|
+
scheduler = Scheduler()
|
|
33
|
+
scheduler.with_delay(1, func)
|
|
34
|
+
scheduler.by_period(5, func)
|
|
35
|
+
scheduler.by_cron('* * * * *', func)
|
|
36
|
+
scheduler.run()
|
|
37
|
+
|
|
38
|
+
# Hello world!
|
|
39
|
+
# Hello world!
|
|
40
|
+
# Hello world!
|
|
41
|
+
# ...
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Отмена вызова (удаление задачи)
|
|
45
|
+
|
|
46
|
+
Если расписание задач планируется динамически менять, например, удалить ранее созданную задачу, то при создании задачи в методах необходимо указать атрибут `task_name`. Пример:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
scheduler = Scheduler()
|
|
50
|
+
scheduler.by_period(1, func, task_name='periodic task')
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Тогда в дальнейшем вы можете отменить запланированные запуски задач через:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
scheduler.cancel('periodic task')
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Параметризация вызываемой функции
|
|
60
|
+
|
|
61
|
+
Планировщик может запускать и функции с аргументами. Аргументы передаются через атрибуты `args` и `kwargs`. Отдельно заметим, что вызываемые объекты так же могут быть акторами, как в примере ниже:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
class SomeActor(Actor):
|
|
65
|
+
|
|
66
|
+
@Actor.method
|
|
67
|
+
def func(self, a, b):
|
|
68
|
+
print('a + b =', a + b)
|
|
69
|
+
|
|
70
|
+
some_actor = SomeActor()
|
|
71
|
+
some_actor.run()
|
|
72
|
+
|
|
73
|
+
scheduler = Scheduler()
|
|
74
|
+
scheduler.run()
|
|
75
|
+
scheduler.by_period(
|
|
76
|
+
2,
|
|
77
|
+
some_actor.func,
|
|
78
|
+
args=(2, 6),
|
|
79
|
+
task_name='some_actor_task',
|
|
80
|
+
)
|
|
81
|
+
# a + b = 8
|
|
82
|
+
# a + b = 8
|
|
83
|
+
# a + b = 8
|
|
84
|
+
# ...
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Типы задач
|
|
88
|
+
|
|
89
|
+
После создания экземпляра планировщика нужно добавить в него задач. Планирование и выполнение задач начнется только после запуска метода `run()`. Отметим, что добавлять задачи в планировщик можно и после вызова метода `run()`.
|
|
90
|
+
|
|
91
|
+
Планировщик поддерживает три типы задач:
|
|
92
|
+
|
|
93
|
+
### Одноразовая отложенная задача
|
|
94
|
+
|
|
95
|
+
Создается через вызов метода планировщика `with_delay`. Задача будет выполнена ровно один раз. Ее вызов отложен на `delay` указанный либо в секундах в `float` либо в `timedelta`.
|
|
96
|
+
|
|
97
|
+
### Переодическая задача
|
|
98
|
+
|
|
99
|
+
Создается через вызов метода планировщика `by_period`. Задача выполнится в первый раз, затем она будет выполняется через период `periodic` заданный либо в секундах в `float` либо в `timedelta`.
|
|
100
|
+
|
|
101
|
+
### Задача по расписанию
|
|
102
|
+
|
|
103
|
+
Создается через вызов метода планировщика `by_cron`. Задача вызов который задается в формате `CRON` строкой.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Classic Scheduler
|
|
2
|
+
|
|
3
|
+
## Планировщик
|
|
4
|
+
|
|
5
|
+
Данная библиотека является продолжением библиотеки акторов `classic actors`. И, как следствие, она зависит от библиотеки акторов.
|
|
6
|
+
|
|
7
|
+
Планировщик - это актор, который вызывает функции по расписанию.
|
|
8
|
+
|
|
9
|
+
Рассмотрим пример использования библиотеки:
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
def func():
|
|
13
|
+
print('Hello world!')
|
|
14
|
+
|
|
15
|
+
scheduler = Scheduler()
|
|
16
|
+
scheduler.with_delay(1, func)
|
|
17
|
+
scheduler.by_period(5, func)
|
|
18
|
+
scheduler.by_cron('* * * * *', func)
|
|
19
|
+
scheduler.run()
|
|
20
|
+
|
|
21
|
+
# Hello world!
|
|
22
|
+
# Hello world!
|
|
23
|
+
# Hello world!
|
|
24
|
+
# ...
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Отмена вызова (удаление задачи)
|
|
28
|
+
|
|
29
|
+
Если расписание задач планируется динамически менять, например, удалить ранее созданную задачу, то при создании задачи в методах необходимо указать атрибут `task_name`. Пример:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
scheduler = Scheduler()
|
|
33
|
+
scheduler.by_period(1, func, task_name='periodic task')
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Тогда в дальнейшем вы можете отменить запланированные запуски задач через:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
scheduler.cancel('periodic task')
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Параметризация вызываемой функции
|
|
43
|
+
|
|
44
|
+
Планировщик может запускать и функции с аргументами. Аргументы передаются через атрибуты `args` и `kwargs`. Отдельно заметим, что вызываемые объекты так же могут быть акторами, как в примере ниже:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
class SomeActor(Actor):
|
|
48
|
+
|
|
49
|
+
@Actor.method
|
|
50
|
+
def func(self, a, b):
|
|
51
|
+
print('a + b =', a + b)
|
|
52
|
+
|
|
53
|
+
some_actor = SomeActor()
|
|
54
|
+
some_actor.run()
|
|
55
|
+
|
|
56
|
+
scheduler = Scheduler()
|
|
57
|
+
scheduler.run()
|
|
58
|
+
scheduler.by_period(
|
|
59
|
+
2,
|
|
60
|
+
some_actor.func,
|
|
61
|
+
args=(2, 6),
|
|
62
|
+
task_name='some_actor_task',
|
|
63
|
+
)
|
|
64
|
+
# a + b = 8
|
|
65
|
+
# a + b = 8
|
|
66
|
+
# a + b = 8
|
|
67
|
+
# ...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Типы задач
|
|
71
|
+
|
|
72
|
+
После создания экземпляра планировщика нужно добавить в него задач. Планирование и выполнение задач начнется только после запуска метода `run()`. Отметим, что добавлять задачи в планировщик можно и после вызова метода `run()`.
|
|
73
|
+
|
|
74
|
+
Планировщик поддерживает три типы задач:
|
|
75
|
+
|
|
76
|
+
### Одноразовая отложенная задача
|
|
77
|
+
|
|
78
|
+
Создается через вызов метода планировщика `with_delay`. Задача будет выполнена ровно один раз. Ее вызов отложен на `delay` указанный либо в секундах в `float` либо в `timedelta`.
|
|
79
|
+
|
|
80
|
+
### Переодическая задача
|
|
81
|
+
|
|
82
|
+
Создается через вызов метода планировщика `by_period`. Задача выполнится в первый раз, затем она будет выполняется через период `periodic` заданный либо в секундах в `float` либо в `timedelta`.
|
|
83
|
+
|
|
84
|
+
### Задача по расписанию
|
|
85
|
+
|
|
86
|
+
Создается через вызов метода планировщика `by_cron`. Задача вызов который задается в формате `CRON` строкой.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = classic-scheduler
|
|
3
|
+
version = 1.0.0
|
|
4
|
+
description = Provides scheduler
|
|
5
|
+
author = Sergei Variasov, Vasiliy Pichugin
|
|
6
|
+
author_email = variasov@gmail.com, pichugin_v_i@mail.ru
|
|
7
|
+
long_description = file: README.md
|
|
8
|
+
long_description_content_type = text/markdown
|
|
9
|
+
url = https://github.com/variasov/classic-scheduler
|
|
10
|
+
project_urls =
|
|
11
|
+
Bug Tracker = https://github.com/variasov/classic-scheduler/issues
|
|
12
|
+
classifiers =
|
|
13
|
+
Programming Language :: Python :: 3.10
|
|
14
|
+
License :: OSI Approved :: MIT License
|
|
15
|
+
Operating System :: OS Independent
|
|
16
|
+
Development Status :: 4 - Beta
|
|
17
|
+
|
|
18
|
+
[options]
|
|
19
|
+
packages = find_namespace:
|
|
20
|
+
python_requires = >=3.10
|
|
21
|
+
install_requires =
|
|
22
|
+
croniter
|
|
23
|
+
package_dir =
|
|
24
|
+
= sources
|
|
25
|
+
|
|
26
|
+
[options.packages.find]
|
|
27
|
+
where = sources
|
|
28
|
+
|
|
29
|
+
[options.extras_require]
|
|
30
|
+
dev =
|
|
31
|
+
pytest~=6.2
|
|
32
|
+
pytest-cov~=2.12
|
|
33
|
+
twine~=3.4
|
|
34
|
+
build~=0.7
|
|
35
|
+
|
|
36
|
+
[egg_info]
|
|
37
|
+
tag_build =
|
|
38
|
+
tag_date = 0
|
|
39
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .scheduler import BaseScheduler, SimpleScheduler, ThreadPoolScheduler
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import heapq
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
from queue import Empty, Queue
|
|
6
|
+
from threading import Thread
|
|
7
|
+
from typing import Any, Callable, Union
|
|
8
|
+
|
|
9
|
+
from .task import (STOP, CronTask, OneTimeTask, PeriodicTask,
|
|
10
|
+
ScheduleCancellation, Task)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseScheduler(ABC):
|
|
14
|
+
"""
|
|
15
|
+
Планировщик задач. Вызывает переданные ему Callable объекты в соответствии
|
|
16
|
+
с расписанием.
|
|
17
|
+
"""
|
|
18
|
+
_stopped: bool
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
super().__init__()
|
|
22
|
+
self._thread = Thread(target=self.loop)
|
|
23
|
+
self._tasks = []
|
|
24
|
+
self._inbox = Queue()
|
|
25
|
+
|
|
26
|
+
def _get_timeout(self) -> float:
|
|
27
|
+
"""
|
|
28
|
+
Возвращает время ожидания выполнения следующей задачи.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
float: тайм-аут ожидания задачи.
|
|
32
|
+
"""
|
|
33
|
+
if len(self._tasks) < 1:
|
|
34
|
+
return 60
|
|
35
|
+
|
|
36
|
+
dt_next_task = self._tasks[0].next_run_time
|
|
37
|
+
dt_current = datetime.now(timezone.utc)
|
|
38
|
+
if dt_next_task < dt_current:
|
|
39
|
+
return 0
|
|
40
|
+
else:
|
|
41
|
+
return (dt_next_task - dt_current).total_seconds()
|
|
42
|
+
|
|
43
|
+
def _schedule(self, task: Task) -> None:
|
|
44
|
+
heapq.heappush(self._tasks, task)
|
|
45
|
+
|
|
46
|
+
def _unshedule(self, task_name: str) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Отменяет выполнение запланированной задачи.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
task_name (str): Имя задачи для отмены.
|
|
52
|
+
"""
|
|
53
|
+
tasks = [t for t in self._tasks if t.name != task_name]
|
|
54
|
+
if len(self._tasks) != len(tasks):
|
|
55
|
+
heapq.heapify(tasks)
|
|
56
|
+
self._tasks = tasks
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def _execute(self, task: Task) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Выполняет текущую задачу.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
task (Task): Задача, которую необходимо выполнить.
|
|
65
|
+
"""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def _stop(self) -> None:
|
|
69
|
+
self._stopped = True
|
|
70
|
+
|
|
71
|
+
def loop(self) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Главный цикл выполнения планировщика.
|
|
74
|
+
"""
|
|
75
|
+
self._stopped = False
|
|
76
|
+
while not self._stopped:
|
|
77
|
+
try:
|
|
78
|
+
timeout = self._get_timeout()
|
|
79
|
+
if timeout > 0:
|
|
80
|
+
try:
|
|
81
|
+
command = self._inbox.get(block=True, timeout=timeout)
|
|
82
|
+
except Empty:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
if isinstance(command, Task):
|
|
86
|
+
self._schedule(command)
|
|
87
|
+
elif isinstance(command, ScheduleCancellation):
|
|
88
|
+
self._unshedule(command.task_name)
|
|
89
|
+
elif command is STOP:
|
|
90
|
+
break
|
|
91
|
+
else:
|
|
92
|
+
task: Task = heapq.heappop(self._tasks)
|
|
93
|
+
self._execute(task)
|
|
94
|
+
|
|
95
|
+
task.set_next_run_time()
|
|
96
|
+
if task.next_run_time:
|
|
97
|
+
self._schedule(task)
|
|
98
|
+
except KeyboardInterrupt:
|
|
99
|
+
break
|
|
100
|
+
self._stop()
|
|
101
|
+
|
|
102
|
+
def with_delay(
|
|
103
|
+
self,
|
|
104
|
+
delay: Union[timedelta, float],
|
|
105
|
+
job: Callable[[], None],
|
|
106
|
+
args: tuple = None,
|
|
107
|
+
kwargs: dict = None,
|
|
108
|
+
task_name: str = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Добавляет отложенную задачу в планировщик.
|
|
112
|
+
Задача будет выполнена ровно один раз.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
delay (Union[timedelta, float]): Отсрочка выполнения задачи.
|
|
116
|
+
job (Callable[[], Any]): Вызываемая в задаче функция.
|
|
117
|
+
args (tuple, optional): Аргументы. По умолчанию None.
|
|
118
|
+
kwargs (dict, optional): Аргументы. По умолчанию None.
|
|
119
|
+
task_name (str, optional): Имя таски. По умолчанию None.
|
|
120
|
+
"""
|
|
121
|
+
if not isinstance(delay, timedelta):
|
|
122
|
+
delay = timedelta(seconds=delay)
|
|
123
|
+
|
|
124
|
+
date = datetime.now(timezone.utc) + delay
|
|
125
|
+
task = OneTimeTask(
|
|
126
|
+
date=date,
|
|
127
|
+
job=job,
|
|
128
|
+
args=args,
|
|
129
|
+
kwargs=kwargs,
|
|
130
|
+
name=task_name,
|
|
131
|
+
)
|
|
132
|
+
self._inbox.put(task)
|
|
133
|
+
|
|
134
|
+
def by_cron(
|
|
135
|
+
self,
|
|
136
|
+
schedule: str,
|
|
137
|
+
job: Callable[[], Any],
|
|
138
|
+
args: tuple = None,
|
|
139
|
+
kwargs: dict = None,
|
|
140
|
+
task_name: str = None,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Добавляет задачу которая будет выполнятся по CRON расписанию.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
schedule (str): Расписание задачи в CRON формате.
|
|
147
|
+
job (Callable[[], Any]): Вызываемая в задаче функция.
|
|
148
|
+
args (tuple, optional): Аргументы. По умолчанию None.
|
|
149
|
+
kwargs (dict, optional): Аргументы. По умолчанию None.
|
|
150
|
+
task_name (str, optional): Имя таски. По умолчанию None.
|
|
151
|
+
"""
|
|
152
|
+
task = CronTask(
|
|
153
|
+
schedule=schedule,
|
|
154
|
+
job=job,
|
|
155
|
+
args=args,
|
|
156
|
+
kwargs=kwargs,
|
|
157
|
+
name=task_name,
|
|
158
|
+
)
|
|
159
|
+
self._inbox.put(task)
|
|
160
|
+
|
|
161
|
+
def by_period(
|
|
162
|
+
self,
|
|
163
|
+
period: Union[timedelta, float],
|
|
164
|
+
job: Callable[[], Any],
|
|
165
|
+
args: tuple = None,
|
|
166
|
+
kwargs: dict = None,
|
|
167
|
+
task_name: str = None,
|
|
168
|
+
) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Добавляет задачу которая будет выполняется периодически через
|
|
171
|
+
указанный интервал времени.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
period (Union[timedelta, float]): Период выполнения задачи.
|
|
175
|
+
job (Callable[[], Any]): Вызываемая в задаче функция.
|
|
176
|
+
args (tuple, optional): Аргументы. По умолчанию None.
|
|
177
|
+
kwargs (dict, optional): Аргументы. По умолчанию None.
|
|
178
|
+
task_name (_type_, optional): Имя таски. По умолчанию None.
|
|
179
|
+
"""
|
|
180
|
+
if not isinstance(period, timedelta):
|
|
181
|
+
period = timedelta(seconds=period)
|
|
182
|
+
|
|
183
|
+
task = PeriodicTask(
|
|
184
|
+
period=period,
|
|
185
|
+
job=job,
|
|
186
|
+
args=args,
|
|
187
|
+
kwargs=kwargs,
|
|
188
|
+
name=task_name,
|
|
189
|
+
)
|
|
190
|
+
self._inbox.put(task)
|
|
191
|
+
|
|
192
|
+
def unshedule(self, task_name: str) -> None:
|
|
193
|
+
"""
|
|
194
|
+
Отменяет выполнение запланированной задачи.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
task_name (str): Имя задачи для отмены.
|
|
198
|
+
"""
|
|
199
|
+
cancellation = ScheduleCancellation(task_name=task_name)
|
|
200
|
+
self._inbox.put(cancellation)
|
|
201
|
+
|
|
202
|
+
def start(self) -> None:
|
|
203
|
+
"""
|
|
204
|
+
Запускает выполнение планировщика.
|
|
205
|
+
"""
|
|
206
|
+
self._thread.start()
|
|
207
|
+
|
|
208
|
+
def stop(self) -> None:
|
|
209
|
+
"""
|
|
210
|
+
Завершает выполнение планировщика.
|
|
211
|
+
"""
|
|
212
|
+
self._inbox.put(STOP)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class SimpleScheduler(BaseScheduler):
|
|
216
|
+
"""
|
|
217
|
+
Планировщик задач. Вызывает переданные ему Callable объекты в своем потоке
|
|
218
|
+
в соответствии с расписанием. Поскольку поток один, то не допускается
|
|
219
|
+
параллельное выполнение задач.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
def _execute(self, task: Task) -> None:
|
|
223
|
+
task.run_job()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class ThreadPoolScheduler(BaseScheduler):
|
|
227
|
+
"""
|
|
228
|
+
Планировщик задач. Вызывает переданные ему Callable объекты в пуле потоков
|
|
229
|
+
в соответствии с расписанием.
|
|
230
|
+
|
|
231
|
+
Данный планировщик не предоставляет гарантий
|
|
232
|
+
того, что одна и та же задача не будет работать в разных потоках
|
|
233
|
+
одновременно.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
def __init__(self, workers_count: int = 5):
|
|
237
|
+
super().__init__()
|
|
238
|
+
self._thread_pool = ThreadPoolExecutor(max_workers=workers_count)
|
|
239
|
+
|
|
240
|
+
def _execute(self, task: Task) -> None:
|
|
241
|
+
self._thread_pool.submit(task.run_job)
|
|
242
|
+
|
|
243
|
+
def _stop(self) -> None:
|
|
244
|
+
super()._stop()
|
|
245
|
+
self._thread_pool.shutdown(wait=True)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
from typing import Any, Callable, Optional
|
|
6
|
+
|
|
7
|
+
from croniter import croniter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Task(ABC):
|
|
11
|
+
"""
|
|
12
|
+
Задача, выполняемая в потоке планировщика.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
job: Callable[[...], None],
|
|
18
|
+
args: tuple = None,
|
|
19
|
+
kwargs: dict = None,
|
|
20
|
+
name: Optional[str] = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
self._job = job
|
|
23
|
+
self._args = args or ()
|
|
24
|
+
self._kwargs = kwargs or {}
|
|
25
|
+
self._name = name
|
|
26
|
+
self._next_run_time = None # время следующего запуска задачи
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def name(self) -> str:
|
|
30
|
+
return self._name
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def next_run_time(self) -> datetime:
|
|
34
|
+
"""
|
|
35
|
+
Свойство возвращающее время следующего запуска.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
datetime: Время следующего запуска.
|
|
39
|
+
"""
|
|
40
|
+
return self._next_run_time
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def set_next_run_time(self) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Метод рассчитывающий у устанавливающий время следующего запуска.
|
|
46
|
+
Переопределяется в дочерних классах.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def run_job(self) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Вызывает переданный Callable объект со всем аргументами
|
|
52
|
+
в потоке планировщика.
|
|
53
|
+
"""
|
|
54
|
+
logging.info(
|
|
55
|
+
'Task [%s] started with schedule [%s]',
|
|
56
|
+
self._name,
|
|
57
|
+
self._next_run_time,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
self._job(*self._args, **self._kwargs)
|
|
62
|
+
except Exception as ex:
|
|
63
|
+
logging.exception(
|
|
64
|
+
'Unexpected error occurred in task [%s]: "%s".',
|
|
65
|
+
self._name,
|
|
66
|
+
ex,
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
logging.info('Task completed [%s]', self._name)
|
|
70
|
+
|
|
71
|
+
def __lt__(self, other: 'Task') -> bool:
|
|
72
|
+
return self._next_run_time < other._next_run_time
|
|
73
|
+
|
|
74
|
+
def __gt__(self, other: 'Task') -> bool:
|
|
75
|
+
return self._next_run_time > other._next_run_time
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class OneTimeTask(Task):
|
|
79
|
+
"""
|
|
80
|
+
Одноразовая задача, которая будет выполнена ровно один раз.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, date: datetime, *args: Any, **kwargs: Any) -> None:
|
|
84
|
+
super().__init__(*args, **kwargs)
|
|
85
|
+
self._next_run_time = date
|
|
86
|
+
|
|
87
|
+
def set_next_run_time(self) -> None:
|
|
88
|
+
self._next_run_time = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class CronTask(Task):
|
|
92
|
+
"""
|
|
93
|
+
Задача которая выполняется по CRON расписанию.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, schedule: str, *args: Any, **kwargs: Any) -> None:
|
|
97
|
+
super().__init__(*args, **kwargs)
|
|
98
|
+
self._schedule = croniter(schedule, datetime.now(timezone.utc))
|
|
99
|
+
self._next_run_time = self._schedule.get_next(datetime)
|
|
100
|
+
|
|
101
|
+
def set_next_run_time(self) -> None:
|
|
102
|
+
self._next_run_time = self._schedule.get_next(datetime)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class PeriodicTask(Task):
|
|
106
|
+
"""
|
|
107
|
+
Задача которая будет выполняется переодически
|
|
108
|
+
через указанный интервал времени.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, period: timedelta, *args: Any, **kwargs: Any) -> None:
|
|
112
|
+
super().__init__(*args, **kwargs)
|
|
113
|
+
self._period = period
|
|
114
|
+
self._next_run_time = datetime.now(timezone.utc)
|
|
115
|
+
|
|
116
|
+
def set_next_run_time(self) -> None:
|
|
117
|
+
self._next_run_time = datetime.now(timezone.utc) + self._period
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class ScheduleCancellation:
|
|
122
|
+
"""
|
|
123
|
+
Команда отмены запланированной задачи.
|
|
124
|
+
"""
|
|
125
|
+
task_name: str
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
STOP = object()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: classic-scheduler
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Provides scheduler
|
|
5
|
+
Home-page: https://github.com/variasov/classic-scheduler
|
|
6
|
+
Author: Sergei Variasov, Vasiliy Pichugin
|
|
7
|
+
Author-email: variasov@gmail.com, pichugin_v_i@mail.ru
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/variasov/classic-scheduler/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
|
|
18
|
+
# Classic Scheduler
|
|
19
|
+
|
|
20
|
+
## Планировщик
|
|
21
|
+
|
|
22
|
+
Данная библиотека является продолжением библиотеки акторов `classic actors`. И, как следствие, она зависит от библиотеки акторов.
|
|
23
|
+
|
|
24
|
+
Планировщик - это актор, который вызывает функции по расписанию.
|
|
25
|
+
|
|
26
|
+
Рассмотрим пример использования библиотеки:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
def func():
|
|
30
|
+
print('Hello world!')
|
|
31
|
+
|
|
32
|
+
scheduler = Scheduler()
|
|
33
|
+
scheduler.with_delay(1, func)
|
|
34
|
+
scheduler.by_period(5, func)
|
|
35
|
+
scheduler.by_cron('* * * * *', func)
|
|
36
|
+
scheduler.run()
|
|
37
|
+
|
|
38
|
+
# Hello world!
|
|
39
|
+
# Hello world!
|
|
40
|
+
# Hello world!
|
|
41
|
+
# ...
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Отмена вызова (удаление задачи)
|
|
45
|
+
|
|
46
|
+
Если расписание задач планируется динамически менять, например, удалить ранее созданную задачу, то при создании задачи в методах необходимо указать атрибут `task_name`. Пример:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
scheduler = Scheduler()
|
|
50
|
+
scheduler.by_period(1, func, task_name='periodic task')
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Тогда в дальнейшем вы можете отменить запланированные запуски задач через:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
scheduler.cancel('periodic task')
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Параметризация вызываемой функции
|
|
60
|
+
|
|
61
|
+
Планировщик может запускать и функции с аргументами. Аргументы передаются через атрибуты `args` и `kwargs`. Отдельно заметим, что вызываемые объекты так же могут быть акторами, как в примере ниже:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
class SomeActor(Actor):
|
|
65
|
+
|
|
66
|
+
@Actor.method
|
|
67
|
+
def func(self, a, b):
|
|
68
|
+
print('a + b =', a + b)
|
|
69
|
+
|
|
70
|
+
some_actor = SomeActor()
|
|
71
|
+
some_actor.run()
|
|
72
|
+
|
|
73
|
+
scheduler = Scheduler()
|
|
74
|
+
scheduler.run()
|
|
75
|
+
scheduler.by_period(
|
|
76
|
+
2,
|
|
77
|
+
some_actor.func,
|
|
78
|
+
args=(2, 6),
|
|
79
|
+
task_name='some_actor_task',
|
|
80
|
+
)
|
|
81
|
+
# a + b = 8
|
|
82
|
+
# a + b = 8
|
|
83
|
+
# a + b = 8
|
|
84
|
+
# ...
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Типы задач
|
|
88
|
+
|
|
89
|
+
После создания экземпляра планировщика нужно добавить в него задач. Планирование и выполнение задач начнется только после запуска метода `run()`. Отметим, что добавлять задачи в планировщик можно и после вызова метода `run()`.
|
|
90
|
+
|
|
91
|
+
Планировщик поддерживает три типы задач:
|
|
92
|
+
|
|
93
|
+
### Одноразовая отложенная задача
|
|
94
|
+
|
|
95
|
+
Создается через вызов метода планировщика `with_delay`. Задача будет выполнена ровно один раз. Ее вызов отложен на `delay` указанный либо в секундах в `float` либо в `timedelta`.
|
|
96
|
+
|
|
97
|
+
### Переодическая задача
|
|
98
|
+
|
|
99
|
+
Создается через вызов метода планировщика `by_period`. Задача выполнится в первый раз, затем она будет выполняется через период `periodic` заданный либо в секундах в `float` либо в `timedelta`.
|
|
100
|
+
|
|
101
|
+
### Задача по расписанию
|
|
102
|
+
|
|
103
|
+
Создается через вызов метода планировщика `by_cron`. Задача вызов который задается в формате `CRON` строкой.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.cfg
|
|
5
|
+
setup.py
|
|
6
|
+
sources/classic/scheduler/__init__.py
|
|
7
|
+
sources/classic/scheduler/scheduler.py
|
|
8
|
+
sources/classic/scheduler/task.py
|
|
9
|
+
sources/classic_scheduler.egg-info/PKG-INFO
|
|
10
|
+
sources/classic_scheduler.egg-info/SOURCES.txt
|
|
11
|
+
sources/classic_scheduler.egg-info/dependency_links.txt
|
|
12
|
+
sources/classic_scheduler.egg-info/requires.txt
|
|
13
|
+
sources/classic_scheduler.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
classic
|