Metadata-Version: 2.4
Name: jobrail
Version: 0.3.0
Summary: A lightweight, observable job runner with a built-in web API
License: AGPL-3.0-only
Project-URL: Homepage, https://github.com/Paracosm-Lab/jobrail
Project-URL: Repository, https://github.com/Paracosm-Lab/jobrail
Project-URL: Issues, https://github.com/Paracosm-Lab/jobrail/issues
Project-URL: Changelog, https://github.com/Paracosm-Lab/jobrail/blob/main/CHANGELOG.md
Keywords: jobs,scheduler,cron,runner,monitoring
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Monitoring
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: tomli-w>=1.0.0
Requires-Dist: fastapi>=0.115.0
Requires-Dist: uvicorn>=0.34.0
Requires-Dist: pydantic>=2.10.0
Requires-Dist: pydantic-settings>=2.0.0
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: httpx>=0.28.0
Requires-Dist: apscheduler>=3.10.4
Requires-Dist: prometheus-client>=0.21.0
Requires-Dist: prometheus-fastapi-instrumentator>=7.0.0
Requires-Dist: jinja2>=3.1.0
Requires-Dist: itsdangerous>=2.1.0
Requires-Dist: python-json-logger>=3.0.0
Requires-Dist: click>=8.1.0
Requires-Dist: rich>=13.0.0
Requires-Dist: alembic>=1.13.0
Requires-Dist: sqlalchemy>=2.0.0
Requires-Dist: cryptography>=42.0.0
Provides-Extra: sentry
Requires-Dist: sentry-sdk[fastapi]>=2.0.0; extra == "sentry"
Provides-Extra: db
Requires-Dist: asyncpg>=0.30.0; extra == "db"
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.20.0; extra == "otel"
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == "otel"
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0; extra == "otel"
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.41b0; extra == "otel"
Requires-Dist: opentelemetry-instrumentation-httpx>=0.41b0; extra == "otel"
Provides-Extra: tui
Requires-Dist: textual>=0.60.0; extra == "tui"
Provides-Extra: dev
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
Requires-Dist: markdown>=3.5; extra == "dev"
Requires-Dist: ruff>=0.6.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Provides-Extra: aws
Requires-Dist: boto3>=1.26.0; extra == "aws"
Provides-Extra: docker
Requires-Dist: aiodocker>=0.21.0; extra == "docker"
Dynamic: license-file

# Jobrail

Cron jobs with visibility. Know what ran, what failed, and why.

> Ofelia with memory. Sidekiq without Redis. Kubernetes CronJobs without Kubernetes.

---

## Documentation

Full documentation is available at [paracosm-lab.github.io/jobrail](https://paracosm-lab.github.io/jobrail/) and in-app at [localhost:8010/docs](http://localhost:8010/docs).

## The problem

Every production system accumulates scheduled work: cert checks, backup verification, health sweeps, cleanup tasks, cost reports. This work isn't your application — it's the operational plumbing that keeps everything else running.

Your options today:

- **Ofelia / cron** — fire and forget. No history, no API, no metrics. When a job fails silently at 3 AM, you find out from your users.
- **Celery / Sidekiq** — built for distributed task queues, not a weekly cert check. You're standing up Redis, workers, and a monitoring layer for something that should be simple.
- **Kubernetes CronJobs** — requires Kubernetes. State lives in pod logs. "Did my backup job actually succeed last Tuesday?" is a kubectl archaeology project.
- **Shell scripts on cron** — works until it doesn't. No concurrency protection, no structured output, debugging means SSH and grep.

None of these treat observability as a first-class feature.

## What Jobrail does differently

**Every job execution is persisted.** Jobrail writes a record to SQLite the moment a job starts running, and updates it when it finishes. Runs survive restarts. If the process dies mid-job, you'll see a `status: running` row with no `finished_at` — that's a signal, not silence.

**The API makes everything programmable.** List jobs, trigger a run, inspect history — all over REST. Your CI pipeline can kick off a backup verification. Your deploy script can check that health sweeps are passing. No SSH required.

**Prometheus metrics are built in.** `jobrail_job_runs_total`, `jobrail_job_duration_seconds`, `jobrail_job_last_success_timestamp` — exposed at `/metrics`, scraped by your existing monitoring stack, visualized in Grafana. A sample dashboard ships in the repo.

**Writing a job is just Python.** No framework magic. Define an `async def run()` function, return a `JobResult`, register it in one dict. That's the entire contract.

```python
from jobs.base import JobResult

async def run() -> JobResult:
    # your logic here
    return JobResult(success=True, summary="all 5 endpoints healthy")
```

## Quick start

### With Docker Compose

```bash
git clone https://github.com/Paracosm-Lab/jobrail.git
cd jobrail
cp .env.example .env
docker compose up -d
```

Jobrail is running at `http://localhost:8010`. The two example jobs (hello-world and http-health-check) are already scheduled.

### Verify it works

```bash
# List all registered jobs
curl http://localhost:8010/jobs | jq

# Trigger a job manually
curl -X POST http://localhost:8010/jobs/hello-world/trigger \
  -H "Authorization: Bearer dev-token-change-me"

# Check run history
curl http://localhost:8010/jobs/hello-world/runs | jq

# View Prometheus metrics
curl http://localhost:8010/metrics | grep jobrail_
```

### With Kamal

A deploy template is included at `config/deploy.yml`. Update the server IP, registry, and secrets, then:

```bash
kamal setup
```

## Writing your first job

See the [Writing Jobs](docs/user/writing-jobs.md) guide for the full walkthrough.

## API

| Endpoint | Method | Auth | Description |
|---|---|---|---|
| `/health` | GET | No | Liveness check |
| `/metrics` | GET | No | Prometheus metrics |
| `/jobs` | GET | No | List all jobs with status and next run time |
| `/jobs/runs` | GET | No | Cross-job run history |
| `/jobs/{name}/trigger` | POST | Token | Trigger a job immediately |
| `/jobs/{name}/runs` | GET | No | Run history for a specific job |
| `/jobs/{name}/runs/{id}` | GET | No | Single run details |
| `/admin/info` | GET | Token | Service info, uptime, scheduler state |
| `/admin/maintenance` | POST | Token | Pause/resume all scheduled jobs |

Auth endpoints require `Authorization: Bearer <SERVICE_TOKEN>` header.

## Configuration

See the [Configuration Reference](docs/user/configuration.md).

## Monitoring

See the [Monitoring guide](docs/user/monitoring.md) for Prometheus metrics and Grafana setup.

## How it compares

| | Jobrail | Ofelia | Celery beat | k8s CronJob |
|---|---|---|---|---|
| Persistent run history | Yes | No | Partial | No |
| REST API | Yes | No | No | No |
| Prometheus metrics | Yes | No | Partial | No |
| Manual trigger | Yes | No | Yes | Yes |
| Concurrency protection | Yes | Yes | Yes | Partial |
| External dependencies | None | None | Redis | Kubernetes |
| Grafana dashboard included | Yes | No | No | No |

## Where it fits

Jobrail is not a replacement for your application's background job system. Sidekiq processes your user's uploads. Celery runs your ML pipeline. Those belong in your application.

Jobrail handles the operational layer alongside it — the cert checks, health sweeps, backup verification, and cleanup tasks that don't belong inside any one service. It's the scheduled ops work that every team does, and that most teams have cobbled together from cron entries, shell scripts, and hope.

## Example jobs

See the [Example Job Catalog](docs/developer/examples.md).

## License

MIT
