From a URL on the internet to a CSV on your disk in 50 lines.
Data Engineering Module 1Every data pipeline you'll ever write is some version of those three verbs. Today's pipeline is the simplest possible one — three Python functions and a __main__.
ETL vs ELT: same three letters, swapped order. Modern lakehouses prefer ELT. We'll cover both.
.json.gz fileshttps://data.gharchive.org/YYYY-MM-DD-H.json.gzOne event looks like:
{
"id": "12345",
"type": "PushEvent",
"actor": {"login": "octocat"},
"repo": {"name": "octocat/hello"},
"created_at": "2024-01-01T15:00:00Z"
}
type50 lines of Python. We'll keep coming back to this script for the rest of Module 1.
import requests, gzip, json, pathlib
URL = "https://data.gharchive.org/{hour}.json.gz"
def fetch_hour(hour: str, out_dir: pathlib.Path) -> pathlib.Path:
out_dir.mkdir(parents=True, exist_ok=True)
dest = out_dir / f"{hour}.json.gz"
if dest.exists():
return dest # poor man's caching
resp = requests.get(URL.format(hour=hour), timeout=30)
resp.raise_for_status()
dest.write_bytes(resp.content)
return dest
Notice: we cache by hour. Re-running is free. This is the first hint of idempotency — Lesson 06's whole topic.
from collections import Counter
def count_by_type(path: pathlib.Path) -> Counter:
counts = Counter()
with gzip.open(path, "rt") as f:
for line in f:
ev = json.loads(line)
counts[ev["type"]] += 1
return counts
Use Counter, not dict. Use a streaming reader (for line in f) not f.read() — gharchive hours can spike to 200 MB.
import csv
def write_csv(counts: Counter, path: pathlib.Path) -> None:
with path.open("w", newline="") as f:
w = csv.writer(f)
w.writerow(["type", "count"])
for evtype, n in counts.most_common():
w.writerow([evtype, n])
if __name__ == "__main__":
import sys
hour = sys.argv[1] # e.g. "2024-01-01-15"
raw = fetch_hour(hour, pathlib.Path("data/raw"))
counts = count_by_type(raw)
write_csv(counts, pathlib.Path(f"data/out/{hour}.csv"))
print(f"wrote {sum(counts.values())} events across {len(counts)} types")
| type | count |
|---|---|
| PushEvent | 45,212 |
| CreateEvent | 12,103 |
| PullRequestEvent | 8,940 |
| WatchEvent | 7,712 |
| ... | ... |
Counter (constant-memory grouping)__main__ guardEach of those is a lesson on its own — we'll deepen them across Module 1.
That's literally the rest of the course. Each lesson erases one item from this list.
data-engineering/sample-pipeline/
extract/gharchive.py # fetch_hour
transform/events.py # count_by_type, write_csv
requirements.txt
Today you'll fill the NotImplementedError stubs that Plan A scaffolded.
We do batch ETL today. The modern lakehouse prefers ELT (load first, transform on the warehouse) — that pattern is covered in Module 3 when we introduce dbt and a cloud warehouse.
Streaming (Kafka, Flink) is a sibling discipline and intentionally not in this course. It requires a different runtime model; treating it as "just faster batch" is one of the classic mistakes in data engineering. Our gharchive pipeline is batch by design — the data is already hourly.