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.

@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.