Spaces:
Sleeping
Sleeping
| """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")) | |
| 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 | |
| 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", [])), | |
| ) | |
| 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 | |