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.
@@ -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
@@ -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