plain.jobs 0.33.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of plain.jobs might be problematic. Click here for more details.
- plain/jobs/CHANGELOG.md +186 -0
- plain/jobs/README.md +253 -0
- plain/jobs/__init__.py +4 -0
- plain/jobs/admin.py +238 -0
- plain/jobs/chores.py +17 -0
- plain/jobs/cli.py +153 -0
- plain/jobs/config.py +19 -0
- plain/jobs/default_settings.py +6 -0
- plain/jobs/jobs.py +226 -0
- plain/jobs/middleware.py +20 -0
- plain/jobs/migrations/0001_initial.py +246 -0
- plain/jobs/migrations/0002_job_span_id_job_trace_id_jobrequest_span_id_and_more.py +61 -0
- plain/jobs/migrations/0003_rename_job_jobprocess_and_more.py +80 -0
- plain/jobs/migrations/0004_rename_tables_to_plainjobs.py +33 -0
- plain/jobs/migrations/0005_rename_constraints_and_indexes.py +174 -0
- plain/jobs/migrations/0006_alter_jobprocess_table_alter_jobrequest_table_and_more.py +24 -0
- plain/jobs/migrations/__init__.py +0 -0
- plain/jobs/models.py +438 -0
- plain/jobs/parameters.py +193 -0
- plain/jobs/registry.py +60 -0
- plain/jobs/scheduling.py +251 -0
- plain/jobs/templates/admin/plainqueue/jobresult_detail.html +8 -0
- plain/jobs/workers.py +322 -0
- plain_jobs-0.33.0.dist-info/METADATA +264 -0
- plain_jobs-0.33.0.dist-info/RECORD +27 -0
- plain_jobs-0.33.0.dist-info/WHEEL +4 -0
- plain_jobs-0.33.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plain.jobs
|
|
3
|
+
Version: 0.33.0
|
|
4
|
+
Summary: Process background jobs with a database-driven job queue.
|
|
5
|
+
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Requires-Dist: plain-models<1.0.0
|
|
9
|
+
Requires-Dist: plain<1.0.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# plain.jobs
|
|
13
|
+
|
|
14
|
+
**Process background jobs with a database-driven job queue.**
|
|
15
|
+
|
|
16
|
+
- [Overview](#overview)
|
|
17
|
+
- [Local development](#local-development)
|
|
18
|
+
- [Job parameters](#job-parameters)
|
|
19
|
+
- [Job methods](#job-methods)
|
|
20
|
+
- [Scheduled jobs](#scheduled-jobs)
|
|
21
|
+
- [Admin interface](#admin-interface)
|
|
22
|
+
- [Job history](#job-history)
|
|
23
|
+
- [Monitoring](#monitoring)
|
|
24
|
+
- [FAQs](#faqs)
|
|
25
|
+
- [Installation](#installation)
|
|
26
|
+
|
|
27
|
+
## Overview
|
|
28
|
+
|
|
29
|
+
Jobs are defined using the [`Job`](./jobs.py#Job) base class and the `run()` method at a minimum.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from plain.jobs import Job, register_job
|
|
33
|
+
from plain.email import send_mail
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@register_job
|
|
37
|
+
class WelcomeUserJob(Job):
|
|
38
|
+
def __init__(self, user):
|
|
39
|
+
self.user = user
|
|
40
|
+
|
|
41
|
+
def run(self):
|
|
42
|
+
send_mail(
|
|
43
|
+
subject="Welcome!",
|
|
44
|
+
message=f"Hello from Plain, {self.user}",
|
|
45
|
+
from_email="welcome@plainframework.com",
|
|
46
|
+
recipient_list=[self.user.email],
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
You can then create an instance of the job and call [`run_in_worker()`](./jobs.py#Job.run_in_worker) to enqueue it for a background worker to pick up.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
user = User.query.get(id=1)
|
|
54
|
+
WelcomeUserJob(user).run_in_worker()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Workers are run using the `plain jobs worker` command.
|
|
58
|
+
|
|
59
|
+
Jobs can be defined in any Python file, but it is suggested to use `app/jobs.py` or `app/{pkg}/jobs.py` as those will be imported automatically so the [`@register_job`](./registry.py#register_job) decorator will fire.
|
|
60
|
+
|
|
61
|
+
Run database migrations after installation:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
plain migrate
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Local development
|
|
68
|
+
|
|
69
|
+
In development, you will typically want to run the worker alongside your app. With [`plain.dev`](/plain-dev/plain/dev/README.md) you can do this by adding it to the `[tool.plain.dev.run]` section of your `pyproject.toml` file. Currently, you will need to use something like [watchfiles](https://pypi.org/project/watchfiles/) to add auto-reloading to the worker.
|
|
70
|
+
|
|
71
|
+
```toml
|
|
72
|
+
# pyproject.toml
|
|
73
|
+
[tool.plain.dev.run]
|
|
74
|
+
worker = {cmd = "watchfiles --filter python \"plain jobs worker --stats-every 0 --max-processes 2\" ."}
|
|
75
|
+
worker-slow = {cmd = "watchfiles --filter python \"plain jobs worker --queue slow --stats-every 0 --max-processes 2\" ."}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Job parameters
|
|
79
|
+
|
|
80
|
+
When calling `run_in_worker()`, you can specify several parameters to control job execution:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
job.run_in_worker(
|
|
84
|
+
queue="slow", # Target a specific queue (default: "default")
|
|
85
|
+
delay=60, # Delay in seconds (or timedelta/datetime)
|
|
86
|
+
priority=10, # Higher numbers run first (default: 0, use negatives for lower priority)
|
|
87
|
+
retries=3, # Number of retry attempts (default: 0)
|
|
88
|
+
unique_key="user-123-welcome", # Prevent duplicate jobs
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
For more advanced parameter options, see [`Job.run_in_worker()`](./jobs.py#Job.run_in_worker).
|
|
93
|
+
|
|
94
|
+
## Job methods
|
|
95
|
+
|
|
96
|
+
The [`Job`](./jobs.py#Job) base class provides several methods you can override to customize behavior:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
class MyJob(Job):
|
|
100
|
+
def run(self):
|
|
101
|
+
# Required: The main job logic
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
def get_queue(self) -> str:
|
|
105
|
+
# Specify the default queue for this job type
|
|
106
|
+
return "default"
|
|
107
|
+
|
|
108
|
+
def get_priority(self) -> int:
|
|
109
|
+
# Set the default priority
|
|
110
|
+
# Higher numbers run first: 10 > 5 > 0 > -5 > -10
|
|
111
|
+
# Use positive numbers for high priority, negative for low priority
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
def get_retries(self) -> int:
|
|
115
|
+
# Number of retry attempts on failure
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
def get_retry_delay(self, attempt: int) -> int:
|
|
119
|
+
# Delay in seconds before retry (attempt starts at 1)
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
def get_unique_key(self) -> str:
|
|
123
|
+
# Return a key to prevent duplicate jobs
|
|
124
|
+
return ""
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Scheduled jobs
|
|
128
|
+
|
|
129
|
+
You can schedule jobs to run at specific times using the [`Schedule`](./scheduling.py#Schedule) class:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from plain.jobs import Job, register_job
|
|
133
|
+
from plain.jobs.scheduling import Schedule
|
|
134
|
+
|
|
135
|
+
@register_job
|
|
136
|
+
class DailyReportJob(Job):
|
|
137
|
+
schedule = Schedule.from_cron("0 9 * * *") # Every day at 9 AM
|
|
138
|
+
|
|
139
|
+
def run(self):
|
|
140
|
+
# Generate daily report
|
|
141
|
+
pass
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The `Schedule` class supports standard cron syntax and special strings:
|
|
145
|
+
|
|
146
|
+
- `@yearly` or `@annually` - Run once a year
|
|
147
|
+
- `@monthly` - Run once a month
|
|
148
|
+
- `@weekly` - Run once a week
|
|
149
|
+
- `@daily` or `@midnight` - Run once a day
|
|
150
|
+
- `@hourly` - Run once an hour
|
|
151
|
+
|
|
152
|
+
For custom schedules, see [`Schedule`](./scheduling.py#Schedule).
|
|
153
|
+
|
|
154
|
+
## Admin interface
|
|
155
|
+
|
|
156
|
+
The jobs package includes admin views for monitoring jobs under the "Jobs" section. The admin interface provides:
|
|
157
|
+
|
|
158
|
+
- **Requests**: View pending jobs in the queue
|
|
159
|
+
- **Processes**: Monitor currently running jobs
|
|
160
|
+
- **Results**: Review completed and failed job history
|
|
161
|
+
|
|
162
|
+
Dashboard cards show at-a-glance statistics for successful, errored, lost, and retried jobs.
|
|
163
|
+
|
|
164
|
+
## Job history
|
|
165
|
+
|
|
166
|
+
Job execution history is stored in the [`JobResult`](./models.py#JobResult) model. This includes:
|
|
167
|
+
|
|
168
|
+
- Job class and parameters
|
|
169
|
+
- Start and end times
|
|
170
|
+
- Success/failure status
|
|
171
|
+
- Error messages and tracebacks for failed jobs
|
|
172
|
+
- Worker information
|
|
173
|
+
|
|
174
|
+
History retention is controlled by the `JOBS_RESULTS_RETENTION` setting (defaults to 7 days):
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
# app/settings.py
|
|
178
|
+
JOBS_RESULTS_RETENTION = 60 * 60 * 24 * 30 # 30 days (in seconds)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Job timeout can be configured with `JOBS_TIMEOUT` (defaults to 1 day):
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# app/settings.py
|
|
185
|
+
JOBS_TIMEOUT = 60 * 60 * 24 # 1 day (in seconds)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Monitoring
|
|
189
|
+
|
|
190
|
+
Workers report statistics and can be monitored using the `--stats-every` option:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Report stats every 60 seconds
|
|
194
|
+
plain jobs worker --stats-every 60
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The worker integrates with OpenTelemetry for distributed tracing. Spans are created for:
|
|
198
|
+
|
|
199
|
+
- Job scheduling (`run_in_worker`)
|
|
200
|
+
- Job execution
|
|
201
|
+
- Job completion/failure
|
|
202
|
+
|
|
203
|
+
Jobs can be linked to the originating trace context, allowing you to track jobs initiated from web requests.
|
|
204
|
+
|
|
205
|
+
## FAQs
|
|
206
|
+
|
|
207
|
+
#### How do I ensure a job only runs once?
|
|
208
|
+
|
|
209
|
+
Return a unique key from the `get_unique_key()` method:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
class ProcessUserDataJob(Job):
|
|
213
|
+
def __init__(self, user_id):
|
|
214
|
+
self.user_id = user_id
|
|
215
|
+
|
|
216
|
+
def get_unique_key(self):
|
|
217
|
+
return f"process-user-{self.user_id}"
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### Can I run multiple workers?
|
|
221
|
+
|
|
222
|
+
Yes, you can run multiple worker processes:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
plain jobs worker --max-processes 4
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Or run workers for specific queues:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
plain jobs worker --queue slow --max-processes 2
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### How do I handle job failures?
|
|
235
|
+
|
|
236
|
+
Set the number of retries and implement retry delays:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
class MyJob(Job):
|
|
240
|
+
def get_retries(self):
|
|
241
|
+
return 3
|
|
242
|
+
|
|
243
|
+
def get_retry_delay(self, attempt):
|
|
244
|
+
# Exponential backoff: 1s, 2s, 4s
|
|
245
|
+
return 2 ** (attempt - 1)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Installation
|
|
249
|
+
|
|
250
|
+
Install the `plain.jobs` package from [PyPI](https://pypi.org/project/plain.jobs/):
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
uv add plain.jobs
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Add to your `INSTALLED_PACKAGES`:
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
# app/settings.py
|
|
260
|
+
INSTALLED_PACKAGES = [
|
|
261
|
+
...
|
|
262
|
+
"plain.jobs",
|
|
263
|
+
]
|
|
264
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
plain/jobs/CHANGELOG.md,sha256=OWHFWE2xIcsfdhYTK999K06ilx6Ioc0sP4_2EzfMML8,8823
|
|
2
|
+
plain/jobs/README.md,sha256=mzaGucJn54gBG4bobjjIj0hHUAYXz-0SsQZIRd503nI,6858
|
|
3
|
+
plain/jobs/__init__.py,sha256=p2ATql3HyPzPTV34gJQ04caT7tcNQLbBGM6uIoDPbjo,92
|
|
4
|
+
plain/jobs/admin.py,sha256=IhB6nkHKHB5CJfwPEoNW4pQKUi_4ewpNGkOCo4XwO0g,6719
|
|
5
|
+
plain/jobs/chores.py,sha256=5WdLlCDPppX78yfS4LczIG7UeVR9DAoJsJHTT2Codd4,483
|
|
6
|
+
plain/jobs/cli.py,sha256=gDkyMBeKsv-vE1vnZdIsg1wv2PDSWwOYr2GHtgbQIlg,4087
|
|
7
|
+
plain/jobs/config.py,sha256=PQsl-LxWsWLnjC98f0mvtdcCOuXvXKDMjrCRf1fq44Y,550
|
|
8
|
+
plain/jobs/default_settings.py,sha256=r_95ucg_KY1XW1jarZy8VO3p-ylbllKMUrHzOPJiX6U,227
|
|
9
|
+
plain/jobs/jobs.py,sha256=IPQ2vlhfLm5gvdZTR52WINDAWRUPN0Mjc_EhKjqYhAk,7843
|
|
10
|
+
plain/jobs/middleware.py,sha256=bz8aPBY0RbtLS4kic8mzPOd3EyQFCVRQ2uTCttT3RpE,573
|
|
11
|
+
plain/jobs/models.py,sha256=EvO5vHbsTdI0OJIIJRpGEKks9pm_INB33B1q6VeMSUc,16014
|
|
12
|
+
plain/jobs/parameters.py,sha256=t9PwEZgwNCJx3YobsT-jfaVZdfMBS54XJcBrT9Wnsg0,6313
|
|
13
|
+
plain/jobs/registry.py,sha256=Rwn5Htll10e549vD2Mu0oyoDynyHhE0bGYZ2bq9uzPU,1679
|
|
14
|
+
plain/jobs/scheduling.py,sha256=4BQWeRGPYrhNjq9296GCvGw6-1-a3anjFGqc1mdK3fw,7805
|
|
15
|
+
plain/jobs/workers.py,sha256=e32UgMch2pugqwLxRWZfH_kq0PtDuxMxHwbAQ0yYMV4,11941
|
|
16
|
+
plain/jobs/migrations/0001_initial.py,sha256=EIgIEMVyTsStyx9dmKM8Jb_hwn694Yo31-74DZkNTqo,9452
|
|
17
|
+
plain/jobs/migrations/0002_job_span_id_job_trace_id_jobrequest_span_id_and_more.py,sha256=ph5BwwOAwdfjdNh9RItYmX_IA29lO-Dd9GymYzvChXQ,1953
|
|
18
|
+
plain/jobs/migrations/0003_rename_job_jobprocess_and_more.py,sha256=EdLucHxiH_QshLL2peIcMULRCQyFMPxh476AxCxW5Wk,2615
|
|
19
|
+
plain/jobs/migrations/0004_rename_tables_to_plainjobs.py,sha256=mCy4WMHI9xEXstbXI06cgSsFCMVSeQc9vOsU7ukhv4k,1061
|
|
20
|
+
plain/jobs/migrations/0005_rename_constraints_and_indexes.py,sha256=PDGpOw6__tVfn-0BAFv_5OwWt6eBo2QF2kxeTZ92JKg,6408
|
|
21
|
+
plain/jobs/migrations/0006_alter_jobprocess_table_alter_jobrequest_table_and_more.py,sha256=FY0_pcw0mL8MkUSatpDXWtA_xQw0kTZBGIyjLcmYeJE,546
|
|
22
|
+
plain/jobs/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
+
plain/jobs/templates/admin/plainqueue/jobresult_detail.html,sha256=Ybp1s_dARo_bFDcLEzEfETheP8SzqHHE_NNSKhv_eh8,198
|
|
24
|
+
plain_jobs-0.33.0.dist-info/METADATA,sha256=0GquVURZJxnR44lGpSrgYSg2LxKvXaXz7URnpSQjCCk,7185
|
|
25
|
+
plain_jobs-0.33.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
26
|
+
plain_jobs-0.33.0.dist-info/licenses/LICENSE,sha256=cvKM3OlqHx3ijD6e34zsSUkPvzl-ya3Dd63A6EHL94U,1500
|
|
27
|
+
plain_jobs-0.33.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023, Dropseed, LLC
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|