patera-taskmanager 0.1.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.
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task manager class
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import (
|
|
6
|
+
Callable,
|
|
7
|
+
Tuple,
|
|
8
|
+
Optional,
|
|
9
|
+
Type,
|
|
10
|
+
cast,
|
|
11
|
+
TYPE_CHECKING,
|
|
12
|
+
Any,
|
|
13
|
+
TypedDict,
|
|
14
|
+
NotRequired,
|
|
15
|
+
)
|
|
16
|
+
from functools import wraps
|
|
17
|
+
|
|
18
|
+
from apscheduler.job import Job
|
|
19
|
+
from apscheduler.schedulers.base import BaseScheduler
|
|
20
|
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
21
|
+
from apscheduler.jobstores.base import JobLookupError, BaseJobStore
|
|
22
|
+
from apscheduler.jobstores.memory import MemoryJobStore
|
|
23
|
+
from apscheduler.executors.base import BaseExecutor
|
|
24
|
+
from apscheduler.executors.asyncio import AsyncIOExecutor
|
|
25
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
26
|
+
|
|
27
|
+
from patera.utilities import run_sync_or_async, run_in_background
|
|
28
|
+
from patera.base_extension import BaseExtension
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from patera import Patera
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _TaskManagerConfigs(BaseModel):
|
|
35
|
+
"""Configuration model for TaskManager extension."""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
38
|
+
|
|
39
|
+
NICE_NAME: Optional[str] = Field(
|
|
40
|
+
"Task manager",
|
|
41
|
+
description="Human readable name for the task manager for the admin dashboard",
|
|
42
|
+
)
|
|
43
|
+
SCHEDULER: Optional[Type[BaseScheduler]] = Field(
|
|
44
|
+
default=AsyncIOScheduler,
|
|
45
|
+
description="Scheduler class to use, must be subclass of apscheduler.schedulers.BaseScheduler. Default AsyncIOScheduler",
|
|
46
|
+
)
|
|
47
|
+
JOB_STORES: Optional[dict[str, BaseJobStore]] = Field(
|
|
48
|
+
default={"default": MemoryJobStore()},
|
|
49
|
+
description="Job stores configuration dictionary. Default MemoryJobStore",
|
|
50
|
+
)
|
|
51
|
+
EXECUTORS: Optional[dict[str, BaseExecutor]] = Field(
|
|
52
|
+
default={"default": AsyncIOExecutor()},
|
|
53
|
+
description="Executors configuration dictionary. Default AsyncIOExecutor",
|
|
54
|
+
)
|
|
55
|
+
JOB_DEFAULTS: Optional[dict[str, bool | int]] = Field(
|
|
56
|
+
default={"coalesce": False, "max_instances": 3},
|
|
57
|
+
description="Default job settings dictionary",
|
|
58
|
+
)
|
|
59
|
+
DAEMON: Optional[bool] = Field(
|
|
60
|
+
default=True,
|
|
61
|
+
description="Whether the scheduler should run as a daemon. Default True",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TaskManagerConfig(TypedDict):
|
|
66
|
+
"""TypedDict for TaskManager configuration."""
|
|
67
|
+
|
|
68
|
+
NICE_NAME: NotRequired[str]
|
|
69
|
+
SCHEDULER: NotRequired[Type[BaseScheduler]]
|
|
70
|
+
JOB_STORES: NotRequired[dict[str, BaseJobStore]]
|
|
71
|
+
EXECUTORS: NotRequired[dict[str, BaseExecutor]]
|
|
72
|
+
JOB_DEFAULTS: NotRequired[dict[str, bool | int]]
|
|
73
|
+
DAEMON: NotRequired[bool]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TaskManager(BaseExtension):
|
|
77
|
+
"""
|
|
78
|
+
Task manager class for scheduling and managing backgroudn tasks.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, configs_name: str = "TASK_MANAGER") -> None:
|
|
82
|
+
self._configs_name: str = cast(str, configs_name)
|
|
83
|
+
self._configs: dict[str, Any] = {}
|
|
84
|
+
self._app: "Patera"
|
|
85
|
+
self._job_stores: dict
|
|
86
|
+
self._executors: dict
|
|
87
|
+
self._job_defaults: dict
|
|
88
|
+
self._daemon: bool
|
|
89
|
+
self._scheduler: AsyncIOScheduler
|
|
90
|
+
self._initial_jobs_methods_list: list[Tuple] = []
|
|
91
|
+
self._active_jobs: dict[str, Job] = {}
|
|
92
|
+
|
|
93
|
+
def init_app(self, app: "Patera"):
|
|
94
|
+
"""
|
|
95
|
+
Initlizer for TaskManager with Patera app
|
|
96
|
+
"""
|
|
97
|
+
self._app = app # type: ignore
|
|
98
|
+
self._configs = app.get_conf(self._configs_name, {})
|
|
99
|
+
|
|
100
|
+
self._configs = self.validate_configs(self._configs, _TaskManagerConfigs)
|
|
101
|
+
self._job_stores = self._configs["JOB_STORES"]
|
|
102
|
+
self._executors = self._configs["EXECUTORS"]
|
|
103
|
+
self._job_defaults = self._configs["JOB_DEFAULTS"]
|
|
104
|
+
self._daemon = self._configs["DAEMON"]
|
|
105
|
+
self._scheduler = self._configs["SCHEDULER"]
|
|
106
|
+
|
|
107
|
+
self._scheduler = self._scheduler(
|
|
108
|
+
jobstores=self._job_stores,
|
|
109
|
+
executors=self._executors,
|
|
110
|
+
job_defaults=self._job_defaults,
|
|
111
|
+
daemon=self._daemon,
|
|
112
|
+
) # type: ignore
|
|
113
|
+
self._app.add_extension(self)
|
|
114
|
+
self._get_defined_jobs()
|
|
115
|
+
self._app.add_on_startup_method(self._start_scheduler)
|
|
116
|
+
self._app.add_on_shutdown_method(self._stop_scheduler)
|
|
117
|
+
|
|
118
|
+
def pause_scheduler(self):
|
|
119
|
+
"""
|
|
120
|
+
Pauses scheduler execution
|
|
121
|
+
"""
|
|
122
|
+
self.scheduler.pause()
|
|
123
|
+
|
|
124
|
+
def resume_scheduler(self):
|
|
125
|
+
"""
|
|
126
|
+
Resumes paused scheduler execution
|
|
127
|
+
"""
|
|
128
|
+
self.scheduler.resume()
|
|
129
|
+
|
|
130
|
+
async def _start_scheduler(self):
|
|
131
|
+
"""
|
|
132
|
+
On startup hook for starting the scheduler
|
|
133
|
+
"""
|
|
134
|
+
self.scheduler.start()
|
|
135
|
+
self._start_initial_jobs()
|
|
136
|
+
|
|
137
|
+
async def _stop_scheduler(self):
|
|
138
|
+
"""
|
|
139
|
+
On shutdown hook for shuting the scheduler down
|
|
140
|
+
"""
|
|
141
|
+
self.scheduler.shutdown()
|
|
142
|
+
|
|
143
|
+
def _get_defined_jobs(self):
|
|
144
|
+
for name in dir(self):
|
|
145
|
+
method = getattr(self, name)
|
|
146
|
+
if not callable(method):
|
|
147
|
+
continue
|
|
148
|
+
scheduler_method = getattr(method, "_scheduler_job", None)
|
|
149
|
+
if scheduler_method:
|
|
150
|
+
self._initial_jobs_methods_list.append(
|
|
151
|
+
(method, scheduler_method["args"], scheduler_method["kwargs"])
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def _start_initial_jobs(self) -> None:
|
|
155
|
+
"""
|
|
156
|
+
Starts all initial jobs (decorated functions)
|
|
157
|
+
"""
|
|
158
|
+
if (
|
|
159
|
+
self._initial_jobs_methods_list is None
|
|
160
|
+
or len(self._initial_jobs_methods_list) == 0
|
|
161
|
+
):
|
|
162
|
+
return
|
|
163
|
+
for func, args, kwargs in self._initial_jobs_methods_list:
|
|
164
|
+
job: Job = self.scheduler.add_job(func, *args, **kwargs)
|
|
165
|
+
self._active_jobs[job.id] = job
|
|
166
|
+
self._initial_jobs_methods_list = []
|
|
167
|
+
|
|
168
|
+
def run_background_task(self, func: Callable, *args, **kwargs):
|
|
169
|
+
"""
|
|
170
|
+
Runs a method in the background (fire and forget).
|
|
171
|
+
Used for running functions whose execution doesn't have to be awaited/returned to the user.
|
|
172
|
+
Example: a route handler can return a response immediately, and the
|
|
173
|
+
task is executed in a seperate thread (sending an email for example).
|
|
174
|
+
|
|
175
|
+
Uses the patera.utilities.run_in_background method
|
|
176
|
+
"""
|
|
177
|
+
run_in_background(func, *args, **kwargs)
|
|
178
|
+
|
|
179
|
+
def add_job(self, func: Callable, *args, **kwargs) -> Job:
|
|
180
|
+
"""
|
|
181
|
+
Adds job
|
|
182
|
+
"""
|
|
183
|
+
job: Job = self.scheduler.add_job(func, *args, **kwargs)
|
|
184
|
+
self._active_jobs[job.id] = job
|
|
185
|
+
return job
|
|
186
|
+
|
|
187
|
+
def remove_job(self, job: str | Job, job_store: Optional[str] = None):
|
|
188
|
+
"""
|
|
189
|
+
Removes a job.
|
|
190
|
+
:param job: job id (str) or the Job instance returned by the scheduler.add_job method
|
|
191
|
+
"""
|
|
192
|
+
if isinstance(job, Job):
|
|
193
|
+
job = job.id
|
|
194
|
+
return self._remove_job(cast(str, job), job_store)
|
|
195
|
+
|
|
196
|
+
def pause_job(self, job: str | Job):
|
|
197
|
+
"""
|
|
198
|
+
Pauses the job
|
|
199
|
+
"""
|
|
200
|
+
if isinstance(job, Job):
|
|
201
|
+
return job.pause()
|
|
202
|
+
active_job: Optional[Job] = self._active_jobs.get(job, None)
|
|
203
|
+
if job is None:
|
|
204
|
+
raise JobLookupError(job)
|
|
205
|
+
return cast(Job, active_job).pause()
|
|
206
|
+
|
|
207
|
+
def resume_job(self, job: str | Job):
|
|
208
|
+
"""
|
|
209
|
+
Resumes job
|
|
210
|
+
:param paused_job: id or Job instance
|
|
211
|
+
"""
|
|
212
|
+
if isinstance(job, Job):
|
|
213
|
+
return job.resume()
|
|
214
|
+
paused_job: Optional[Job] = self._active_jobs.get(job, None)
|
|
215
|
+
if paused_job is None:
|
|
216
|
+
raise JobLookupError(paused_job)
|
|
217
|
+
return paused_job.resume()
|
|
218
|
+
|
|
219
|
+
def get_job(self, job_id: str) -> Job | None:
|
|
220
|
+
return self._active_jobs.get(job_id, None)
|
|
221
|
+
|
|
222
|
+
def _remove_job(self, job_id: str, job_store=None):
|
|
223
|
+
"""
|
|
224
|
+
Removes job from job list
|
|
225
|
+
"""
|
|
226
|
+
self.scheduler.remove_job(job_id, job_store)
|
|
227
|
+
del self._active_jobs[job_id]
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def jobs(self) -> dict[str, Job]:
|
|
231
|
+
"""
|
|
232
|
+
Returns dictionary of running jobs
|
|
233
|
+
"""
|
|
234
|
+
return self._active_jobs
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def scheduler(self) -> AsyncIOScheduler:
|
|
238
|
+
"""
|
|
239
|
+
Returns the background scheduler instance
|
|
240
|
+
"""
|
|
241
|
+
return self._scheduler
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def nice_name(self) -> str:
|
|
245
|
+
"""Nice name of the instance"""
|
|
246
|
+
return self._configs["NICE_NAME"]
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def app(self) -> "Patera":
|
|
250
|
+
"""
|
|
251
|
+
Application instance
|
|
252
|
+
"""
|
|
253
|
+
return cast("Patera", self._app)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def schedule_job(*args, **kwargs):
|
|
257
|
+
"""
|
|
258
|
+
```
|
|
259
|
+
A decorator to add a function as a scheduled job in the given APScheduler instance.
|
|
260
|
+
The decorated function is added to a list of tuples (func, args, kwargs) and the job
|
|
261
|
+
is started when the scheduler instance is started (on_startup event of Patera)
|
|
262
|
+
IMPORTANT: The decorator should be the top-most decorator of the function to make sure
|
|
263
|
+
any other decorator is applied before the job is added to the job list
|
|
264
|
+
:param args: Positional arguments to pass to scheduler.add_job().
|
|
265
|
+
Typically, the first of these args is the trigger (e.g. 'interval', 'cron', etc.).
|
|
266
|
+
:param kwargs: Keyword arguments to pass to scheduler.add_job().
|
|
267
|
+
Example:
|
|
268
|
+
Runs a job with id 'my_job_id' every 5 minutes
|
|
269
|
+
|
|
270
|
+
@schedule_job('interval', minutes=5, id='my_job_id')
|
|
271
|
+
async def my_job(self):
|
|
272
|
+
#some task
|
|
273
|
+
```
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
def decorator(func: Callable):
|
|
277
|
+
@wraps(func)
|
|
278
|
+
async def wrapper(self, *f_args, **f_kwargs):
|
|
279
|
+
return await run_sync_or_async(func, self, *f_args, **f_kwargs)
|
|
280
|
+
|
|
281
|
+
setattr(wrapper, "_scheduler_job", {"args": args, "kwargs": kwargs})
|
|
282
|
+
return wrapper
|
|
283
|
+
|
|
284
|
+
return decorator
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
patera/taskmanager/__init__.py,sha256=BK7Zz0bvYMkW9qAmRnf1L4cIsfhnmzfb_N1MChkDqsY,166
|
|
2
|
+
patera/taskmanager/task_manager.py,sha256=VajYYftq93TNfx9z1yZBofuoMZLB_bm4YQXdHmV9Nkg,9385
|
|
3
|
+
patera_taskmanager-0.1.0.dist-info/WHEEL,sha256=mydTeHxOpFHo-DnYhAd_3ATePms-g4rrYvM7wJK8P-U,80
|
|
4
|
+
patera_taskmanager-0.1.0.dist-info/METADATA,sha256=IJ_YMhiJ_J4DsaHZJ0YSF7mhifIllbHPfr0sYkQYEW4,143
|
|
5
|
+
patera_taskmanager-0.1.0.dist-info/RECORD,,
|