Skip to main content

Cronos — Serverless Multi-Source ETL Pipeline

Cronos ETL Pipeline CI Python License: MIT Lint: ruff

Fully automated, serverless, zero-cost ETL. GitHub Actions runs daily at 00:00 UTC, scrapes every enabled source, validates and deduplicates the results, regenerates statistics and a dashboard, and commits everything back to the repository (GitOps). No servers, no database, no bills.

Live dashboard: https://farajzada.github.io/cronos/

Why Cronos?

  • Pluggable sources — add a new site or API by writing one small class (guide below). Ships with two reference implementations: HTML scraping (quotes) and an official JSON API (hackernews).
  • Idempotent by construction — every source declares a stable dedup key (natural id or content hash); loads are append-only with O(1) set() membership checks. Run it 1× or 100×, the dataset is the same.
  • Corruption cannot ship — an integrity validator (schema, key uniqueness, per-source rules like content-hash verification) gates the GitOps commit.
  • Deterministic outputs — stats and dashboard are derived purely from data (no timestamps), so unchanged data means byte-identical files and a silent no-op run.
  • A real frontend for free — self-contained dashboard (tabs per source, SVG charts, facet filters, live search, light/dark theme, CSV/JSON export) served by GitHub Pages, regenerated on every data change.

Architecture

┌────────────────┐   cron: 0 0 * * *   ┌──────────────────────────────┐
│ GitHub Actions │────────────────────▶│ ubuntu-latest + Python 3.11  │
│   (scheduler)  │                     │  pip cache (actions/cache)   │
└────────────────┘                     └──────────────┬───────────────┘
                                                      │
        ┌─────────────────────────────────────────────▼─────────────┐
        │ cronos run       Source registry → scrape each source     │
        │                  → append-only dedup load (data/<src>.csv)│
        │ cronos validate  schema + key uniqueness + source rules   │
        │ cronos stats     data/stats.json + Actions job summary    │
        │ cronos report    docs/index.html (self-contained SPA)     │
        └─────────────────────────────────────────────┬─────────────┘
                                                      │ git status --porcelain
                                      ┌───────────────▼───────────────┐
                                      │ changed? → bot commit + push  │
                                      │          → Pages redeploys    │
                                      │ unchanged? → graceful exit 0  │
                                      └───────────────────────────────┘

Bundled sources:

Source Type Dataset Dedup key
quotes HTML pages data/quotes.csv SHA-256 of text::author
hackernews JSON API data/hackernews.csv HN item id (first-seen snapshot)

Quick start

Use as a template (recommended): click Use this template on GitHub to get your own data-collecting repo in one click, then adapt the sources.

Install from PyPI:

pip install cronos-pipeline
cronos all        # run → validate → stats → report (in your working dir)

Develop from source:

git clone https://github.com/farajzada/cronos.git && cd cronos
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"

cronos all        # run → validate → stats → report
cronos sources    # list available/enabled sources
python -m http.server -d docs 8000   # view the dashboard locally

Every stage is also runnable as a module (python -m cronos.scraper, etc.) — that is exactly what the GitHub Actions workflow does.

The second consecutive cronos run appends 0 rows — that is the idempotency guarantee working, not a bug.

Configuration

Everything is environment-variable driven (see cronos/config.py):

Variable Default Purpose
CRONOS_SOURCES quotes,hackernews Enabled sources (comma-sep)
CRONOS_DATA_DIR data/ Dataset directory
CRONOS_QUOTES_URL https://quotes.toscrape.com/ Quotes target
CRONOS_HN_URL Algolia front-page endpoint Hacker News API
CRONOS_MAX_PAGES 50 Pagination ceiling
CRONOS_CONNECT_TIMEOUT 5.0 Connect timeout (s)
CRONOS_READ_TIMEOUT 20.0 Read timeout (s)
CRONOS_MAX_RETRIES 3 Attempts per request
CRONOS_RETRY_BACKOFF 2.0 Backoff base (s, linear)
CRONOS_POLITENESS_DELAY 0.5 Delay between pages (s)

Adding your own source

The pipeline is generic over the Source contract — scraper, validator, metrics and dashboard all adapt automatically:

# cronos/sources/mysite.py
from cronos.sources.base import Source

class MySiteSource(Source):
    name = "mysite"                      # → data/mysite.csv
    title = "My Site"                    # dashboard tab label
    fieldnames = ["post_id", "title", "url", "category"]
    key_field = "post_id"                # stable dedup key
    display_columns = [("title", "Title"), ("category", "Category")]
    stat_fields = [("category", None)]   # aggregated in stats.json
    facet_field = "category"             # filter chips + bar chart

    def scrape(self, client):
        payload = client.get_json("https://mysite.example/api/posts")
        for post in payload["posts"]:
            yield {
                "post_id": str(post["id"]),
                "title": post["title"],
                "url": post["url"],
                "category": post["category"],
            }

Then register it in cronos/sources/__init__.py and enable it: CRONOS_SOURCES=quotes,hackernews,mysite. Full checklist in CONTRIBUTING.md.

Respect the target's robots.txt and terms of service — prefer official APIs, keep the politeness delay and retry backoff.

Project layout

cronos/
├── .github/workflows/
│   ├── cronos_pipeline.yml     # daily ETL: cron + cache + GitOps commit
│   └── ci.yml                  # ruff lint/format + pytest on every push
├── data/                       # append-only datasets + stats.json
├── docs/index.html             # self-contained dashboard (GitHub Pages)
├── cronos/
│   ├── cli.py                  # `cronos` command
│   ├── config.py               # env-overridable configuration
│   ├── http_client.py          # retries, timeouts, UA rotation
│   ├── storage.py              # generic idempotent CSV writer
│   ├── scraper.py / validator.py / metrics.py / report.py
│   └── sources/                # Source contract + implementations
└── tests/                      # 37 unit tests, network fully mocked

Operational guarantees

  • Idempotent: stable dedup keys; re-runs never duplicate rows.
  • Append-only: history is never rewritten.
  • Validated: corrupt data fails the run before it can be pushed.
  • Fault-isolated: one failing source never blocks the others.
  • Deterministic: unchanged data → byte-identical outputs → no commit churn.
  • Race-safe & bounded: concurrency group, page ceilings, job timeouts.
  • Zero-cost: public repo Actions minutes + GitHub Pages hosting.

Contributing & security

PRs welcome — see CONTRIBUTING.md. New data sources are the most valued contribution. Security reports: SECURITY.md.

License

MIT

Release files for cronos-pipeline 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cronos-pipeline 1.0.0
File Size Uploaded
cronos_pipeline-1.0.0.tar.gz 28.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for cronos-pipeline 1.0.0
File Interpreter ABI Platform
cronos_pipeline-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 54.4 kB

Release files / cronos_pipeline-1.0.0.tar.gz

Download URL cronos_pipeline-1.0.0.tar.gz
Size 28.8 kB
Tags Source
SHA-256 checksum
How to use checksums
f088afbc8c23c67db403cc106149a052282a93b426fef194fb985081383b321c
BLAKE2b-256 checksum
How to use checksums
c610068fcd8940777659208613fbaa01d80813d30546f3ba4abaf24418874d36
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 18, 2026.

Transparency log

Release files / cronos_pipeline-1.0.0-py3-none-any.whl

Download URL cronos_pipeline-1.0.0-py3-none-any.whl
Size 25.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6376b7152778d518d33dff24d39fd194c6d942144d0a53573f02d8a8bcdb070b
BLAKE2b-256 checksum
How to use checksums
1cffdbe716bda4d3d4749eaf1f6bf19ebee272e427e384d1559a398b32c25247
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 18, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page