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,7 @@
1
+ """
2
+ Scheduler sub-package
3
+ """
4
+
5
+ from .task_manager import TaskManager, schedule_job, TaskManagerConfig
6
+
7
+ __all__ = ["TaskManager", "schedule_job", "TaskManagerConfig"]
@@ -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,6 @@
1
+ Metadata-Version: 2.3
2
+ Name: patera-taskmanager
3
+ Version: 0.1.0
4
+ Requires-Dist: apscheduler>=3.11.2
5
+ Requires-Dist: patera
6
+ Requires-Python: >=3.12
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.10.9
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any