Airflow module
Demo
First Airflow shift
Build a three-task extract → transform → load DAG over a tiny orders CSV, trigger it, and read the summary file.
45 minLocal lab
Do this after Your first DAG and Operators. Keep the stack from Getting Started running.
You will orchestrate a tiny retail extract: read a CSV, total amount by store, write a summary file. The website runs nothing. Data stays a handful of rows — XCom only passes the file path.
What you need
- Airflow UI at
http://127.0.0.1:18080(userairflow/airflow) ~/dcubes/airflow-labwithdags/anddata/bind-mounted
If the stack is down:
cd ~/dcubes/airflow-lab
docker compose up -d
1. Land the CSV
Download orders.csv or write it:
mkdir -p ~/dcubes/airflow-lab/data
cat > ~/dcubes/airflow-lab/data/orders.csv << 'EOF'
order_id,store,amount
1,north,12.50
2,south,8.00
3,north,20.00
4,east,5.25
5,south,11.75
EOF
Inside the container that path is /opt/airflow/data/orders.csv (see Getting Started volumes).
ls -l ~/dcubes/airflow-lab/data/orders.csv
2. Write the DAG
cat > ~/dcubes/airflow-lab/dags/retail_shift.py << 'EOF'
import csv
from collections import defaultdict
from datetime import datetime
from pathlib import Path
from airflow.sdk import dag, task
DATA = Path("/opt/airflow/data")
@dag(
schedule=None,
start_date=datetime(2024, 1, 1),
catchup=False,
tags=["dcubes", "shift"],
)
def retail_shift():
@task
def extract() -> str:
src = DATA / "orders.csv"
if not src.is_file():
raise FileNotFoundError(f"missing {src}")
print(f"extract {src}")
return str(src)
@task
def transform(path: str) -> str:
totals: dict[str, float] = defaultdict(float)
with Path(path).open(newline="") as handle:
for row in csv.DictReader(handle):
totals[row["store"]] += float(row["amount"])
out = DATA / "store_totals.csv"
with out.open("w", newline="") as handle:
writer = csv.writer(handle)
writer.writerow(["store", "amount"])
for store in sorted(totals):
writer.writerow([store, f"{totals[store]:.2f}"])
print(f"wrote {out}")
return str(out)
@task
def load(path: str) -> None:
text = Path(path).read_text()
summary = DATA / "summary.txt"
summary.write_text("store totals\n" + text)
print(summary.read_text())
load(transform(extract()))
retail_shift()
EOF
Wait 30–60 seconds. Refresh the UI. You want retail_shift.
If it is missing:
docker compose -f ~/dcubes/airflow-lab/compose.yaml logs airflow-dag-processor --tail 40
(or cd ~/dcubes/airflow-lab && docker compose logs airflow-dag-processor --tail 40)
3. Trigger and inspect
- Unpause retail_shift.
- Trigger.
- Graph:
extract→transform→load, all success. extractlog mentions/opt/airflow/data/orders.csv.loadlog prints store totals.
On the host:
cat ~/dcubes/airflow-lab/data/store_totals.csv
cat ~/dcubes/airflow-lab/data/summary.txt
store,amount
east,5.25
north,32.50
south,19.75
north is 12.50 + 20.00. If a number is wrong, the DAG is not the database — fix the CSV or the transform and clear transform + downstream.
4. Optional: version the DAG
You already have Git.
cd ~/dcubes/airflow-lab
printf '%s\n' '.env' 'logs/' 'config/' 'data/' '__pycache__/' > .gitignore
git init
git add compose.yaml dags/hello_airflow.py dags/retail_shift.py .gitignore
git status
git commit -m "Add Airflow lab compose and retail_shift DAG"
Do not commit .env, logs/, or data/. Compose and DAG files are the source of truth.
Checklist
-
retail_shiftappears in the UI - One successful run, three green tasks
-
data/store_totals.csvmatches the example - XCom values are paths, not the CSV contents
Common mistakes
- CSV on the host but not under
data/— the container only sees the bind mount - Returning the parsed rows from
extract— return the path - Leaving
fail_then_fix.pyasthis is not python— delete it so the processor stays clean
Next
Airflow is the first Data Engineering module. The lakehouse lab is next: the same machine, a bigger Compose stack. Warehouses and modeling stay coming soon. Still guidance only, still on your machine.
When you are done with the lab for the day:
cd ~/dcubes/airflow-lab
docker compose down