import os import threading import time import requests from fastapi import FastAPI, HTTPException, Security, Depends from fastapi.security.api_key import APIKeyHeader from openpyxl import load_workbook from io import BytesIO from dotenv import load_dotenv load_dotenv() API_KEY = os.getenv("API_KEY", "change-me") EXCEL_URL = os.getenv( "EXCEL_URL", "https://files.constantcontact.com/d8c838c7001/d68c2f97-5cd3-40c9-b0df-04296ccdcc7b.xlsx", ) REFRESH_HOURS = int(os.getenv("REFRESH_HOURS", "6")) app = FastAPI(title="MTZ Stock API", version="1.0.0") products_cache = [] last_refresh = None cache_lock = threading.Lock() api_key_header = APIKeyHeader(name="X-API-Key") def verify_key(key: str = Security(api_key_header)): if key != API_KEY: raise HTTPException(status_code=403, detail="Invalid API Key") return key def download_and_parse(): global products_cache, last_refresh resp = requests.get(EXCEL_URL, timeout=60) resp.raise_for_status() wb = load_workbook(BytesIO(resp.content), read_only=True, data_only=True) ws = wb.active rows = list(ws.iter_rows(min_row=6, values_only=True)) parsed = [] for row in rows: if row[1] is None: # col B (index 1) = item_code continue ean_raw = row[3] # col D if ean_raw is None: ean = "" elif isinstance(ean_raw, (int, float)): ean = str(int(ean_raw)) else: ean = str(ean_raw).strip() brand_raw = row[9] # col J brand = str(brand_raw) if brand_raw is not None else "" min_box = row[10] if row[10] is not None else 1 # col K parsed.append( { "item_code": int(row[1]), "ean": ean, "description": str(row[2] or ""), "price_usd": float(row[4] or 0), "stock": int(row[5] or 0), "brand": brand, "min_box_qty": int(min_box), } ) with cache_lock: products_cache = parsed last_refresh = time.time() wb.close() def background_refresh(): while True: time.sleep(REFRESH_HOURS * 3600) try: download_and_parse() except Exception as e: print(f"Background refresh failed: {e}") @app.on_event("startup") def startup(): download_and_parse() t = threading.Thread(target=background_refresh, daemon=True) t.start() @app.get("/api/health") def health(): return { "status": "ok", "product_count": len(products_cache), "last_refresh": last_refresh, } @app.get("/api/products", dependencies=[Depends(verify_key)]) def get_products(): with cache_lock: return products_cache @app.post("/api/refresh", dependencies=[Depends(verify_key)]) def refresh(): download_and_parse() return {"status": "ok", "product_count": len(products_cache)}