feat: deploy-ready — env-aware Discogs enrichment + Dockerfile
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
e1fcf94f93
commit
9b8ecdb84d
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.venv/
|
||||||
|
.git/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
7
Dockerfile
Normal file
7
Dockerfile
Normal file
@ -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"]
|
||||||
31
app/mirror.py
Normal file
31
app/mirror.py
Normal file
@ -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 {}
|
||||||
@ -1,7 +1,8 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy import text
|
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
|
# 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.
|
# 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()]
|
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):
|
def _attach(recs, meta):
|
||||||
for r in recs:
|
for r in recs:
|
||||||
m = meta.get(r["release_id"], {})
|
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
|
return recs
|
||||||
|
|
||||||
|
|
||||||
@ -48,7 +34,7 @@ async def scene(db=Depends(get_db)):
|
|||||||
FROM inventory
|
FROM inventory
|
||||||
WHERE in_stock AND crate_id IS NOT NULL AND release_id IS NOT NULL
|
WHERE in_stock AND crate_id IS NOT NULL AND release_id IS NOT NULL
|
||||||
ORDER BY crate_id, slot_number NULLS LAST"""))).mappings()]
|
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 {
|
return {
|
||||||
"space": active, "spaces": spaces,
|
"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
|
SELECT release_id, slot_number, sku, price, condition
|
||||||
FROM inventory WHERE crate_id = :c AND in_stock AND release_id IS NOT NULL
|
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()]
|
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}
|
return {"ok": True, "crate_id": crate_id, "records": recs}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,8 @@ from sqlalchemy import text
|
|||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from .auth import require_token
|
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).
|
# 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:
|
# 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
|
title = artist = None
|
||||||
weight = None
|
weight = None
|
||||||
if body.release_id:
|
if body.release_id:
|
||||||
try: # ponytail: mirror down → stage un-enriched rather than fail the intake
|
# ponytail: mirror down → stage un-enriched rather than fail the intake (enrich() swallows)
|
||||||
async with MirrorSession() as mirror:
|
meta = (await mirror_enrich([body.release_id])).get(body.release_id)
|
||||||
m = (await mirror.execute(text(
|
if meta:
|
||||||
"SELECT title, artists_sort, estimated_weight FROM release WHERE id = :r"
|
title, artist, weight = meta["title"], meta["artist"], meta["weight"]
|
||||||
), {"r": body.release_id})).mappings().first()
|
|
||||||
if m:
|
|
||||||
title, artist, weight = m["title"], m["artists_sort"], m["estimated_weight"]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sku = body.sku or _new_sku()
|
sku = body.sku or _new_sku()
|
||||||
await db.execute(text("""
|
await db.execute(text("""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user