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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ [pyboot.plugins]
2
+ execute = pyboot_execute.execute:ExecutePlugin
@@ -0,0 +1,2 @@
1
+ pyboot-framework>=0.1.3
2
+ APScheduler<4,>=3.10
@@ -0,0 +1 @@
1
+ pyboot_execute