""" Brand Registry Service - Stores Theme Packs (theme.json + assets refs) - Serves immutable theme versions by brand_id """ from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import Any, Dict, Optional from datetime import datetime import json import logging import os import uuid from pathlib import Path logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) DATA_DIR = Path(os.getenv("BRAND_REGISTRY_DATA", "/data/brand-registry")) THEMES_DIR = DATA_DIR / "brands" app = FastAPI( title="Brand Registry Service", description="Single source of truth for brand themes", version="0.1.0" ) class ThemePublishRequest(BaseModel): theme: Dict[str, Any] theme_version: Optional[str] = None metadata: Optional[Dict[str, Any]] = None class ThemeResponse(BaseModel): brand_id: str theme_version: str theme: Dict[str, Any] metadata: Optional[Dict[str, Any]] = None created_at: str def _ensure_dirs() -> None: THEMES_DIR.mkdir(parents=True, exist_ok=True) def _theme_path(brand_id: str, theme_version: str) -> Path: return THEMES_DIR / brand_id / "themes" / theme_version / "theme.json" def _meta_path(brand_id: str, theme_version: str) -> Path: return THEMES_DIR / brand_id / "themes" / theme_version / "meta.json" def _list_versions(brand_id: str) -> list: base = THEMES_DIR / brand_id / "themes" if not base.exists(): return [] versions = [p.name for p in base.iterdir() if p.is_dir()] return sorted(versions) @app.get("/") async def root() -> Dict[str, Any]: _ensure_dirs() return { "service": "brand-registry", "status": "running", "data_dir": str(DATA_DIR), "version": "0.1.0" } @app.get("/health") async def health() -> Dict[str, Any]: _ensure_dirs() return {"status": "healthy"} @app.post("/brands/{brand_id}/themes", response_model=ThemeResponse) async def publish_theme(brand_id: str, payload: ThemePublishRequest) -> ThemeResponse: _ensure_dirs() theme_version = payload.theme_version or f"v1-{uuid.uuid4().hex[:8]}" theme_path = _theme_path(brand_id, theme_version) theme_path.parent.mkdir(parents=True, exist_ok=True) created_at = datetime.utcnow().isoformat() + "Z" meta = { "brand_id": brand_id, "theme_version": theme_version, "created_at": created_at, "metadata": payload.metadata or {} } theme_path.write_text(json.dumps(payload.theme, ensure_ascii=False, indent=2), encoding="utf-8") _meta_path(brand_id, theme_version).write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8") logger.info("Published theme: %s/%s", brand_id, theme_version) return ThemeResponse( brand_id=brand_id, theme_version=theme_version, theme=payload.theme, metadata=payload.metadata or {}, created_at=created_at ) @app.get("/brands/{brand_id}/themes/{theme_version}", response_model=ThemeResponse) async def get_theme(brand_id: str, theme_version: str) -> ThemeResponse: theme_path = _theme_path(brand_id, theme_version) if not theme_path.exists(): raise HTTPException(status_code=404, detail="Theme not found") theme = json.loads(theme_path.read_text(encoding="utf-8")) meta_path = _meta_path(brand_id, theme_version) meta = json.loads(meta_path.read_text(encoding="utf-8")) if meta_path.exists() else {} return ThemeResponse( brand_id=brand_id, theme_version=theme_version, theme=theme, metadata=meta.get("metadata"), created_at=meta.get("created_at", "") ) @app.get("/brands/{brand_id}/latest", response_model=ThemeResponse) async def get_latest(brand_id: str) -> ThemeResponse: versions = _list_versions(brand_id) if not versions: raise HTTPException(status_code=404, detail="No themes for brand") return await get_theme(brand_id, versions[-1])