"""One-time (idempotent) seed of Postgres from the existing Google Sheets. This is the ONLY place the Sheet is treated as authoritative — to bootstrap the DB. After this, Postgres is the source of truth and Sheets is a downstream mirror. Safe to re-run: keyed tables upsert by primary key; activity_log is replaced wholesale. Run inside the container: docker compose exec smb-crm python import_from_sheets.py """ import os import sys import db from sheets import Sheets def upsert(cur, entity, records): spec = db.TABLES[entity] cols = spec["cols"] pk = spec["pk"] n = 0 for rec in records: row = db.coerce_row(entity, rec) if entity != "activity_log" and not row.get(pk): continue # skip rows without a primary key placeholders = ", ".join(["%s"] * len(cols)) collist = ", ".join(cols) if entity == "activity_log": cur.execute(f"INSERT INTO {entity} ({collist}) VALUES ({placeholders})", [row[c] for c in cols]) else: updates = ", ".join(f"{c} = EXCLUDED.{c}" for c in cols if c != pk) cur.execute( f"INSERT INTO {entity} ({collist}) VALUES ({placeholders}) " f"ON CONFLICT ({pk}) DO UPDATE SET {updates}", [row[c] for c in cols]) n += 1 return n def main(): sh = Sheets(os.environ["GOOGLE_SA_JSON"], os.environ["SHEET_ID"]) with db.connect() as conn: with conn.cursor() as cur: cur.execute("TRUNCATE activity_log RESTART IDENTITY") for entity, spec in db.TABLES.items(): records = sh.read_records(spec["tab"]) n = upsert(cur, entity, records) print(f" {entity:<13} <- {spec['tab']:<13} {n} row(s)") conn.commit() print("import complete.") if __name__ == "__main__": sys.exit(main())