| 1 | """Idempotent seed loader. Creates an admin user, a source, a watchlist, ingests the |
| 2 | bundled sample exports, and stands up a demo investigation with sightings attached. |
| 3 | Safe to run repeatedly (uses get-or-create + content-hash import dedup).""" |
| 4 | from __future__ import annotations |
| 5 | |
| 6 | import hashlib |
| 7 | import os |
| 8 | |
| 9 | from sqlalchemy import select |
| 10 | |
| 11 | from app.core.security import hash_password |
| 12 | from app.db.session import SessionLocal |
| 13 | from app.models import ( |
| 14 | Import, Investigation, Sighting, Source, User, Watchlist, WatchlistEntry, PlateRead, |
| 15 | ) |
| 16 | from app.services.ingest import process_import |
| 17 | |
| 18 | SAMPLE_DIR = os.path.join(os.path.dirname(__file__), "sample_data") |
| 19 | |
| 20 | |
| 21 | def get_or_create(db, model, defaults=None, **filters): |
| 22 | obj = db.scalar(select(model).filter_by(**filters)) |
| 23 | if obj: |
| 24 | return obj, False |
| 25 | obj = model(**filters, **(defaults or {})) |
| 26 | db.add(obj) |
| 27 | db.flush() |
| 28 | return obj, True |
| 29 | |
| 30 | |
| 31 | def ingest_file(db, source, path): |
| 32 | with open(path, "rb") as fh: |
| 33 | data = fh.read() |
| 34 | sha = hashlib.sha256(data).hexdigest() |
| 35 | existing = db.scalar(select(Import).where(Import.sha256 == sha)) |
| 36 | if existing: |
| 37 | return existing.id, "already imported" |
| 38 | ctype = ("text/csv" if path.endswith(".csv") |
| 39 | else "application/pdf" if path.endswith(".pdf") else "application/json") |
| 40 | imp = Import(source_id=source.id, filename=os.path.basename(path), |
| 41 | content_type=ctype, sha256=sha, raw_blob_ref=path, status="received") |
| 42 | db.add(imp) |
| 43 | db.commit() |
| 44 | summary = process_import(db, imp.id) |
| 45 | return imp.id, summary |
| 46 | |
| 47 | |
| 48 | def main(): |
| 49 | db = SessionLocal() |
| 50 | try: |
| 51 | admin, created = get_or_create( |
| 52 | db, User, email="admin@sightline.app", |
| 53 | defaults={"hashed_password": hash_password("changeme123"), |
| 54 | "full_name": "Demo Admin", "role": "admin"}) |
| 55 | if created: |
| 56 | print("created admin: admin@sightline.app / changeme123") |
| 57 | |
| 58 | source, _ = get_or_create( |
| 59 | db, Source, name="Demo Flock Export", |
| 60 | defaults={"system_type": "flock", "organization": "default", |
| 61 | "jurisdiction": "Shelby County, TN", "trust_level": 0.9}) |
| 62 | |
| 63 | wl, _ = get_or_create(db, Watchlist, name="Stolen Vehicles", |
| 64 | defaults={"kind": "plate", "organization": "default", |
| 65 | "created_by": admin.id}) |
| 66 | if not db.scalar(select(WatchlistEntry).where(WatchlistEntry.plate_norm == "XYZ7788")): |
| 67 | db.add(WatchlistEntry(watchlist_id=wl.id, plate_norm="XYZ7788", |
| 68 | reason="reported stolen (demo)")) |
| 69 | db.commit() |
| 70 | |
| 71 | for fname in ("flock_sample.csv", "flock_sample.json", "flock_report.pdf"): |
| 72 | path = os.path.join(SAMPLE_DIR, fname) |
| 73 | if os.path.exists(path): |
| 74 | _id, summary = ingest_file(db, source, path) |
| 75 | print(f"ingested {fname}: {summary}") |
| 76 | |
| 77 | inv, created = get_or_create( |
| 78 | db, Investigation, title="Demo: Downtown Corridor Activity", |
| 79 | defaults={"summary": "Sample investigation tracking vehicles through the " |
| 80 | "downtown/Poplar corridor.", "priority": "high", |
| 81 | "status": "active", "organization": "default", "lead_user_id": admin.id}) |
| 82 | if created: |
| 83 | for plate in ("ABC1234", "XYZ7788", "GHI5566"): |
| 84 | reads = db.scalars(select(PlateRead).where(PlateRead.plate_norm == plate)).all() |
| 85 | for r in reads: |
| 86 | s = db.scalar(select(Sighting).where(Sighting.plate_read_id == r.id)) |
| 87 | if s: |
| 88 | s.investigation_id = inv.id |
| 89 | db.commit() |
| 90 | print(f"created investigation: {inv.title} ({inv.id})") |
| 91 | |
| 92 | total = db.scalar(select(PlateRead).limit(1)) |
| 93 | print("seed complete." if total else "seed complete (no reads - check sample data).") |
| 94 | finally: |
| 95 | db.close() |
| 96 | |
| 97 | |
| 98 | if __name__ == "__main__": |
| 99 | main() |