Task scheduling without infrastructure. One file. Zero deps.
Project description
pulsare
Task scheduling without infrastructure. One file. Zero deps.
pip install pulsare
Why pulsare?
| Feature | schedule | APScheduler | celery beat | pulsare |
|---|---|---|---|---|
| Cron expressions | - | + | + | + |
| Async support | - | + | - | + |
| Concurrent execution | - | + | + | + |
| Retries + backoff | - | - | + | + |
| Timeouts | - | - | - | + |
| Persistence | - | + | + | + |
| Zero dependencies | + | - | - | + |
| Single file | + | - | - | + |
Quick start
from pulsare import Scheduler, every, cron, once
s = Scheduler()
@s.job(every(30).seconds, retries=3, timeout=10)
def health_check():
ping_service()
@s.job(every().day.at("09:00"), missed="run_once")
def daily_report():
send_report()
@s.job(cron("*/5 * * * *"), max_instances=2)
def rotate_cache():
do_rotation()
s.run() # blocking loop (Ctrl+C to stop)
Fluent interval API
every(5).seconds
every(2).minutes
every(1).hours
every(3).days
every().day.at("09:00")
every().monday.at("08:30")
every().friday.at("17:00:00") # HH:MM:SS supported
Cron expressions
Standard 5-field cron: minute hour day-of-month month day-of-week
cron("*/5 * * * *") # every 5 minutes
cron("0 9 * * 1-5") # weekdays at 9am
cron("30 2 1 * *") # 1st of month at 2:30am
cron("0 0 29 2 *") # Feb 29 only (leap years)
One-shot tasks
once(after=30) # run once, 30 seconds from now
once(after=0) # run once, immediately
Concurrency
s = Scheduler(max_workers=4)
@s.job(every(10).seconds, max_instances=2)
def my_task():
slow_operation() # up to 2 running in parallel
Retries with backoff
@s.job(every(1).minutes, retries=3, retry_delay=2, retry_backoff=2)
def flaky_task():
call_external_api() # retries at 2s, 4s, 8s
Timeouts
@s.job(every(1).minutes, timeout=30, max_consecutive_timeouts=3)
def risky_task(cancel_event=None):
while not cancel_event.is_set(): # cooperative cancellation
do_chunk()
Jobs that exceed max_consecutive_timeouts are automatically paused.
Lifecycle hooks
@s.job(
every(5).minutes,
on_success=lambda name, val, dur: log(f"{name} ok in {dur:.1f}s"),
on_failure=lambda name, exc, dur: alert(f"{name} failed: {exc}"),
on_timeout=lambda name, dur: alert(f"{name} timed out"),
)
def monitored_task():
return process_data()
Metrics
m = s.get_metrics("my_task")
# {'success_count': 42, 'fail_count': 1, 'timeout_count': 0,
# 'retry_count': 3, 'total_runs': 43, 'avg_duration': 1.23, ...}
all_m = s.all_metrics() # dict of all jobs
Tags, pause, cancel
j = s.add(my_fn, every(1).minutes, tags={"api", "critical"})
s.pause("api") # pause all jobs tagged "api"
s.resume("api") # resume them
s.cancel("api") # cancel all tagged "api"
s.cancel_by_name("my_fn") # cancel by name
s.purge() # remove cancelled jobs
Persistence (SQLite)
from pulsare import Scheduler, SQLiteStore, every
store = SQLiteStore("jobs.db") # WAL mode, zero config
s = Scheduler(store=store)
@s.job(every(5).minutes)
def my_task():
do_work()
s.run() # state survives restarts
Persists: next_run, metrics, paused flag. Jobs matched by name on restart.
Async
import asyncio
from pulsare import Scheduler, every
s = Scheduler()
@s.job(every(10).seconds, timeout=5)
async def async_task():
async with aiohttp.ClientSession() as session:
await session.get("https://api.example.com")
asyncio.run(s.run_async()) # native async loop
Async jobs share a persistent event loop -- connection pools, caches, etc. work correctly.
Missed job policies
from pulsare import MissedPolicy
@s.job(every(1).hours, missed="skip") # default: reschedule to next slot
@s.job(every(1).hours, missed="run_once") # execute once, then resume
@s.job(every(1).hours, missed="run_all") # catch up all missed intervals
Timezone
from zoneinfo import ZoneInfo
s = Scheduler(tz=ZoneInfo("America/New_York"))
# All jobs use this timezone for .at() and cron expressions
System timezone detected automatically (Windows registry, /etc/localtime, $TZ).
Requirements
- Python >= 3.10
- Zero external dependencies
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pulsare-1.0.0.tar.gz.
File metadata
- Download URL: pulsare-1.0.0.tar.gz
- Upload date:
- Size: 23.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be66e3c95089656ed4f7b6fd712197a206e8694d7ec6c592740068fae4776b93
|
|
| MD5 |
9cf454a0bbccd0380d2f43b7026df14c
|
|
| BLAKE2b-256 |
3c4b50f2e8cdd447a2abefd2d76f00ca52a5893868883a80a1eb8b33a93c4f3c
|
File details
Details for the file pulsare-1.0.0-py3-none-any.whl.
File metadata
- Download URL: pulsare-1.0.0-py3-none-any.whl
- Upload date:
- Size: 24.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe1a4ead994a90967e0f682bfc98877357e81cf66a4ae7af36770b5113617575
|
|
| MD5 |
0de8f5d012e258fcee62d7a989339b78
|
|
| BLAKE2b-256 |
81875314b4f75d51c384f4550941436c7a16dcd85a6b038481af3812c20434c1
|