← back to La Permits School
run_pipeline.sh
72 lines
#!/usr/bin/env bash
# run_pipeline.sh — one full LIVE cycle: refresh current permits, recover contractors,
# merge into a growing master, refresh stats, commit if anything changed. $0, idempotent.
# Safe to run on a loop — the LADBS detail fetch is cached + rate-limited.
set -euo pipefail
cd "$(dirname "$0")"
# overlap guard — never let two cycles run at once (mkdir is atomic)
LOCK="tmp/.cycle.lock"
mkdir -p tmp
if ! mkdir "$LOCK" 2>/dev/null; then
echo "another cycle is running (lock held) — exiting."; exit 0
fi
trap 'rmdir "$LOCK" 2>/dev/null || true' EXIT
echo "=== [$(date '+%Y-%m-%d %H:%M')] LIVE pipeline cycle ==="
# 1. pull current permits (last 365d, >$1M commercial/new) — updates ~daily upstream
python3 pull_permits_live.py --min-valuation 1000000 --days 365 --out permits_live.csv
# 2. recover live contractor + CSLB license per permit (cached -> only new permits fetch)
python3 enrich_live_contractor.py --in permits_live.csv --out permits_live_enriched.csv --delay 1.0
# 3. merge into the accumulating master (dedupe by permit_nbr; keep first-seen date)
python3 - <<'PY'
import csv, os
MASTER="master_live.csv"
new=list(csv.DictReader(open("permits_live_enriched.csv",newline="",encoding="utf-8")))
if not new: raise SystemExit("no enriched rows")
cols=list(new[0].keys())
if "first_seen" not in cols: cols=cols+["first_seen"]
seen={}
if os.path.exists(MASTER):
for r in csv.DictReader(open(MASTER,newline="",encoding="utf-8")):
seen[r["permit_nbr"]]=r
added=0
for r in new:
k=r["permit_nbr"]
if k in seen: # keep original first_seen
r["first_seen"]=seen[k].get("first_seen","")
else:
r["first_seen"]=r.get("issue_date","")
added+=1
seen[k]=r
# STABLE sort (valuation desc, then permit_nbr) so unchanged data -> byte-identical file
rows=sorted(seen.values(), key=lambda r: (-float(r.get("valuation") or 0), r.get("permit_nbr","")))
with open(MASTER,"w",newline="",encoding="utf-8") as f:
w=csv.DictWriter(f,fieldnames=cols); w.writeheader()
for r in rows: w.writerow({c:r.get(c,"") for c in cols})
print(f" master: {len(rows)} total permits (+{added} new this cycle)")
# stats file carries ONLY stable totals (no volatile per-cycle counter) so no-op
# cycles produce no diff and the git guard below correctly skips them
open("live_stats.txt","w").write(
f"master_live permits: {len(rows)}\n"
f"total value: ${sum(float(r.get('valuation') or 0) for r in rows):,.0f}\n")
PY
# 4. regenerate the ranked live board (the loop's analytical product)
python3 live_board.py
# 5. commit if the tracked artifacts changed (master + stats + board force-tracked; data is the point)
git add -f master_live.csv live_stats.txt LIVE-BOARD.md 2>/dev/null || true
git add -A
if ! git diff --cached --quiet; then
git commit -q -m "live cycle: refresh permits + contractors, merge master ($(date '+%Y-%m-%d %H:%M'))" \
--author="Steve Abrams <steve@designerwallcoverings.com>"
echo " committed."
else
echo " no changes to commit."
fi
echo "=== cycle done (cost: \$0) ==="