From 9b8ecdb84d819342653223b13517096fbb119be0 Mon Sep 17 00:00:00 2001 From: type-two Date: Sun, 21 Jun 2026 13:08:49 +1000 Subject: [PATCH] =?UTF-8?q?feat:=20deploy-ready=20=E2=80=94=20env-aware=20?= =?UTF-8?q?Discogs=20enrichment=20+=20Dockerfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app/mirror.py: centralised enrichment, DISCOGS_MIRROR_KIND selects full (ultra `release`) vs vps (`discogs_release`); virtual.py + wowplatter_v1.py use it. - Dockerfile + .dockerignore for the VPS container. Co-Authored-By: Claude Opus 4.8 --- .dockerignore | 5 +++++ Dockerfile | 7 +++++++ app/mirror.py | 31 +++++++++++++++++++++++++++++++ app/virtual.py | 24 +++++------------------- app/wowplatter_v1.py | 16 ++++++---------- 5 files changed, 54 insertions(+), 29 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 app/mirror.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..076f176 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.venv/ +.git/ +__pycache__/ +*.pyc +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b3de2e8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.12-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +EXPOSE 8010 +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8010"] diff --git a/app/mirror.py b/app/mirror.py new file mode 100644 index 0000000..53d827d --- /dev/null +++ b/app/mirror.py @@ -0,0 +1,31 @@ +import os + +from sqlalchemy import text + +from .db import MirrorSession + +# Enrichment from the Discogs mirror. Two known shapes, selected by env so the same code runs +# on ultra (the full dump) and on the VPS (dealgod's discogs_release subset): +# full → table `release` cols artists_sort / thumb / estimated_weight (ultra) +# vps → table `discogs_release` cols artist (no cover/weight) (dealgod-db) +KIND = os.getenv("DISCOGS_MIRROR_KIND", "full") +if KIND == "vps": + _SQL = ("SELECT id, title, artist, NULL::text AS thumb, NULL::int AS weight " + "FROM discogs_release WHERE id = ANY(:ids)") +else: + _SQL = ("SELECT id, title, artists_sort AS artist, " + "COALESCE(thumb_local_url, thumb) AS thumb, estimated_weight AS weight " + "FROM release WHERE id = ANY(:ids)") + + +async def enrich(ids): + """release_id(s) → {id: {title, artist, thumb, weight}}. Best-effort: mirror down → {}.""" + ids = [i for i in set(ids) if i is not None] + if not ids: + return {} + try: + async with MirrorSession() as m: + rows = await m.execute(text(_SQL), {"ids": ids}) + return {r["id"]: dict(r) for r in rows.mappings()} + except Exception: + return {} diff --git a/app/virtual.py b/app/virtual.py index b84ffb2..70d78bd 100644 --- a/app/virtual.py +++ b/app/virtual.py @@ -1,7 +1,8 @@ from fastapi import APIRouter, Depends from sqlalchemy import text -from .db import get_db, MirrorSession +from .db import get_db +from .mirror import enrich as mirror_enrich # The virtual store API — PUBLIC read (it's the customer-facing storefront). Replaces # WowPlatter's payload.php: same scene shape, but from Postgres instead of a full WP bootstrap. @@ -13,25 +14,10 @@ async def _all(db, table, where=""): return [dict(r) for r in rows.mappings()] -async def _enrich(release_ids): - """Pull title/artist/cover from the discogs_full mirror for a set of release ids.""" - ids = [i for i in release_ids if i is not None] - if not ids: - return {} - try: - async with MirrorSession() as m: - rows = await m.execute(text( - "SELECT id, title, artists_sort, COALESCE(thumb_local_url, thumb) AS thumb " - "FROM release WHERE id = ANY(:ids)"), {"ids": ids}) - return {r["id"]: dict(r) for r in rows.mappings()} - except Exception: - return {} # mirror down → scene still renders, just without covers/titles - - def _attach(recs, meta): for r in recs: m = meta.get(r["release_id"], {}) - r["title"], r["artist"], r["thumb"] = m.get("title"), m.get("artists_sort"), m.get("thumb") + r["title"], r["artist"], r["thumb"] = m.get("title"), m.get("artist"), m.get("thumb") return recs @@ -48,7 +34,7 @@ async def scene(db=Depends(get_db)): FROM inventory WHERE in_stock AND crate_id IS NOT NULL AND release_id IS NOT NULL ORDER BY crate_id, slot_number NULLS LAST"""))).mappings()] - recs = _attach(recs, await _enrich({r["release_id"] for r in recs})) + recs = _attach(recs, await mirror_enrich({r["release_id"] for r in recs})) return { "space": active, "spaces": spaces, @@ -72,7 +58,7 @@ async def crate_records(crate_id: int, db=Depends(get_db)): SELECT release_id, slot_number, sku, price, condition FROM inventory WHERE crate_id = :c AND in_stock AND release_id IS NOT NULL ORDER BY slot_number NULLS LAST"""), {"c": crate_id})).mappings()] - recs = _attach(recs, await _enrich({r["release_id"] for r in recs})) + recs = _attach(recs, await mirror_enrich({r["release_id"] for r in recs})) return {"ok": True, "crate_id": crate_id, "records": recs} diff --git a/app/wowplatter_v1.py b/app/wowplatter_v1.py index 0864fc2..c810dec 100644 --- a/app/wowplatter_v1.py +++ b/app/wowplatter_v1.py @@ -7,7 +7,8 @@ from sqlalchemy import text from . import __version__ from .auth import require_token -from .db import get_db, MirrorSession +from .db import get_db +from .mirror import enrich as mirror_enrich # The wowplatter/v1 contract — RecordGod's front door (see RECORDGOD_PLAN.md §3). # Routes 1, 2, 5 live here. Routes 3-4 (price/labels) are deliberately NOT defined yet: @@ -76,15 +77,10 @@ async def inventory_intake(body: IntakeIn, ident=Depends(require_token), db=Depe title = artist = None weight = None if body.release_id: - try: # ponytail: mirror down → stage un-enriched rather than fail the intake - async with MirrorSession() as mirror: - m = (await mirror.execute(text( - "SELECT title, artists_sort, estimated_weight FROM release WHERE id = :r" - ), {"r": body.release_id})).mappings().first() - if m: - title, artist, weight = m["title"], m["artists_sort"], m["estimated_weight"] - except Exception: - pass + # ponytail: mirror down → stage un-enriched rather than fail the intake (enrich() swallows) + meta = (await mirror_enrich([body.release_id])).get(body.release_id) + if meta: + title, artist, weight = meta["title"], meta["artist"], meta["weight"] sku = body.sku or _new_sku() await db.execute(text("""