pyboot-execute 0.1.1__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.
- pyboot_execute-0.1.1/LICENSE +21 -0
- pyboot_execute-0.1.1/PKG-INFO +338 -0
- pyboot_execute-0.1.1/README.md +312 -0
- pyboot_execute-0.1.1/pyproject.toml +53 -0
- pyboot_execute-0.1.1/setup.cfg +4 -0
- pyboot_execute-0.1.1/src/pyboot_execute/Attr.py +12 -0
- pyboot_execute-0.1.1/src/pyboot_execute/__init__.py +17 -0
- pyboot_execute-0.1.1/src/pyboot_execute/execute.py +222 -0
- pyboot_execute-0.1.1/src/pyboot_execute.egg-info/PKG-INFO +338 -0
- pyboot_execute-0.1.1/src/pyboot_execute.egg-info/SOURCES.txt +12 -0
- pyboot_execute-0.1.1/src/pyboot_execute.egg-info/dependency_links.txt +1 -0
- pyboot_execute-0.1.1/src/pyboot_execute.egg-info/entry_points.txt +2 -0
- pyboot_execute-0.1.1/src/pyboot_execute.egg-info/requires.txt +2 -0
- pyboot_execute-0.1.1/src/pyboot_execute.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pyboot contributors
|
|
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,338 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyboot-execute
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Task scheduling and background execution plugin for PyBoot
|
|
5
|
+
Author: Siddharth Karn
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/devSiddharthKarn/pyboot
|
|
8
|
+
Project-URL: Documentation, https://github.com/devSiddharthKarn/pyboot/tree/main/plugins/pyboot_execute
|
|
9
|
+
Project-URL: Repository, https://github.com/devSiddharthKarn/pyboot
|
|
10
|
+
Project-URL: Issues, https://github.com/devSiddharthKarn/pyboot/issues
|
|
11
|
+
Keywords: pyboot,scheduler,task-scheduling,background-jobs,apscheduler
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pyboot-framework>=0.1.3
|
|
24
|
+
Requires-Dist: APScheduler<4,>=3.10
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# pyboot-execute
|
|
28
|
+
|
|
29
|
+
`pyboot-execute` is a PyBoot plugin for registering background services and
|
|
30
|
+
running recurring synchronous jobs with APScheduler's
|
|
31
|
+
`BackgroundScheduler`.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- Register background service classes with `BackgroundService`.
|
|
36
|
+
- Run synchronous methods at a fixed interval.
|
|
37
|
+
- Run a method again after a delay following the previous completion.
|
|
38
|
+
- Apply an optional initial delay to interval and fixed-delay jobs.
|
|
39
|
+
- Run methods from five-field cron expressions with `CronJob`.
|
|
40
|
+
- Resolve constructor dependencies through PyBoot's IoC container.
|
|
41
|
+
- Start the scheduler when PyBoot reaches its ready lifecycle stage.
|
|
42
|
+
- Shut down the scheduler during normal interpreter exit.
|
|
43
|
+
|
|
44
|
+
## Requirements
|
|
45
|
+
|
|
46
|
+
- Python 3.10 or newer.
|
|
47
|
+
- `pyboot-framework` 0.1.3 or newer.
|
|
48
|
+
- APScheduler 3.10 through 3.x.
|
|
49
|
+
|
|
50
|
+
The plugin uses APScheduler's in-process `BackgroundScheduler`. It is not a
|
|
51
|
+
distributed or persistent job scheduler.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
Install the published distributions:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
python -m pip install pyboot-framework pyboot-execute
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For a source checkout from the repository root:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m pip install -e pyboot
|
|
65
|
+
python -m pip install -e plugins/pyboot_execute
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The package declares the PyBoot and APScheduler dependencies. Using
|
|
69
|
+
`python -m pip` ensures they are installed into the interpreter used to run
|
|
70
|
+
the application.
|
|
71
|
+
|
|
72
|
+
Verify plugin discovery:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
python -c "from importlib.metadata import entry_points; print([e.name for e in entry_points(group='pyboot.plugins')])"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The output should include `execute`.
|
|
79
|
+
|
|
80
|
+
## Quick start
|
|
81
|
+
|
|
82
|
+
Create a background service in the package passed to `bootstrap()`:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from pyboot_execute import BackgroundService, Scheduled
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@BackgroundService()
|
|
89
|
+
class NotificationService:
|
|
90
|
+
@Scheduled(interval_ms=5_000)
|
|
91
|
+
def send_notifications(self) -> None:
|
|
92
|
+
print("Checking pending notifications")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Bootstrap PyBoot:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from pyboot.core.application import PyBoot
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
pyboot = PyBoot(app_instance=None, debug=False)
|
|
102
|
+
pyboot.bootstrap(package="app")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
When PyBoot loads the installed `execute` entry point, it discovers the
|
|
106
|
+
background service, registers it as an IoC bean, schedules its method, and
|
|
107
|
+
starts the scheduler during `on_ready`.
|
|
108
|
+
|
|
109
|
+
## Scheduling modes
|
|
110
|
+
|
|
111
|
+
### Fixed interval
|
|
112
|
+
|
|
113
|
+
`interval_ms` schedules executions at a recurring interval:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
@BackgroundService()
|
|
117
|
+
class CacheRefresher:
|
|
118
|
+
@Scheduled(interval_ms=60_000, initial_delay_ms=5_000)
|
|
119
|
+
def refresh(self) -> None:
|
|
120
|
+
print("Refreshing cache")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The first execution occurs after `initial_delay_ms`; later executions use the
|
|
124
|
+
configured interval. If the interval job takes longer than its interval,
|
|
125
|
+
APScheduler's normal job execution rules apply and executions may overlap or
|
|
126
|
+
be skipped according to the scheduler configuration.
|
|
127
|
+
|
|
128
|
+
### Fixed delay
|
|
129
|
+
|
|
130
|
+
`fixed_delay_ms` schedules the next run only after the previous invocation
|
|
131
|
+
returns:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
@BackgroundService()
|
|
135
|
+
class DataSynchronizer:
|
|
136
|
+
@Scheduled(fixed_delay_ms=30_000, initial_delay_ms=10_000)
|
|
137
|
+
def synchronize(self) -> None:
|
|
138
|
+
print("Synchronizing data")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
If the method takes 8 seconds and the delay is 30 seconds, the next run is
|
|
142
|
+
scheduled approximately 30 seconds after completion, not 30 seconds after the
|
|
143
|
+
previous start.
|
|
144
|
+
|
|
145
|
+
Exactly one of `interval_ms` and `fixed_delay_ms` must be supplied. Both must
|
|
146
|
+
be positive. `initial_delay_ms` may be omitted or set to `None`; it must not be
|
|
147
|
+
negative.
|
|
148
|
+
|
|
149
|
+
### Cron schedules
|
|
150
|
+
|
|
151
|
+
`CronJob` schedules a synchronous method using APScheduler's
|
|
152
|
+
`CronTrigger.from_crontab()`:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from pyboot_execute import BackgroundService, CronJob
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@BackgroundService()
|
|
159
|
+
class CleanupService:
|
|
160
|
+
@CronJob("0 0 * * *")
|
|
161
|
+
def remove_expired_sessions(self) -> None:
|
|
162
|
+
print("Removing expired sessions")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The expression uses five fields:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
minute hour day-of-month month day-of-week
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Examples:
|
|
172
|
+
|
|
173
|
+
| Expression | Meaning |
|
|
174
|
+
| --- | --- |
|
|
175
|
+
| `* * * * *` | Every minute |
|
|
176
|
+
| `*/5 * * * *` | Every five minutes |
|
|
177
|
+
| `0 * * * *` | At the start of every hour |
|
|
178
|
+
| `0 0 * * *` | Every day at midnight |
|
|
179
|
+
| `0 9 * * 1-5` | At 09:00 on weekdays |
|
|
180
|
+
| `0 0 1 * *` | At midnight on the first day of each month |
|
|
181
|
+
|
|
182
|
+
Cron jobs use the scheduler's configured timezone. This plugin currently
|
|
183
|
+
creates `BackgroundScheduler()` without an explicit timezone, so deployments
|
|
184
|
+
should document the host or container timezone when relying on wall-clock
|
|
185
|
+
schedules.
|
|
186
|
+
|
|
187
|
+
## Dependency injection
|
|
188
|
+
|
|
189
|
+
Background services are regular PyBoot-managed beans. Declare dependencies in
|
|
190
|
+
the constructor with type annotations:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from pyboot.core.builtins.component import Component
|
|
194
|
+
from pyboot_execute import BackgroundService, Scheduled
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@Component()
|
|
198
|
+
class ReportRepository:
|
|
199
|
+
def pending_reports(self) -> list[str]:
|
|
200
|
+
return ["daily-summary"]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@BackgroundService()
|
|
204
|
+
class ReportWorker:
|
|
205
|
+
def __init__(self, repository: ReportRepository) -> None:
|
|
206
|
+
self.repository = repository
|
|
207
|
+
|
|
208
|
+
@Scheduled(interval_ms=60_000)
|
|
209
|
+
def generate_reports(self) -> None:
|
|
210
|
+
for report in self.repository.pending_reports():
|
|
211
|
+
print(f"Generating {report}")
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Do not instantiate a `BackgroundService` manually. PyBoot creates the bean and
|
|
215
|
+
resolves its required constructor dependencies. All required constructor
|
|
216
|
+
parameters need type annotations that the IoC container can resolve.
|
|
217
|
+
|
|
218
|
+
## Public API
|
|
219
|
+
|
|
220
|
+
### `BackgroundService`
|
|
221
|
+
|
|
222
|
+
`@BackgroundService()` marks a class for registration by `ExecutePlugin`.
|
|
223
|
+
Scheduled methods are inspected on the resulting bean after PyBoot creates it.
|
|
224
|
+
|
|
225
|
+
### `Scheduled`
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
@Scheduled(
|
|
229
|
+
interval_ms: int | None = None,
|
|
230
|
+
fixed_delay_ms: int | None = None,
|
|
231
|
+
initial_delay_ms: int | None = None,
|
|
232
|
+
)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Exactly one primary schedule value is required. Invalid combinations and
|
|
236
|
+
non-positive values raise `ValueError` during decorator construction.
|
|
237
|
+
|
|
238
|
+
### `CronJob`
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
@CronJob(trigger: str)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`CronJob` schedules the decorated method using APScheduler's
|
|
245
|
+
`CronTrigger.from_crontab()`. The trigger string must be a valid five-field
|
|
246
|
+
cron expression. Invalid expressions raise an APScheduler trigger error during
|
|
247
|
+
application loading.
|
|
248
|
+
|
|
249
|
+
### `ScheduleType`
|
|
250
|
+
|
|
251
|
+
`ScheduleType.INTERVAL` and `ScheduleType.FIXED_DELAY` describe the internal
|
|
252
|
+
schedule metadata used by `Scheduled`.
|
|
253
|
+
|
|
254
|
+
### `ExecutePlugin`
|
|
255
|
+
|
|
256
|
+
`ExecutePlugin` is registered automatically through the
|
|
257
|
+
`pyboot.plugins` entry-point group:
|
|
258
|
+
|
|
259
|
+
```toml
|
|
260
|
+
[project.entry-points."pyboot.plugins"]
|
|
261
|
+
execute = "pyboot_execute.execute:ExecutePlugin"
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Most users should not instantiate it directly.
|
|
265
|
+
|
|
266
|
+
## Application layout
|
|
267
|
+
|
|
268
|
+
```text
|
|
269
|
+
service/
|
|
270
|
+
├── main.py
|
|
271
|
+
└── app/
|
|
272
|
+
├── __init__.py
|
|
273
|
+
├── components.py
|
|
274
|
+
└── jobs.py
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
PyBoot scans the package passed to `bootstrap()` and its submodules. Keep
|
|
278
|
+
background services and their dependencies inside that package, and define
|
|
279
|
+
classes in the modules being scanned.
|
|
280
|
+
|
|
281
|
+
## Lifecycle and shutdown
|
|
282
|
+
|
|
283
|
+
The plugin registers background services during `on_register`, obtains their
|
|
284
|
+
beans during `after_load`, and starts its `BackgroundScheduler` during
|
|
285
|
+
`on_ready`. It registers an `atexit` callback and calls
|
|
286
|
+
`shutdown(wait=False)` when the scheduler is running.
|
|
287
|
+
|
|
288
|
+
Normal interpreter shutdown is covered; crashes and forced termination are not.
|
|
289
|
+
Jobs are recreated from decorators on every process start. The plugin does not
|
|
290
|
+
persist job state or dynamically created jobs.
|
|
291
|
+
|
|
292
|
+
## Production guidance
|
|
293
|
+
|
|
294
|
+
### Multiple workers
|
|
295
|
+
|
|
296
|
+
Every process creates its own scheduler. If an application runs with four
|
|
297
|
+
workers, the same job can execute four times. Use a single dedicated scheduler
|
|
298
|
+
process, a distributed scheduler, or an application-level distributed lock
|
|
299
|
+
when a job must run once globally.
|
|
300
|
+
|
|
301
|
+
### Reliability
|
|
302
|
+
|
|
303
|
+
- Make jobs idempotent where possible.
|
|
304
|
+
- Use explicit timeouts for external calls.
|
|
305
|
+
- Add structured logging and metrics around each job.
|
|
306
|
+
- Catch and handle expected business failures inside the job.
|
|
307
|
+
- Keep secrets out of log messages.
|
|
308
|
+
- Consider a persistent or distributed job system for critical workloads.
|
|
309
|
+
- Test startup and shutdown behavior in the deployment environment.
|
|
310
|
+
|
|
311
|
+
### Concurrency
|
|
312
|
+
|
|
313
|
+
Scheduled methods are synchronous and run in APScheduler worker threads.
|
|
314
|
+
CPU-heavy or long-running jobs can consume worker capacity. Do not assume the
|
|
315
|
+
main application thread and scheduled workers share transaction or request
|
|
316
|
+
context.
|
|
317
|
+
|
|
318
|
+
## Testing
|
|
319
|
+
|
|
320
|
+
Test the service logic independently from scheduling, then verify registration
|
|
321
|
+
with a PyBoot bootstrap:
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
from pyboot.core.application import PyBoot
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def test_application_bootstrap() -> None:
|
|
328
|
+
pyboot = PyBoot(app_instance=None, debug=False)
|
|
329
|
+
pyboot.bootstrap(package="app")
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
For deterministic job tests, call the service method directly or use a
|
|
333
|
+
controlled scheduler test fixture. Avoid waiting on real millisecond delays in
|
|
334
|
+
unit tests.
|
|
335
|
+
|
|
336
|
+
## License
|
|
337
|
+
|
|
338
|
+
This project is distributed under the MIT License. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# pyboot-execute
|
|
2
|
+
|
|
3
|
+
`pyboot-execute` is a PyBoot plugin for registering background services and
|
|
4
|
+
running recurring synchronous jobs with APScheduler's
|
|
5
|
+
`BackgroundScheduler`.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Register background service classes with `BackgroundService`.
|
|
10
|
+
- Run synchronous methods at a fixed interval.
|
|
11
|
+
- Run a method again after a delay following the previous completion.
|
|
12
|
+
- Apply an optional initial delay to interval and fixed-delay jobs.
|
|
13
|
+
- Run methods from five-field cron expressions with `CronJob`.
|
|
14
|
+
- Resolve constructor dependencies through PyBoot's IoC container.
|
|
15
|
+
- Start the scheduler when PyBoot reaches its ready lifecycle stage.
|
|
16
|
+
- Shut down the scheduler during normal interpreter exit.
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Python 3.10 or newer.
|
|
21
|
+
- `pyboot-framework` 0.1.3 or newer.
|
|
22
|
+
- APScheduler 3.10 through 3.x.
|
|
23
|
+
|
|
24
|
+
The plugin uses APScheduler's in-process `BackgroundScheduler`. It is not a
|
|
25
|
+
distributed or persistent job scheduler.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Install the published distributions:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python -m pip install pyboot-framework pyboot-execute
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For a source checkout from the repository root:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python -m pip install -e pyboot
|
|
39
|
+
python -m pip install -e plugins/pyboot_execute
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The package declares the PyBoot and APScheduler dependencies. Using
|
|
43
|
+
`python -m pip` ensures they are installed into the interpreter used to run
|
|
44
|
+
the application.
|
|
45
|
+
|
|
46
|
+
Verify plugin discovery:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python -c "from importlib.metadata import entry_points; print([e.name for e in entry_points(group='pyboot.plugins')])"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The output should include `execute`.
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
Create a background service in the package passed to `bootstrap()`:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from pyboot_execute import BackgroundService, Scheduled
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@BackgroundService()
|
|
63
|
+
class NotificationService:
|
|
64
|
+
@Scheduled(interval_ms=5_000)
|
|
65
|
+
def send_notifications(self) -> None:
|
|
66
|
+
print("Checking pending notifications")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Bootstrap PyBoot:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from pyboot.core.application import PyBoot
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
pyboot = PyBoot(app_instance=None, debug=False)
|
|
76
|
+
pyboot.bootstrap(package="app")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
When PyBoot loads the installed `execute` entry point, it discovers the
|
|
80
|
+
background service, registers it as an IoC bean, schedules its method, and
|
|
81
|
+
starts the scheduler during `on_ready`.
|
|
82
|
+
|
|
83
|
+
## Scheduling modes
|
|
84
|
+
|
|
85
|
+
### Fixed interval
|
|
86
|
+
|
|
87
|
+
`interval_ms` schedules executions at a recurring interval:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
@BackgroundService()
|
|
91
|
+
class CacheRefresher:
|
|
92
|
+
@Scheduled(interval_ms=60_000, initial_delay_ms=5_000)
|
|
93
|
+
def refresh(self) -> None:
|
|
94
|
+
print("Refreshing cache")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The first execution occurs after `initial_delay_ms`; later executions use the
|
|
98
|
+
configured interval. If the interval job takes longer than its interval,
|
|
99
|
+
APScheduler's normal job execution rules apply and executions may overlap or
|
|
100
|
+
be skipped according to the scheduler configuration.
|
|
101
|
+
|
|
102
|
+
### Fixed delay
|
|
103
|
+
|
|
104
|
+
`fixed_delay_ms` schedules the next run only after the previous invocation
|
|
105
|
+
returns:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
@BackgroundService()
|
|
109
|
+
class DataSynchronizer:
|
|
110
|
+
@Scheduled(fixed_delay_ms=30_000, initial_delay_ms=10_000)
|
|
111
|
+
def synchronize(self) -> None:
|
|
112
|
+
print("Synchronizing data")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
If the method takes 8 seconds and the delay is 30 seconds, the next run is
|
|
116
|
+
scheduled approximately 30 seconds after completion, not 30 seconds after the
|
|
117
|
+
previous start.
|
|
118
|
+
|
|
119
|
+
Exactly one of `interval_ms` and `fixed_delay_ms` must be supplied. Both must
|
|
120
|
+
be positive. `initial_delay_ms` may be omitted or set to `None`; it must not be
|
|
121
|
+
negative.
|
|
122
|
+
|
|
123
|
+
### Cron schedules
|
|
124
|
+
|
|
125
|
+
`CronJob` schedules a synchronous method using APScheduler's
|
|
126
|
+
`CronTrigger.from_crontab()`:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from pyboot_execute import BackgroundService, CronJob
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@BackgroundService()
|
|
133
|
+
class CleanupService:
|
|
134
|
+
@CronJob("0 0 * * *")
|
|
135
|
+
def remove_expired_sessions(self) -> None:
|
|
136
|
+
print("Removing expired sessions")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The expression uses five fields:
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
minute hour day-of-month month day-of-week
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Examples:
|
|
146
|
+
|
|
147
|
+
| Expression | Meaning |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `* * * * *` | Every minute |
|
|
150
|
+
| `*/5 * * * *` | Every five minutes |
|
|
151
|
+
| `0 * * * *` | At the start of every hour |
|
|
152
|
+
| `0 0 * * *` | Every day at midnight |
|
|
153
|
+
| `0 9 * * 1-5` | At 09:00 on weekdays |
|
|
154
|
+
| `0 0 1 * *` | At midnight on the first day of each month |
|
|
155
|
+
|
|
156
|
+
Cron jobs use the scheduler's configured timezone. This plugin currently
|
|
157
|
+
creates `BackgroundScheduler()` without an explicit timezone, so deployments
|
|
158
|
+
should document the host or container timezone when relying on wall-clock
|
|
159
|
+
schedules.
|
|
160
|
+
|
|
161
|
+
## Dependency injection
|
|
162
|
+
|
|
163
|
+
Background services are regular PyBoot-managed beans. Declare dependencies in
|
|
164
|
+
the constructor with type annotations:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from pyboot.core.builtins.component import Component
|
|
168
|
+
from pyboot_execute import BackgroundService, Scheduled
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@Component()
|
|
172
|
+
class ReportRepository:
|
|
173
|
+
def pending_reports(self) -> list[str]:
|
|
174
|
+
return ["daily-summary"]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@BackgroundService()
|
|
178
|
+
class ReportWorker:
|
|
179
|
+
def __init__(self, repository: ReportRepository) -> None:
|
|
180
|
+
self.repository = repository
|
|
181
|
+
|
|
182
|
+
@Scheduled(interval_ms=60_000)
|
|
183
|
+
def generate_reports(self) -> None:
|
|
184
|
+
for report in self.repository.pending_reports():
|
|
185
|
+
print(f"Generating {report}")
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Do not instantiate a `BackgroundService` manually. PyBoot creates the bean and
|
|
189
|
+
resolves its required constructor dependencies. All required constructor
|
|
190
|
+
parameters need type annotations that the IoC container can resolve.
|
|
191
|
+
|
|
192
|
+
## Public API
|
|
193
|
+
|
|
194
|
+
### `BackgroundService`
|
|
195
|
+
|
|
196
|
+
`@BackgroundService()` marks a class for registration by `ExecutePlugin`.
|
|
197
|
+
Scheduled methods are inspected on the resulting bean after PyBoot creates it.
|
|
198
|
+
|
|
199
|
+
### `Scheduled`
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
@Scheduled(
|
|
203
|
+
interval_ms: int | None = None,
|
|
204
|
+
fixed_delay_ms: int | None = None,
|
|
205
|
+
initial_delay_ms: int | None = None,
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Exactly one primary schedule value is required. Invalid combinations and
|
|
210
|
+
non-positive values raise `ValueError` during decorator construction.
|
|
211
|
+
|
|
212
|
+
### `CronJob`
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
@CronJob(trigger: str)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`CronJob` schedules the decorated method using APScheduler's
|
|
219
|
+
`CronTrigger.from_crontab()`. The trigger string must be a valid five-field
|
|
220
|
+
cron expression. Invalid expressions raise an APScheduler trigger error during
|
|
221
|
+
application loading.
|
|
222
|
+
|
|
223
|
+
### `ScheduleType`
|
|
224
|
+
|
|
225
|
+
`ScheduleType.INTERVAL` and `ScheduleType.FIXED_DELAY` describe the internal
|
|
226
|
+
schedule metadata used by `Scheduled`.
|
|
227
|
+
|
|
228
|
+
### `ExecutePlugin`
|
|
229
|
+
|
|
230
|
+
`ExecutePlugin` is registered automatically through the
|
|
231
|
+
`pyboot.plugins` entry-point group:
|
|
232
|
+
|
|
233
|
+
```toml
|
|
234
|
+
[project.entry-points."pyboot.plugins"]
|
|
235
|
+
execute = "pyboot_execute.execute:ExecutePlugin"
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Most users should not instantiate it directly.
|
|
239
|
+
|
|
240
|
+
## Application layout
|
|
241
|
+
|
|
242
|
+
```text
|
|
243
|
+
service/
|
|
244
|
+
├── main.py
|
|
245
|
+
└── app/
|
|
246
|
+
├── __init__.py
|
|
247
|
+
├── components.py
|
|
248
|
+
└── jobs.py
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
PyBoot scans the package passed to `bootstrap()` and its submodules. Keep
|
|
252
|
+
background services and their dependencies inside that package, and define
|
|
253
|
+
classes in the modules being scanned.
|
|
254
|
+
|
|
255
|
+
## Lifecycle and shutdown
|
|
256
|
+
|
|
257
|
+
The plugin registers background services during `on_register`, obtains their
|
|
258
|
+
beans during `after_load`, and starts its `BackgroundScheduler` during
|
|
259
|
+
`on_ready`. It registers an `atexit` callback and calls
|
|
260
|
+
`shutdown(wait=False)` when the scheduler is running.
|
|
261
|
+
|
|
262
|
+
Normal interpreter shutdown is covered; crashes and forced termination are not.
|
|
263
|
+
Jobs are recreated from decorators on every process start. The plugin does not
|
|
264
|
+
persist job state or dynamically created jobs.
|
|
265
|
+
|
|
266
|
+
## Production guidance
|
|
267
|
+
|
|
268
|
+
### Multiple workers
|
|
269
|
+
|
|
270
|
+
Every process creates its own scheduler. If an application runs with four
|
|
271
|
+
workers, the same job can execute four times. Use a single dedicated scheduler
|
|
272
|
+
process, a distributed scheduler, or an application-level distributed lock
|
|
273
|
+
when a job must run once globally.
|
|
274
|
+
|
|
275
|
+
### Reliability
|
|
276
|
+
|
|
277
|
+
- Make jobs idempotent where possible.
|
|
278
|
+
- Use explicit timeouts for external calls.
|
|
279
|
+
- Add structured logging and metrics around each job.
|
|
280
|
+
- Catch and handle expected business failures inside the job.
|
|
281
|
+
- Keep secrets out of log messages.
|
|
282
|
+
- Consider a persistent or distributed job system for critical workloads.
|
|
283
|
+
- Test startup and shutdown behavior in the deployment environment.
|
|
284
|
+
|
|
285
|
+
### Concurrency
|
|
286
|
+
|
|
287
|
+
Scheduled methods are synchronous and run in APScheduler worker threads.
|
|
288
|
+
CPU-heavy or long-running jobs can consume worker capacity. Do not assume the
|
|
289
|
+
main application thread and scheduled workers share transaction or request
|
|
290
|
+
context.
|
|
291
|
+
|
|
292
|
+
## Testing
|
|
293
|
+
|
|
294
|
+
Test the service logic independently from scheduling, then verify registration
|
|
295
|
+
with a PyBoot bootstrap:
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
from pyboot.core.application import PyBoot
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_application_bootstrap() -> None:
|
|
302
|
+
pyboot = PyBoot(app_instance=None, debug=False)
|
|
303
|
+
pyboot.bootstrap(package="app")
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
For deterministic job tests, call the service method directly or use a
|
|
307
|
+
controlled scheduler test fixture. Avoid waiting on real millisecond delays in
|
|
308
|
+
unit tests.
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
This project is distributed under the MIT License. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=75", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyboot-execute"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Task scheduling and background execution plugin for PyBoot"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Siddharth Karn" }
|
|
15
|
+
]
|
|
16
|
+
keywords = [
|
|
17
|
+
"pyboot",
|
|
18
|
+
"scheduler",
|
|
19
|
+
"task-scheduling",
|
|
20
|
+
"background-jobs",
|
|
21
|
+
"apscheduler"
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 3 - Alpha",
|
|
25
|
+
"Framework :: AsyncIO",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Topic :: Software Development :: Libraries :: Python Modules"
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
dependencies = [
|
|
35
|
+
"pyboot-framework>=0.1.3",
|
|
36
|
+
"APScheduler>=3.10,<4"
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/devSiddharthKarn/pyboot"
|
|
41
|
+
Documentation = "https://github.com/devSiddharthKarn/pyboot/tree/main/plugins/pyboot_execute"
|
|
42
|
+
Repository = "https://github.com/devSiddharthKarn/pyboot"
|
|
43
|
+
Issues = "https://github.com/devSiddharthKarn/pyboot/issues"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
[project.entry-points."pyboot.plugins"]
|
|
47
|
+
execute="pyboot_execute.execute:ExecutePlugin"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools]
|
|
53
|
+
include-package-data = true
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
class BGAttr(Enum):
|
|
4
|
+
IS_BACKGROUND_SERVICE="__is_background_service__"
|
|
5
|
+
|
|
6
|
+
IS_SCHEDULED_SERVICE_EXECUTOR="__is_scheduled_service_executor__"
|
|
7
|
+
SCHEDULE_TYPE="__schedule_type__"
|
|
8
|
+
SCHEDULE_VALUE_MS="__schedule_value_ms__"
|
|
9
|
+
SCHEDULE_INITIAL_DELAY_MS="__schedule_initial_delay_ms__"
|
|
10
|
+
|
|
11
|
+
IS_CRON_JOB_EXECUTOR="__is_cron_job_executor__"
|
|
12
|
+
CRON_JOB_TRIGGER="__cron_job_trigger__"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Public API for the PyBoot background execution plugin."""
|
|
2
|
+
|
|
3
|
+
from .execute import (
|
|
4
|
+
BackgroundService,
|
|
5
|
+
CronJob,
|
|
6
|
+
ExecutePlugin,
|
|
7
|
+
ScheduleType,
|
|
8
|
+
Scheduled,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"BackgroundService",
|
|
13
|
+
"CronJob",
|
|
14
|
+
"ExecutePlugin",
|
|
15
|
+
"ScheduleType",
|
|
16
|
+
"Scheduled",
|
|
17
|
+
]
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
from pyboot.core.pyboot_context import PyBootContext
|
|
2
|
+
from pyboot.core.types import ClassT, MethodTBase
|
|
3
|
+
from pyboot.core.pyboot_plugin import PyBootPlugin
|
|
4
|
+
|
|
5
|
+
from .Attr import BGAttr
|
|
6
|
+
|
|
7
|
+
from apscheduler.schedulers.background import BackgroundScheduler # type: ignore[reportUnknownMemberType]
|
|
8
|
+
from apscheduler.triggers.cron import CronTrigger # type: ignore[reportUnknownMemberType]
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
import atexit
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from datetime import datetime,timedelta,timezone
|
|
14
|
+
|
|
15
|
+
#Decorates a class to mark it as a background service. Background services are automatically registered in the IoC container and can have scheduled methods.
|
|
16
|
+
class BackgroundService:
|
|
17
|
+
"""
|
|
18
|
+
Decorator to mark a class as a background service. Background services are automatically registered in the IoC container and can have scheduled methods.
|
|
19
|
+
Usage:
|
|
20
|
+
@BackgroundService()
|
|
21
|
+
class MyBackgroundService:
|
|
22
|
+
|
|
23
|
+
//methods
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def __call__(self, clazz: ClassT) -> ClassT:
|
|
31
|
+
setattr(clazz, BGAttr.IS_BACKGROUND_SERVICE.value, True)
|
|
32
|
+
return clazz
|
|
33
|
+
|
|
34
|
+
class ScheduleType(Enum):
|
|
35
|
+
"""
|
|
36
|
+
Enum for schedule types used in the Scheduled decorator.
|
|
37
|
+
INTERVAL: Represents a schedule that runs at fixed intervals.
|
|
38
|
+
FIXED_DELAY: Represents a schedule that runs with a fixed delay after the previous execution completes.
|
|
39
|
+
"""
|
|
40
|
+
INTERVAL="__interval_type__"
|
|
41
|
+
FIXED_DELAY="__fixed_delay_type__"
|
|
42
|
+
|
|
43
|
+
#Schedules a method to be executed at fixed intervals or with a fixed delay after the previous execution completes. It can either be an interval schedule or a fixed delay schedule, but not both.
|
|
44
|
+
class Scheduled:
|
|
45
|
+
"""
|
|
46
|
+
Decorator to mark a method as scheduled.According to the specified schedule type, the method will be executed at fixed intervals or with a fixed delay after the previous execution completes. It can either be an interval schedule or a fixed delay schedule, but not both.
|
|
47
|
+
"""
|
|
48
|
+
def __init__(
|
|
49
|
+
self, interval_ms: int | None = None, fixed_delay_ms: int | None = None,initial_delay_ms:int|None=None
|
|
50
|
+
) -> None:
|
|
51
|
+
if (interval_ms is None) == (fixed_delay_ms is None):
|
|
52
|
+
raise ValueError("Specify exactly one of interval_ms or fixed_delay_ms")
|
|
53
|
+
|
|
54
|
+
if interval_ms is not None:
|
|
55
|
+
if interval_ms<=0:
|
|
56
|
+
raise ValueError("interval_ms must be positive")
|
|
57
|
+
|
|
58
|
+
if initial_delay_ms is not None and initial_delay_ms<0:
|
|
59
|
+
raise ValueError("initial_delay_ms must be non-negative")
|
|
60
|
+
|
|
61
|
+
self.scheduled_type=ScheduleType.INTERVAL
|
|
62
|
+
self.value_ms=interval_ms
|
|
63
|
+
self.initial_delay_ms=initial_delay_ms
|
|
64
|
+
|
|
65
|
+
elif fixed_delay_ms is not None:
|
|
66
|
+
if fixed_delay_ms<=0:
|
|
67
|
+
raise ValueError("fixed_delay_ms must be positive")
|
|
68
|
+
|
|
69
|
+
if initial_delay_ms is not None and initial_delay_ms<0:
|
|
70
|
+
raise ValueError("initial_delay_ms must be non-negative")
|
|
71
|
+
|
|
72
|
+
self.scheduled_type=ScheduleType.FIXED_DELAY
|
|
73
|
+
self.value_ms=fixed_delay_ms
|
|
74
|
+
self.initial_delay_ms=initial_delay_ms
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
def __call__(self, method: MethodTBase) -> MethodTBase:
|
|
78
|
+
setattr(method, BGAttr.IS_SCHEDULED_SERVICE_EXECUTOR.value, True)
|
|
79
|
+
setattr(method,BGAttr.SCHEDULE_TYPE.value,self.scheduled_type)
|
|
80
|
+
setattr(method,BGAttr.SCHEDULE_VALUE_MS.value,self.value_ms)
|
|
81
|
+
setattr(method,BGAttr.SCHEDULE_INITIAL_DELAY_MS.value,self.initial_delay_ms)
|
|
82
|
+
return method
|
|
83
|
+
|
|
84
|
+
#CronJob decorator to mark a method as a cron job executor. The method will be executed according to the specified cron trigger. trigger should be a string in the format of a cron expression, e.g. "0 0 * * *" for every day at midnight.
|
|
85
|
+
class CronJob:
|
|
86
|
+
"""
|
|
87
|
+
Decorator to mark a method as a cron job executor. The method will be executed according to the specified cron trigger. trigger should be a string in the format of a cron expression, e.g. "0 0 * * *" for every day at midnight.
|
|
88
|
+
"""
|
|
89
|
+
def __init__(self, trigger: str) -> None:
|
|
90
|
+
self.trigger = trigger
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
def __call__(self, method: MethodTBase) -> MethodTBase:
|
|
94
|
+
setattr(method, BGAttr.IS_CRON_JOB_EXECUTOR.value, True)
|
|
95
|
+
setattr(method, BGAttr.CRON_JOB_TRIGGER.value, self.trigger)
|
|
96
|
+
return method
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
#The ExecutorPlugin class is a PyBoot plugin that manages background services and their scheduled methods. It uses the APScheduler library to schedule and execute methods based on the specified schedule type (interval, fixed delay, or cron job). The plugin automatically registers background service classes in the IoC container and starts the scheduler when the application is ready.
|
|
100
|
+
class ExecutePlugin(PyBootPlugin):
|
|
101
|
+
"""
|
|
102
|
+
The ExecutorPlugin class is a PyBoot plugin that manages background services and their scheduled methods. It uses the APScheduler library to schedule and execute methods based on the specified schedule type (interval, fixed delay, or cron job). The plugin automatically registers background service classes in the IoC container and starts the scheduler when the application is ready.
|
|
103
|
+
"""
|
|
104
|
+
def __init__(self) -> None:
|
|
105
|
+
self.simple_scheduler: BackgroundScheduler = BackgroundScheduler()
|
|
106
|
+
atexit.register(self.shutdown)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
def shutdown(self) -> None:
|
|
110
|
+
if self.simple_scheduler.running:
|
|
111
|
+
self.simple_scheduler.shutdown(wait=False) # type: ignore[reportUnknownMemberType]
|
|
112
|
+
|
|
113
|
+
def after_plugins_discovery(self, context: PyBootContext) -> None:
|
|
114
|
+
return super().after_plugins_discovery(context)
|
|
115
|
+
|
|
116
|
+
def before_classes_discovery(self, context: PyBootContext) -> None:
|
|
117
|
+
return super().before_classes_discovery(context)
|
|
118
|
+
|
|
119
|
+
def after_classes_discovery(self, context: PyBootContext) -> None:
|
|
120
|
+
return super().after_classes_discovery(context)
|
|
121
|
+
|
|
122
|
+
def before_register(self, context: PyBootContext) -> None:
|
|
123
|
+
return super().before_register(context)
|
|
124
|
+
|
|
125
|
+
def on_register(self, context: PyBootContext) -> None:
|
|
126
|
+
for clazz in context.discovered_classes:
|
|
127
|
+
|
|
128
|
+
# skip other classes
|
|
129
|
+
if not getattr(clazz, BGAttr.IS_BACKGROUND_SERVICE.value, False):
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
context.ioc.register_class(clazz=clazz)
|
|
133
|
+
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
def after_register(self, context: PyBootContext) -> None:
|
|
137
|
+
return super().after_register(context)
|
|
138
|
+
|
|
139
|
+
def before_load(self, context: PyBootContext) -> None:
|
|
140
|
+
return super().before_load(context)
|
|
141
|
+
|
|
142
|
+
def on_load(self, context: PyBootContext) -> None:
|
|
143
|
+
return super().on_load(context)
|
|
144
|
+
|
|
145
|
+
def _create_fixed_delay_job(self, method:MethodTBase, delay_ms: int):
|
|
146
|
+
def execute():
|
|
147
|
+
try:
|
|
148
|
+
method()
|
|
149
|
+
finally:
|
|
150
|
+
if self.simple_scheduler.running:
|
|
151
|
+
self.simple_scheduler.add_job(# type: ignore[reportUnknownMemberType]
|
|
152
|
+
execute,
|
|
153
|
+
trigger="date",
|
|
154
|
+
run_date=datetime.now(timezone.utc)
|
|
155
|
+
+ timedelta(milliseconds=delay_ms),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return execute
|
|
159
|
+
|
|
160
|
+
def after_load(self, context: PyBootContext) -> None:
|
|
161
|
+
for clazz in context.discovered_classes:
|
|
162
|
+
|
|
163
|
+
# skip other classes
|
|
164
|
+
if not getattr(clazz, BGAttr.IS_BACKGROUND_SERVICE.value, False):
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
bg_service_bean = context.ioc.get_bean(clazz=clazz)
|
|
168
|
+
|
|
169
|
+
for method_name, method in inspect.getmembers(
|
|
170
|
+
bg_service_bean, inspect.ismethod
|
|
171
|
+
):
|
|
172
|
+
|
|
173
|
+
# handle scheduled
|
|
174
|
+
if getattr(method, BGAttr.IS_SCHEDULED_SERVICE_EXECUTOR.value, False):
|
|
175
|
+
|
|
176
|
+
scheduled_type=getattr(method,BGAttr.SCHEDULE_TYPE.value,None)
|
|
177
|
+
|
|
178
|
+
value_ms=getattr(method,BGAttr.SCHEDULE_VALUE_MS.value,None)
|
|
179
|
+
|
|
180
|
+
if scheduled_type is None or value_ms is None:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
f"Scheduled method {method_name} in class {clazz.__name__} is missing required attributes and got values: {scheduled_type}, value_ms={value_ms}"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
initial_delay_ms=getattr(method,BGAttr.SCHEDULE_INITIAL_DELAY_MS.value,0)
|
|
186
|
+
|
|
187
|
+
if scheduled_type==ScheduleType.INTERVAL:
|
|
188
|
+
self.simple_scheduler.add_job(# type: ignore[reportUnknownMemberType]
|
|
189
|
+
func=method,
|
|
190
|
+
trigger="interval",
|
|
191
|
+
seconds=value_ms/1000,
|
|
192
|
+
next_run_time=datetime.now(timezone.utc)+timedelta(milliseconds=initial_delay_ms)
|
|
193
|
+
)
|
|
194
|
+
elif scheduled_type==ScheduleType.FIXED_DELAY:
|
|
195
|
+
job =self._create_fixed_delay_job(method=method,delay_ms=value_ms)
|
|
196
|
+
|
|
197
|
+
self.simple_scheduler.add_job(# type: ignore[reportUnknownMemberType]
|
|
198
|
+
func=job,
|
|
199
|
+
trigger="date",
|
|
200
|
+
run_date=datetime.now(timezone.utc)+timedelta(milliseconds=initial_delay_ms)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
#handle cron-jobs
|
|
204
|
+
if getattr(method, BGAttr.IS_CRON_JOB_EXECUTOR.value, False):
|
|
205
|
+
trigger_str: str = str(
|
|
206
|
+
getattr(method, BGAttr.CRON_JOB_TRIGGER.value, "")
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
trigger = CronTrigger.from_crontab(trigger_str) # type: ignore[reportUnknownMemberType]
|
|
210
|
+
|
|
211
|
+
self.simple_scheduler.add_job( # type: ignore[reportUnknownMemberType]
|
|
212
|
+
func=method, trigger=trigger
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# skip other
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
def on_ready(self, context: PyBootContext) -> None:
|
|
221
|
+
self.simple_scheduler.start() # type: ignore[reportUnknownMemberType]
|
|
222
|
+
return
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyboot-execute
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Task scheduling and background execution plugin for PyBoot
|
|
5
|
+
Author: Siddharth Karn
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/devSiddharthKarn/pyboot
|
|
8
|
+
Project-URL: Documentation, https://github.com/devSiddharthKarn/pyboot/tree/main/plugins/pyboot_execute
|
|
9
|
+
Project-URL: Repository, https://github.com/devSiddharthKarn/pyboot
|
|
10
|
+
Project-URL: Issues, https://github.com/devSiddharthKarn/pyboot/issues
|
|
11
|
+
Keywords: pyboot,scheduler,task-scheduling,background-jobs,apscheduler
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pyboot-framework>=0.1.3
|
|
24
|
+
Requires-Dist: APScheduler<4,>=3.10
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# pyboot-execute
|
|
28
|
+
|
|
29
|
+
`pyboot-execute` is a PyBoot plugin for registering background services and
|
|
30
|
+
running recurring synchronous jobs with APScheduler's
|
|
31
|
+
`BackgroundScheduler`.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- Register background service classes with `BackgroundService`.
|
|
36
|
+
- Run synchronous methods at a fixed interval.
|
|
37
|
+
- Run a method again after a delay following the previous completion.
|
|
38
|
+
- Apply an optional initial delay to interval and fixed-delay jobs.
|
|
39
|
+
- Run methods from five-field cron expressions with `CronJob`.
|
|
40
|
+
- Resolve constructor dependencies through PyBoot's IoC container.
|
|
41
|
+
- Start the scheduler when PyBoot reaches its ready lifecycle stage.
|
|
42
|
+
- Shut down the scheduler during normal interpreter exit.
|
|
43
|
+
|
|
44
|
+
## Requirements
|
|
45
|
+
|
|
46
|
+
- Python 3.10 or newer.
|
|
47
|
+
- `pyboot-framework` 0.1.3 or newer.
|
|
48
|
+
- APScheduler 3.10 through 3.x.
|
|
49
|
+
|
|
50
|
+
The plugin uses APScheduler's in-process `BackgroundScheduler`. It is not a
|
|
51
|
+
distributed or persistent job scheduler.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
Install the published distributions:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
python -m pip install pyboot-framework pyboot-execute
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For a source checkout from the repository root:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m pip install -e pyboot
|
|
65
|
+
python -m pip install -e plugins/pyboot_execute
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The package declares the PyBoot and APScheduler dependencies. Using
|
|
69
|
+
`python -m pip` ensures they are installed into the interpreter used to run
|
|
70
|
+
the application.
|
|
71
|
+
|
|
72
|
+
Verify plugin discovery:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
python -c "from importlib.metadata import entry_points; print([e.name for e in entry_points(group='pyboot.plugins')])"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The output should include `execute`.
|
|
79
|
+
|
|
80
|
+
## Quick start
|
|
81
|
+
|
|
82
|
+
Create a background service in the package passed to `bootstrap()`:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from pyboot_execute import BackgroundService, Scheduled
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@BackgroundService()
|
|
89
|
+
class NotificationService:
|
|
90
|
+
@Scheduled(interval_ms=5_000)
|
|
91
|
+
def send_notifications(self) -> None:
|
|
92
|
+
print("Checking pending notifications")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Bootstrap PyBoot:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from pyboot.core.application import PyBoot
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
pyboot = PyBoot(app_instance=None, debug=False)
|
|
102
|
+
pyboot.bootstrap(package="app")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
When PyBoot loads the installed `execute` entry point, it discovers the
|
|
106
|
+
background service, registers it as an IoC bean, schedules its method, and
|
|
107
|
+
starts the scheduler during `on_ready`.
|
|
108
|
+
|
|
109
|
+
## Scheduling modes
|
|
110
|
+
|
|
111
|
+
### Fixed interval
|
|
112
|
+
|
|
113
|
+
`interval_ms` schedules executions at a recurring interval:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
@BackgroundService()
|
|
117
|
+
class CacheRefresher:
|
|
118
|
+
@Scheduled(interval_ms=60_000, initial_delay_ms=5_000)
|
|
119
|
+
def refresh(self) -> None:
|
|
120
|
+
print("Refreshing cache")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The first execution occurs after `initial_delay_ms`; later executions use the
|
|
124
|
+
configured interval. If the interval job takes longer than its interval,
|
|
125
|
+
APScheduler's normal job execution rules apply and executions may overlap or
|
|
126
|
+
be skipped according to the scheduler configuration.
|
|
127
|
+
|
|
128
|
+
### Fixed delay
|
|
129
|
+
|
|
130
|
+
`fixed_delay_ms` schedules the next run only after the previous invocation
|
|
131
|
+
returns:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
@BackgroundService()
|
|
135
|
+
class DataSynchronizer:
|
|
136
|
+
@Scheduled(fixed_delay_ms=30_000, initial_delay_ms=10_000)
|
|
137
|
+
def synchronize(self) -> None:
|
|
138
|
+
print("Synchronizing data")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
If the method takes 8 seconds and the delay is 30 seconds, the next run is
|
|
142
|
+
scheduled approximately 30 seconds after completion, not 30 seconds after the
|
|
143
|
+
previous start.
|
|
144
|
+
|
|
145
|
+
Exactly one of `interval_ms` and `fixed_delay_ms` must be supplied. Both must
|
|
146
|
+
be positive. `initial_delay_ms` may be omitted or set to `None`; it must not be
|
|
147
|
+
negative.
|
|
148
|
+
|
|
149
|
+
### Cron schedules
|
|
150
|
+
|
|
151
|
+
`CronJob` schedules a synchronous method using APScheduler's
|
|
152
|
+
`CronTrigger.from_crontab()`:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from pyboot_execute import BackgroundService, CronJob
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@BackgroundService()
|
|
159
|
+
class CleanupService:
|
|
160
|
+
@CronJob("0 0 * * *")
|
|
161
|
+
def remove_expired_sessions(self) -> None:
|
|
162
|
+
print("Removing expired sessions")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The expression uses five fields:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
minute hour day-of-month month day-of-week
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Examples:
|
|
172
|
+
|
|
173
|
+
| Expression | Meaning |
|
|
174
|
+
| --- | --- |
|
|
175
|
+
| `* * * * *` | Every minute |
|
|
176
|
+
| `*/5 * * * *` | Every five minutes |
|
|
177
|
+
| `0 * * * *` | At the start of every hour |
|
|
178
|
+
| `0 0 * * *` | Every day at midnight |
|
|
179
|
+
| `0 9 * * 1-5` | At 09:00 on weekdays |
|
|
180
|
+
| `0 0 1 * *` | At midnight on the first day of each month |
|
|
181
|
+
|
|
182
|
+
Cron jobs use the scheduler's configured timezone. This plugin currently
|
|
183
|
+
creates `BackgroundScheduler()` without an explicit timezone, so deployments
|
|
184
|
+
should document the host or container timezone when relying on wall-clock
|
|
185
|
+
schedules.
|
|
186
|
+
|
|
187
|
+
## Dependency injection
|
|
188
|
+
|
|
189
|
+
Background services are regular PyBoot-managed beans. Declare dependencies in
|
|
190
|
+
the constructor with type annotations:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from pyboot.core.builtins.component import Component
|
|
194
|
+
from pyboot_execute import BackgroundService, Scheduled
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@Component()
|
|
198
|
+
class ReportRepository:
|
|
199
|
+
def pending_reports(self) -> list[str]:
|
|
200
|
+
return ["daily-summary"]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@BackgroundService()
|
|
204
|
+
class ReportWorker:
|
|
205
|
+
def __init__(self, repository: ReportRepository) -> None:
|
|
206
|
+
self.repository = repository
|
|
207
|
+
|
|
208
|
+
@Scheduled(interval_ms=60_000)
|
|
209
|
+
def generate_reports(self) -> None:
|
|
210
|
+
for report in self.repository.pending_reports():
|
|
211
|
+
print(f"Generating {report}")
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Do not instantiate a `BackgroundService` manually. PyBoot creates the bean and
|
|
215
|
+
resolves its required constructor dependencies. All required constructor
|
|
216
|
+
parameters need type annotations that the IoC container can resolve.
|
|
217
|
+
|
|
218
|
+
## Public API
|
|
219
|
+
|
|
220
|
+
### `BackgroundService`
|
|
221
|
+
|
|
222
|
+
`@BackgroundService()` marks a class for registration by `ExecutePlugin`.
|
|
223
|
+
Scheduled methods are inspected on the resulting bean after PyBoot creates it.
|
|
224
|
+
|
|
225
|
+
### `Scheduled`
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
@Scheduled(
|
|
229
|
+
interval_ms: int | None = None,
|
|
230
|
+
fixed_delay_ms: int | None = None,
|
|
231
|
+
initial_delay_ms: int | None = None,
|
|
232
|
+
)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Exactly one primary schedule value is required. Invalid combinations and
|
|
236
|
+
non-positive values raise `ValueError` during decorator construction.
|
|
237
|
+
|
|
238
|
+
### `CronJob`
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
@CronJob(trigger: str)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`CronJob` schedules the decorated method using APScheduler's
|
|
245
|
+
`CronTrigger.from_crontab()`. The trigger string must be a valid five-field
|
|
246
|
+
cron expression. Invalid expressions raise an APScheduler trigger error during
|
|
247
|
+
application loading.
|
|
248
|
+
|
|
249
|
+
### `ScheduleType`
|
|
250
|
+
|
|
251
|
+
`ScheduleType.INTERVAL` and `ScheduleType.FIXED_DELAY` describe the internal
|
|
252
|
+
schedule metadata used by `Scheduled`.
|
|
253
|
+
|
|
254
|
+
### `ExecutePlugin`
|
|
255
|
+
|
|
256
|
+
`ExecutePlugin` is registered automatically through the
|
|
257
|
+
`pyboot.plugins` entry-point group:
|
|
258
|
+
|
|
259
|
+
```toml
|
|
260
|
+
[project.entry-points."pyboot.plugins"]
|
|
261
|
+
execute = "pyboot_execute.execute:ExecutePlugin"
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Most users should not instantiate it directly.
|
|
265
|
+
|
|
266
|
+
## Application layout
|
|
267
|
+
|
|
268
|
+
```text
|
|
269
|
+
service/
|
|
270
|
+
├── main.py
|
|
271
|
+
└── app/
|
|
272
|
+
├── __init__.py
|
|
273
|
+
├── components.py
|
|
274
|
+
└── jobs.py
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
PyBoot scans the package passed to `bootstrap()` and its submodules. Keep
|
|
278
|
+
background services and their dependencies inside that package, and define
|
|
279
|
+
classes in the modules being scanned.
|
|
280
|
+
|
|
281
|
+
## Lifecycle and shutdown
|
|
282
|
+
|
|
283
|
+
The plugin registers background services during `on_register`, obtains their
|
|
284
|
+
beans during `after_load`, and starts its `BackgroundScheduler` during
|
|
285
|
+
`on_ready`. It registers an `atexit` callback and calls
|
|
286
|
+
`shutdown(wait=False)` when the scheduler is running.
|
|
287
|
+
|
|
288
|
+
Normal interpreter shutdown is covered; crashes and forced termination are not.
|
|
289
|
+
Jobs are recreated from decorators on every process start. The plugin does not
|
|
290
|
+
persist job state or dynamically created jobs.
|
|
291
|
+
|
|
292
|
+
## Production guidance
|
|
293
|
+
|
|
294
|
+
### Multiple workers
|
|
295
|
+
|
|
296
|
+
Every process creates its own scheduler. If an application runs with four
|
|
297
|
+
workers, the same job can execute four times. Use a single dedicated scheduler
|
|
298
|
+
process, a distributed scheduler, or an application-level distributed lock
|
|
299
|
+
when a job must run once globally.
|
|
300
|
+
|
|
301
|
+
### Reliability
|
|
302
|
+
|
|
303
|
+
- Make jobs idempotent where possible.
|
|
304
|
+
- Use explicit timeouts for external calls.
|
|
305
|
+
- Add structured logging and metrics around each job.
|
|
306
|
+
- Catch and handle expected business failures inside the job.
|
|
307
|
+
- Keep secrets out of log messages.
|
|
308
|
+
- Consider a persistent or distributed job system for critical workloads.
|
|
309
|
+
- Test startup and shutdown behavior in the deployment environment.
|
|
310
|
+
|
|
311
|
+
### Concurrency
|
|
312
|
+
|
|
313
|
+
Scheduled methods are synchronous and run in APScheduler worker threads.
|
|
314
|
+
CPU-heavy or long-running jobs can consume worker capacity. Do not assume the
|
|
315
|
+
main application thread and scheduled workers share transaction or request
|
|
316
|
+
context.
|
|
317
|
+
|
|
318
|
+
## Testing
|
|
319
|
+
|
|
320
|
+
Test the service logic independently from scheduling, then verify registration
|
|
321
|
+
with a PyBoot bootstrap:
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
from pyboot.core.application import PyBoot
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def test_application_bootstrap() -> None:
|
|
328
|
+
pyboot = PyBoot(app_instance=None, debug=False)
|
|
329
|
+
pyboot.bootstrap(package="app")
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
For deterministic job tests, call the service method directly or use a
|
|
333
|
+
controlled scheduler test fixture. Avoid waiting on real millisecond delays in
|
|
334
|
+
unit tests.
|
|
335
|
+
|
|
336
|
+
## License
|
|
337
|
+
|
|
338
|
+
This project is distributed under the MIT License. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/pyboot_execute/Attr.py
|
|
5
|
+
src/pyboot_execute/__init__.py
|
|
6
|
+
src/pyboot_execute/execute.py
|
|
7
|
+
src/pyboot_execute.egg-info/PKG-INFO
|
|
8
|
+
src/pyboot_execute.egg-info/SOURCES.txt
|
|
9
|
+
src/pyboot_execute.egg-info/dependency_links.txt
|
|
10
|
+
src/pyboot_execute.egg-info/entry_points.txt
|
|
11
|
+
src/pyboot_execute.egg-info/requires.txt
|
|
12
|
+
src/pyboot_execute.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyboot_execute
|