pulsare 1.0.0__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.
- pulsare-1.0.0/.gitignore +23 -0
- pulsare-1.0.0/CHANGELOG.md +30 -0
- pulsare-1.0.0/LICENSE +21 -0
- pulsare-1.0.0/PKG-INFO +228 -0
- pulsare-1.0.0/README.md +201 -0
- pulsare-1.0.0/pulsare.py +1653 -0
- pulsare-1.0.0/pyproject.toml +50 -0
- pulsare-1.0.0/tests/__init__.py +0 -0
- pulsare-1.0.0/tests/test_pulsare.py +585 -0
pulsare-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.so
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.eggs/
|
|
9
|
+
*.egg
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.benchmarks/
|
|
12
|
+
*.db
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.mypy_cache/
|
|
15
|
+
.venv/
|
|
16
|
+
venv/
|
|
17
|
+
env/
|
|
18
|
+
.env
|
|
19
|
+
*.swp
|
|
20
|
+
*.swo
|
|
21
|
+
*~
|
|
22
|
+
.DS_Store
|
|
23
|
+
Thumbs.db
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-04-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Fluent interval API: `every(5).seconds`, `every().monday.at("09:00")`
|
|
12
|
+
- Cron expressions (5-field standard)
|
|
13
|
+
- One-shot delayed tasks: `once(after=30)`
|
|
14
|
+
- Concurrent execution with configurable thread pool
|
|
15
|
+
- Per-job max concurrent instances
|
|
16
|
+
- Retry with exponential backoff
|
|
17
|
+
- Execution timeout with cooperative cancellation via `cancel_event`
|
|
18
|
+
- Circuit breaker: `max_consecutive_timeouts` auto-pauses hung jobs
|
|
19
|
+
- Missed job policies: skip, run_once, run_all
|
|
20
|
+
- Lifecycle hooks: on_success, on_failure, on_timeout
|
|
21
|
+
- Built-in metrics (duration, success/fail counts, last_error)
|
|
22
|
+
- Job tagging with pause/resume/cancel by tag
|
|
23
|
+
- Timezone-aware scheduling (system tz auto-detected)
|
|
24
|
+
- Async-native scheduler loop (`run_async()`)
|
|
25
|
+
- Persistent event loop for async jobs in sync context
|
|
26
|
+
- Pluggable persistence via `JobStore` protocol
|
|
27
|
+
- Built-in `SQLiteStore` (WAL mode, UPSERT, zero deps)
|
|
28
|
+
- Anchor-based interval reschedule (prevents cumulative drift)
|
|
29
|
+
- Job name uniqueness enforcement
|
|
30
|
+
- Context manager support
|
pulsare-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Raul
|
|
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.
|
pulsare-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pulsare
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Task scheduling without infrastructure. One file. Zero deps.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ElEscribanoSilente/pulsare
|
|
6
|
+
Project-URL: Repository, https://github.com/ElEscribanoSilente/pulsare
|
|
7
|
+
Project-URL: Issues, https://github.com/ElEscribanoSilente/pulsare/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/ElEscribanoSilente/pulsare/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Raul <msc.framework@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: async,cron,scheduler,task,threading
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Classifier: Topic :: System :: Monitoring
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# pulsare
|
|
29
|
+
|
|
30
|
+
Task scheduling without infrastructure. One file. Zero deps.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
pip install pulsare
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Why pulsare?
|
|
37
|
+
|
|
38
|
+
| Feature | schedule | APScheduler | celery beat | **pulsare** |
|
|
39
|
+
|---|---|---|---|---|
|
|
40
|
+
| Cron expressions | - | + | + | + |
|
|
41
|
+
| Async support | - | + | - | + |
|
|
42
|
+
| Concurrent execution | - | + | + | + |
|
|
43
|
+
| Retries + backoff | - | - | + | + |
|
|
44
|
+
| Timeouts | - | - | - | + |
|
|
45
|
+
| Persistence | - | + | + | + |
|
|
46
|
+
| Zero dependencies | + | - | - | **+** |
|
|
47
|
+
| Single file | + | - | - | **+** |
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from pulsare import Scheduler, every, cron, once
|
|
53
|
+
|
|
54
|
+
s = Scheduler()
|
|
55
|
+
|
|
56
|
+
@s.job(every(30).seconds, retries=3, timeout=10)
|
|
57
|
+
def health_check():
|
|
58
|
+
ping_service()
|
|
59
|
+
|
|
60
|
+
@s.job(every().day.at("09:00"), missed="run_once")
|
|
61
|
+
def daily_report():
|
|
62
|
+
send_report()
|
|
63
|
+
|
|
64
|
+
@s.job(cron("*/5 * * * *"), max_instances=2)
|
|
65
|
+
def rotate_cache():
|
|
66
|
+
do_rotation()
|
|
67
|
+
|
|
68
|
+
s.run() # blocking loop (Ctrl+C to stop)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Fluent interval API
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
every(5).seconds
|
|
75
|
+
every(2).minutes
|
|
76
|
+
every(1).hours
|
|
77
|
+
every(3).days
|
|
78
|
+
every().day.at("09:00")
|
|
79
|
+
every().monday.at("08:30")
|
|
80
|
+
every().friday.at("17:00:00") # HH:MM:SS supported
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Cron expressions
|
|
84
|
+
|
|
85
|
+
Standard 5-field cron: `minute hour day-of-month month day-of-week`
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
cron("*/5 * * * *") # every 5 minutes
|
|
89
|
+
cron("0 9 * * 1-5") # weekdays at 9am
|
|
90
|
+
cron("30 2 1 * *") # 1st of month at 2:30am
|
|
91
|
+
cron("0 0 29 2 *") # Feb 29 only (leap years)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## One-shot tasks
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
once(after=30) # run once, 30 seconds from now
|
|
98
|
+
once(after=0) # run once, immediately
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Concurrency
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
s = Scheduler(max_workers=4)
|
|
105
|
+
|
|
106
|
+
@s.job(every(10).seconds, max_instances=2)
|
|
107
|
+
def my_task():
|
|
108
|
+
slow_operation() # up to 2 running in parallel
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Retries with backoff
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
@s.job(every(1).minutes, retries=3, retry_delay=2, retry_backoff=2)
|
|
115
|
+
def flaky_task():
|
|
116
|
+
call_external_api() # retries at 2s, 4s, 8s
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Timeouts
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
@s.job(every(1).minutes, timeout=30, max_consecutive_timeouts=3)
|
|
123
|
+
def risky_task(cancel_event=None):
|
|
124
|
+
while not cancel_event.is_set(): # cooperative cancellation
|
|
125
|
+
do_chunk()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Jobs that exceed `max_consecutive_timeouts` are automatically paused.
|
|
129
|
+
|
|
130
|
+
## Lifecycle hooks
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
@s.job(
|
|
134
|
+
every(5).minutes,
|
|
135
|
+
on_success=lambda name, val, dur: log(f"{name} ok in {dur:.1f}s"),
|
|
136
|
+
on_failure=lambda name, exc, dur: alert(f"{name} failed: {exc}"),
|
|
137
|
+
on_timeout=lambda name, dur: alert(f"{name} timed out"),
|
|
138
|
+
)
|
|
139
|
+
def monitored_task():
|
|
140
|
+
return process_data()
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Metrics
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
m = s.get_metrics("my_task")
|
|
147
|
+
# {'success_count': 42, 'fail_count': 1, 'timeout_count': 0,
|
|
148
|
+
# 'retry_count': 3, 'total_runs': 43, 'avg_duration': 1.23, ...}
|
|
149
|
+
|
|
150
|
+
all_m = s.all_metrics() # dict of all jobs
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Tags, pause, cancel
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
j = s.add(my_fn, every(1).minutes, tags={"api", "critical"})
|
|
157
|
+
|
|
158
|
+
s.pause("api") # pause all jobs tagged "api"
|
|
159
|
+
s.resume("api") # resume them
|
|
160
|
+
s.cancel("api") # cancel all tagged "api"
|
|
161
|
+
s.cancel_by_name("my_fn") # cancel by name
|
|
162
|
+
s.purge() # remove cancelled jobs
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Persistence (SQLite)
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from pulsare import Scheduler, SQLiteStore, every
|
|
169
|
+
|
|
170
|
+
store = SQLiteStore("jobs.db") # WAL mode, zero config
|
|
171
|
+
s = Scheduler(store=store)
|
|
172
|
+
|
|
173
|
+
@s.job(every(5).minutes)
|
|
174
|
+
def my_task():
|
|
175
|
+
do_work()
|
|
176
|
+
|
|
177
|
+
s.run() # state survives restarts
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Persists: next_run, metrics, paused flag. Jobs matched by name on restart.
|
|
181
|
+
|
|
182
|
+
## Async
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
import asyncio
|
|
186
|
+
from pulsare import Scheduler, every
|
|
187
|
+
|
|
188
|
+
s = Scheduler()
|
|
189
|
+
|
|
190
|
+
@s.job(every(10).seconds, timeout=5)
|
|
191
|
+
async def async_task():
|
|
192
|
+
async with aiohttp.ClientSession() as session:
|
|
193
|
+
await session.get("https://api.example.com")
|
|
194
|
+
|
|
195
|
+
asyncio.run(s.run_async()) # native async loop
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Async jobs share a persistent event loop -- connection pools, caches, etc. work correctly.
|
|
199
|
+
|
|
200
|
+
## Missed job policies
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from pulsare import MissedPolicy
|
|
204
|
+
|
|
205
|
+
@s.job(every(1).hours, missed="skip") # default: reschedule to next slot
|
|
206
|
+
@s.job(every(1).hours, missed="run_once") # execute once, then resume
|
|
207
|
+
@s.job(every(1).hours, missed="run_all") # catch up all missed intervals
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Timezone
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from zoneinfo import ZoneInfo
|
|
214
|
+
|
|
215
|
+
s = Scheduler(tz=ZoneInfo("America/New_York"))
|
|
216
|
+
# All jobs use this timezone for .at() and cron expressions
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
System timezone detected automatically (Windows registry, /etc/localtime, $TZ).
|
|
220
|
+
|
|
221
|
+
## Requirements
|
|
222
|
+
|
|
223
|
+
- Python >= 3.10
|
|
224
|
+
- Zero external dependencies
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT
|
pulsare-1.0.0/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# pulsare
|
|
2
|
+
|
|
3
|
+
Task scheduling without infrastructure. One file. Zero deps.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
pip install pulsare
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Why pulsare?
|
|
10
|
+
|
|
11
|
+
| Feature | schedule | APScheduler | celery beat | **pulsare** |
|
|
12
|
+
|---|---|---|---|---|
|
|
13
|
+
| Cron expressions | - | + | + | + |
|
|
14
|
+
| Async support | - | + | - | + |
|
|
15
|
+
| Concurrent execution | - | + | + | + |
|
|
16
|
+
| Retries + backoff | - | - | + | + |
|
|
17
|
+
| Timeouts | - | - | - | + |
|
|
18
|
+
| Persistence | - | + | + | + |
|
|
19
|
+
| Zero dependencies | + | - | - | **+** |
|
|
20
|
+
| Single file | + | - | - | **+** |
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from pulsare import Scheduler, every, cron, once
|
|
26
|
+
|
|
27
|
+
s = Scheduler()
|
|
28
|
+
|
|
29
|
+
@s.job(every(30).seconds, retries=3, timeout=10)
|
|
30
|
+
def health_check():
|
|
31
|
+
ping_service()
|
|
32
|
+
|
|
33
|
+
@s.job(every().day.at("09:00"), missed="run_once")
|
|
34
|
+
def daily_report():
|
|
35
|
+
send_report()
|
|
36
|
+
|
|
37
|
+
@s.job(cron("*/5 * * * *"), max_instances=2)
|
|
38
|
+
def rotate_cache():
|
|
39
|
+
do_rotation()
|
|
40
|
+
|
|
41
|
+
s.run() # blocking loop (Ctrl+C to stop)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Fluent interval API
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
every(5).seconds
|
|
48
|
+
every(2).minutes
|
|
49
|
+
every(1).hours
|
|
50
|
+
every(3).days
|
|
51
|
+
every().day.at("09:00")
|
|
52
|
+
every().monday.at("08:30")
|
|
53
|
+
every().friday.at("17:00:00") # HH:MM:SS supported
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Cron expressions
|
|
57
|
+
|
|
58
|
+
Standard 5-field cron: `minute hour day-of-month month day-of-week`
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
cron("*/5 * * * *") # every 5 minutes
|
|
62
|
+
cron("0 9 * * 1-5") # weekdays at 9am
|
|
63
|
+
cron("30 2 1 * *") # 1st of month at 2:30am
|
|
64
|
+
cron("0 0 29 2 *") # Feb 29 only (leap years)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## One-shot tasks
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
once(after=30) # run once, 30 seconds from now
|
|
71
|
+
once(after=0) # run once, immediately
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Concurrency
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
s = Scheduler(max_workers=4)
|
|
78
|
+
|
|
79
|
+
@s.job(every(10).seconds, max_instances=2)
|
|
80
|
+
def my_task():
|
|
81
|
+
slow_operation() # up to 2 running in parallel
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Retries with backoff
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
@s.job(every(1).minutes, retries=3, retry_delay=2, retry_backoff=2)
|
|
88
|
+
def flaky_task():
|
|
89
|
+
call_external_api() # retries at 2s, 4s, 8s
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Timeouts
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
@s.job(every(1).minutes, timeout=30, max_consecutive_timeouts=3)
|
|
96
|
+
def risky_task(cancel_event=None):
|
|
97
|
+
while not cancel_event.is_set(): # cooperative cancellation
|
|
98
|
+
do_chunk()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Jobs that exceed `max_consecutive_timeouts` are automatically paused.
|
|
102
|
+
|
|
103
|
+
## Lifecycle hooks
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
@s.job(
|
|
107
|
+
every(5).minutes,
|
|
108
|
+
on_success=lambda name, val, dur: log(f"{name} ok in {dur:.1f}s"),
|
|
109
|
+
on_failure=lambda name, exc, dur: alert(f"{name} failed: {exc}"),
|
|
110
|
+
on_timeout=lambda name, dur: alert(f"{name} timed out"),
|
|
111
|
+
)
|
|
112
|
+
def monitored_task():
|
|
113
|
+
return process_data()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Metrics
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
m = s.get_metrics("my_task")
|
|
120
|
+
# {'success_count': 42, 'fail_count': 1, 'timeout_count': 0,
|
|
121
|
+
# 'retry_count': 3, 'total_runs': 43, 'avg_duration': 1.23, ...}
|
|
122
|
+
|
|
123
|
+
all_m = s.all_metrics() # dict of all jobs
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Tags, pause, cancel
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
j = s.add(my_fn, every(1).minutes, tags={"api", "critical"})
|
|
130
|
+
|
|
131
|
+
s.pause("api") # pause all jobs tagged "api"
|
|
132
|
+
s.resume("api") # resume them
|
|
133
|
+
s.cancel("api") # cancel all tagged "api"
|
|
134
|
+
s.cancel_by_name("my_fn") # cancel by name
|
|
135
|
+
s.purge() # remove cancelled jobs
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Persistence (SQLite)
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from pulsare import Scheduler, SQLiteStore, every
|
|
142
|
+
|
|
143
|
+
store = SQLiteStore("jobs.db") # WAL mode, zero config
|
|
144
|
+
s = Scheduler(store=store)
|
|
145
|
+
|
|
146
|
+
@s.job(every(5).minutes)
|
|
147
|
+
def my_task():
|
|
148
|
+
do_work()
|
|
149
|
+
|
|
150
|
+
s.run() # state survives restarts
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Persists: next_run, metrics, paused flag. Jobs matched by name on restart.
|
|
154
|
+
|
|
155
|
+
## Async
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
import asyncio
|
|
159
|
+
from pulsare import Scheduler, every
|
|
160
|
+
|
|
161
|
+
s = Scheduler()
|
|
162
|
+
|
|
163
|
+
@s.job(every(10).seconds, timeout=5)
|
|
164
|
+
async def async_task():
|
|
165
|
+
async with aiohttp.ClientSession() as session:
|
|
166
|
+
await session.get("https://api.example.com")
|
|
167
|
+
|
|
168
|
+
asyncio.run(s.run_async()) # native async loop
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Async jobs share a persistent event loop -- connection pools, caches, etc. work correctly.
|
|
172
|
+
|
|
173
|
+
## Missed job policies
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from pulsare import MissedPolicy
|
|
177
|
+
|
|
178
|
+
@s.job(every(1).hours, missed="skip") # default: reschedule to next slot
|
|
179
|
+
@s.job(every(1).hours, missed="run_once") # execute once, then resume
|
|
180
|
+
@s.job(every(1).hours, missed="run_all") # catch up all missed intervals
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Timezone
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from zoneinfo import ZoneInfo
|
|
187
|
+
|
|
188
|
+
s = Scheduler(tz=ZoneInfo("America/New_York"))
|
|
189
|
+
# All jobs use this timezone for .at() and cron expressions
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
System timezone detected automatically (Windows registry, /etc/localtime, $TZ).
|
|
193
|
+
|
|
194
|
+
## Requirements
|
|
195
|
+
|
|
196
|
+
- Python >= 3.10
|
|
197
|
+
- Zero external dependencies
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|