Jingrui77's picture
deploy: sync BayesScenParams Agent (2026-05-17T16:01:00Z)
f8a3ca2
Raw
History Blame Contribute Delete
4.68 kB
"""Data import / sample dataset endpoints."""
from __future__ import annotations
import csv
import io
import json
from pathlib import Path
from fastapi import APIRouter, File, HTTPException, UploadFile
from app.api.schemas import SampleDatasetModel, SampleInfoModel
SAMPLES_DIR = Path(__file__).resolve().parents[1] / "data_sources" / "samples"
router = APIRouter(prefix="/api/data", tags=["data"])
SAMPLE_REGISTRY = {
"gdp": {"file": "sample-gdp.json", "icon": "📊"},
"climate": {"file": "sample-climate.json", "icon": "🌡️"},
"population": {"file": "sample-population.json", "icon": "👥"},
}
def _load(name: str) -> dict:
path = SAMPLES_DIR / SAMPLE_REGISTRY[name]["file"]
if not path.exists():
raise HTTPException(status_code=404, detail=f"sample {name} not found")
return json.loads(path.read_text(encoding="utf-8"))
@router.get("/samples", response_model=list[SampleInfoModel])
async def list_samples() -> list[SampleInfoModel]:
out: list[SampleInfoModel] = []
for sid, meta in SAMPLE_REGISTRY.items():
if sid not in SAMPLE_REGISTRY:
continue
data = _load(sid)
out.append(
SampleInfoModel(
id=sid,
name=data.get("name", sid),
name_en=data.get("name_en"),
source=data.get("source", ""),
unit=data.get("unit", ""),
period=data.get("period", ""),
description=data.get("description", ""),
n=len(data.get("values", [])),
icon=meta["icon"],
)
)
return out
@router.get("/samples/{sid}", response_model=SampleDatasetModel)
async def get_sample(sid: str) -> SampleDatasetModel:
if sid not in SAMPLE_REGISTRY:
raise HTTPException(status_code=404, detail=f"unknown sample id {sid}")
data = _load(sid)
return SampleDatasetModel(
id=sid,
name=data.get("name", sid),
name_en=data.get("name_en"),
source=data.get("source", ""),
unit=data.get("unit", ""),
period=data.get("period", ""),
description=data.get("description", ""),
n=len(data.get("values", [])),
icon=SAMPLE_REGISTRY[sid]["icon"],
values=list(data.get("values", [])),
)
@router.post("/upload")
async def upload_csv(file: UploadFile = File(...)) -> dict:
"""Accept a CSV upload, parse the first numeric column, return values + metadata.
Designed for the data-import workflow; the frontend renders a preview and
feeds the values back into /bayesian/compute.
"""
if file.content_type and "csv" not in file.content_type and not file.filename.endswith(".csv"):
raise HTTPException(status_code=415, detail="please upload a CSV file")
raw = await file.read()
try:
text = raw.decode("utf-8-sig")
except UnicodeDecodeError:
text = raw.decode("latin-1")
reader = csv.reader(io.StringIO(text))
rows = list(reader)
if not rows:
raise HTTPException(status_code=400, detail="empty CSV")
# Detect header
header = rows[0]
has_header = any(not _is_number(c) for c in header)
data_rows = rows[1:] if has_header else rows
# Find first column with at least 50% numeric values
n_cols = max((len(r) for r in data_rows), default=0)
chosen_col = 0
best_score = -1.0
for col in range(n_cols):
nums = sum(1 for r in data_rows if col < len(r) and _is_number(r[col]))
score = nums / max(len(data_rows), 1)
if score > best_score:
best_score = score
chosen_col = col
values: list[float] = []
skipped = 0
for r in data_rows:
if chosen_col >= len(r):
skipped += 1
continue
cell = r[chosen_col].strip()
if not cell:
skipped += 1
continue
try:
values.append(float(cell))
except ValueError:
skipped += 1
if len(values) < 2:
raise HTTPException(
status_code=400,
detail=f"could not parse at least 2 numeric values (got {len(values)})",
)
return {
"filename": file.filename,
"values": values,
"n_total_rows": len(data_rows),
"n_used": len(values),
"n_skipped": skipped,
"chosen_column_index": chosen_col,
"chosen_column_label": header[chosen_col]
if has_header and chosen_col < len(header)
else f"column_{chosen_col}",
}
def _is_number(s: str) -> bool:
try:
float(s.strip())
return True
except (ValueError, AttributeError):
return False