feat: initial MTZ Excel-to-JSON API wrapper

This commit is contained in:
Malin
2026-03-20 10:31:19 +01:00
commit db7c31260b
6 changed files with 125 additions and 0 deletions

99
main.py Normal file
View File

@@ -0,0 +1,99 @@
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
ean = str(int(ean_raw)) if ean_raw is not None else ""
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)}