Skip to main content

Serverless, zero-cost GitOps ETL pipeline on GitHub Actions

Project description

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cronos_pipeline-1.0.0.tar.gz (28.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cronos_pipeline-1.0.0-py3-none-any.whl (25.6 kB view details)

Uploaded Python 3

File details

Details for the file cronos_pipeline-1.0.0.tar.gz.

File metadata

  • Download URL: cronos_pipeline-1.0.0.tar.gz
  • Upload date:
  • Size: 28.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cronos_pipeline-1.0.0.tar.gz
Algorithm Hash digest
SHA256 f088afbc8c23c67db403cc106149a052282a93b426fef194fb985081383b321c
MD5 3cf4fa37eab49c420ab3c8b1d384d2a8
BLAKE2b-256 c610068fcd8940777659208613fbaa01d80813d30546f3ba4abaf24418874d36

See more details on using hashes here.

Provenance

The following attestation bundles were made for cronos_pipeline-1.0.0.tar.gz:

Publisher: release.yml on farajzada/cronos

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cronos_pipeline-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: cronos_pipeline-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 25.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cronos_pipeline-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6376b7152778d518d33dff24d39fd194c6d942144d0a53573f02d8a8bcdb070b
MD5 8360f9c09f91cbbce7d5739bd756fe2c
BLAKE2b-256 1cffdbe716bda4d3d4749eaf1f6bf19ebee272e427e384d1559a398b32c25247

See more details on using hashes here.

Provenance

The following attestation bundles were made for cronos_pipeline-1.0.0-py3-none-any.whl:

Publisher: release.yml on farajzada/cronos

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page