PawTrace / backend /scripts /purge_seed_data.py
Elliott Duke
HomingPet: lost-dog reunification (FastAPI + React) with Render deploy
de1e3fc
Raw
History Blame Contribute Delete
6.66 kB
"""Delete the seeded demo data (generated-dot 'dogs' from scripts.make_sample_images) and everything
attached to it, leaving the real loaded datasets untouched.
Demo records are the ones created directly by scripts/seed.py (NOT via the batch loader), so they
have ``dataset_id IS NULL``. We remove:
* every known/unknown dog with ``dataset_id IS NULL`` (Rex, Maple, the demo found dogs) +
their pictures, embeddings, breed predictions, cases, matches, notifications, and media files;
* the demo OWNER users of those dogs — but ONLY non-admin users who own no *dataset* dogs, so the
admin account and the (dataset_id-NULL but real) detached dataset owners are preserved.
Idempotent. Dry-run by default.
Usage (from backend/): python -m scripts.purge_seed_data --delete
"""
from __future__ import annotations
import argparse
from sqlalchemy import and_, delete, func, or_, select
from app.db import SessionLocal
from app.models import (
BreedPrediction, Case, Embedding, KnownDog, Match, Notification, Picture, UnknownDog, User,
)
from app.models.base import SubjectType, UserRole
from app.storage import get_storage
def run(do_delete: bool) -> None:
db = SessionLocal()
try:
known_ids = [i for (i,) in db.execute(select(KnownDog.id).where(KnownDog.dataset_id.is_(None)))]
unknown_ids = [i for (i,) in db.execute(select(UnknownDog.id).where(UnknownDog.dataset_id.is_(None)))]
# Demo owners: owners of these known dogs that are NOT admin and own no dataset dog.
owner_ids = {
o for (o,) in db.execute(
select(KnownDog.owner_id).where(KnownDog.dataset_id.is_(None)).distinct()
)
}
demo_user_ids = []
for uid in owner_ids:
u = db.get(User, uid)
if u is None or u.role == UserRole.admin:
continue
keeps = db.execute(
select(func.count()).select_from(KnownDog).where(
KnownDog.owner_id == uid, KnownDog.dataset_id.isnot(None)
)
).scalar_one()
if keeps == 0:
demo_user_ids.append(uid)
# Cases / pictures / matches tied to the demo dogs or demo owners.
case_conds = []
if known_ids:
case_conds.append(Case.known_dog_id.in_(known_ids))
if unknown_ids:
case_conds.append(Case.unknown_dog_id.in_(unknown_ids))
if demo_user_ids:
case_conds.append(Case.person_id.in_(demo_user_ids))
case_ids = (
[i for (i,) in db.execute(select(Case.id).where(or_(*case_conds)))] if case_conds else []
)
pic_conds = []
if known_ids:
pic_conds.append(and_(Picture.subject_type == SubjectType.known, Picture.subject_id.in_(known_ids)))
if unknown_ids:
pic_conds.append(and_(Picture.subject_type == SubjectType.unknown, Picture.subject_id.in_(unknown_ids)))
pic_rows = (
db.execute(select(Picture.id, Picture.file_path, Picture.thumb_path).where(or_(*pic_conds))).all()
if pic_conds else []
)
pic_ids = [r[0] for r in pic_rows]
media_keys = [k for r in pic_rows for k in (r[1], r[2]) if k]
match_conds = []
if case_ids:
match_conds.append(Match.case_id.in_(case_ids))
match_conds.append(Match.candidate_case_id.in_(case_ids))
if known_ids:
match_conds.append(and_(Match.candidate_type == SubjectType.known, Match.candidate_id.in_(known_ids)))
if unknown_ids:
match_conds.append(and_(Match.candidate_type == SubjectType.unknown, Match.candidate_id.in_(unknown_ids)))
match_ids = (
[i for (i,) in db.execute(select(Match.id).where(or_(*match_conds)))] if match_conds else []
)
print("Seed data to remove:")
print(f" known dogs: {len(known_ids)} {known_ids}")
print(f" unknown dogs: {len(unknown_ids)} {unknown_ids}")
print(f" demo owners: {len(demo_user_ids)} "
f"{[db.get(User, u).email for u in demo_user_ids]}")
print(f" cases: {len(case_ids)} matches: {len(match_ids)} pictures: {len(pic_ids)} "
f"media files: {len(set(media_keys))}")
if not do_delete:
print("\nDRY RUN — nothing deleted. Re-run with --delete.")
return
def _del(stmt):
return db.execute(stmt).rowcount or 0
# Children first (FK order), mirroring services.datasets.purge_dataset.
if demo_user_ids or case_ids or match_ids:
nconds = []
if demo_user_ids:
nconds.append(Notification.user_id.in_(demo_user_ids))
if case_ids:
nconds.append(Notification.case_id.in_(case_ids))
if match_ids:
nconds.append(Notification.match_id.in_(match_ids))
if nconds:
_del(delete(Notification).where(or_(*nconds)))
if pic_ids:
_del(delete(BreedPrediction).where(BreedPrediction.picture_id.in_(pic_ids)))
_del(delete(Embedding).where(Embedding.picture_id.in_(pic_ids)))
if match_ids:
_del(delete(Match).where(Match.id.in_(match_ids)))
if demo_user_ids:
db.execute(Match.__table__.update().where(Match.reviewed_by.in_(demo_user_ids)).values(reviewed_by=None))
if pic_ids:
_del(delete(Picture).where(Picture.id.in_(pic_ids)))
if case_ids:
_del(delete(Case).where(Case.id.in_(case_ids)))
if unknown_ids:
_del(delete(UnknownDog).where(UnknownDog.id.in_(unknown_ids)))
if known_ids:
_del(delete(KnownDog).where(KnownDog.id.in_(known_ids)))
if demo_user_ids:
_del(delete(User).where(User.id.in_(demo_user_ids)))
db.commit()
# Files after commit (best-effort; leftovers are recoverable via scripts.prune_media).
storage = get_storage()
removed = 0
for key in dict.fromkeys(media_keys):
try:
storage.delete(key)
removed += 1
except Exception: # noqa: BLE001
pass
print(f"\nDeleted seed data. Media files removed: {removed}. Admin + dataset dogs untouched.")
finally:
db.close()
def main() -> None:
ap = argparse.ArgumentParser(description="Purge seeded demo (generated-dot) data.")
ap.add_argument("--delete", action="store_true", help="Actually delete (default: dry run)")
args = ap.parse_args()
run(args.delete)
if __name__ == "__main__":
main()