feat: initial MTZ Excel-to-JSON API wrapper
This commit is contained in:
3
.env.example
Normal file
3
.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
API_KEY=change-me-to-a-strong-secret
|
||||
EXCEL_URL=https://files.constantcontact.com/d8c838c7001/d68c2f97-5cd3-40c9-b0df-04296ccdcc7b.xlsx
|
||||
REFRESH_HOURS=6
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.env
|
||||
__pycache__/
|
||||
*.pyc
|
||||
7
Dockerfile
Normal file
7
Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY main.py .
|
||||
EXPOSE 8080
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
|
||||
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
version: "3.9"
|
||||
services:
|
||||
mtz-api:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
env_file: .env
|
||||
restart: unless-stopped
|
||||
99
main.py
Normal file
99
main.py
Normal 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)}
|
||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
fastapi==0.110.0
|
||||
uvicorn[standard]==0.29.0
|
||||
openpyxl==3.1.2
|
||||
requests==2.31.0
|
||||
python-dotenv==1.0.1
|
||||
Reference in New Issue
Block a user