Commit 461ce502 authored by o's avatar o
Browse files

fix: serialize pandas and numpy analysis results

parent 14b28276
Loading
Loading
Loading
Loading
+46 −3
Changes for api/routers/analysis.py: 46 added lines, 3 removed lines.
Original line number Diff line number Diff line
"""분석 실행 API 엔드포인트"""

import math
import traceback

import numpy as np
import pandas as pd

from fastapi import APIRouter, HTTPException
from api.schemas.analysis import AnalysisRunRequest, AnalysisRunResponse
from fastapi.responses import JSONResponse
from api.schemas.analysis import AnalysisRunRequest
from api.services import analysis_service

router = APIRouter()
service = analysis_service


@router.post("/run", response_model=AnalysisRunResponse)
def make_json_safe(value):
    if hasattr(value, "model_dump"):
        return make_json_safe(value.model_dump())

    if isinstance(value, pd.DataFrame):
        return value.replace({np.nan: None}).to_dict(orient="records")

    if isinstance(value, pd.Series):
        return value.replace({np.nan: None}).to_dict()

    if isinstance(value, dict):
        return {key: make_json_safe(item) for key, item in value.items()}

    if isinstance(value, list):
        return [make_json_safe(item) for item in value]

    if isinstance(value, tuple):
        return [make_json_safe(item) for item in value]

    if isinstance(value, np.integer):
        return int(value)

    if isinstance(value, np.floating):
        number = float(value)
        if math.isnan(number):
            return None
        return number

    if isinstance(value, np.ndarray):
        return make_json_safe(value.tolist())

    return value


@router.post("/run")
async def run_analysis(request: AnalysisRunRequest):
    """
    품질 분석 실행
@@ -40,6 +82,7 @@ async def run_analysis(request: AnalysisRunRequest):
    """
    try:
        result = await service.execute(request)
        return result
        return JSONResponse(content=make_json_safe(result.model_dump()))
    except Exception as e:
        traceback.print_exc()
        raise HTTPException(status_code=500, detail=str(e))
+51 −5
Changes for api/routers/results.py: 51 added lines, 5 removed lines.
Original line number Diff line number Diff line
"""결과 조회 API 엔드포인트"""

import math

import numpy as np
import pandas as pd

from fastapi import APIRouter, HTTPException
from api.schemas.analysis import AnalysisSummaryResponse, MetricDetailResponse
from fastapi.responses import JSONResponse
from api.schemas.analysis import AnalysisSummaryResponse
from api.services import analysis_service

router = APIRouter()
service = analysis_service


def make_json_safe(value):
    if hasattr(value, "model_dump"):
        return make_json_safe(value.model_dump())

    if isinstance(value, pd.DataFrame):
        return value.replace({np.nan: None}).to_dict(orient="records")

    if isinstance(value, pd.Series):
        return value.replace({np.nan: None}).to_dict()

    if isinstance(value, dict):
        return {key: make_json_safe(item) for key, item in value.items()}

    if isinstance(value, list):
        return [make_json_safe(item) for item in value]

    if isinstance(value, tuple):
        return [make_json_safe(item) for item in value]

    if isinstance(value, np.integer):
        return int(value)

    if isinstance(value, np.floating):
        number = float(value)
        if math.isnan(number):
            return None
        return number

    if isinstance(value, np.ndarray):
        return make_json_safe(value.tolist())

    try:
        if pd.isna(value):
            return None
    except (TypeError, ValueError):
        pass

    return value


@router.get("/{analysis_id}/summary", response_model=AnalysisSummaryResponse)
async def get_analysis_summary(analysis_id: str):
    """
@@ -22,7 +68,7 @@ async def get_analysis_summary(analysis_id: str):
    return result


@router.get("/{analysis_id}/details/{metric}", response_model=MetricDetailResponse)
@router.get("/{analysis_id}/details/{metric}")
async def get_metric_detail(analysis_id: str, metric: str):
    """
    개별 지표 상세 결과 조회
@@ -32,7 +78,7 @@ async def get_metric_detail(analysis_id: str, metric: str):
    result = service.get_metric_detail(analysis_id, metric)
    if not result:
        raise HTTPException(status_code=404, detail="Result not found")
    return result
    return JSONResponse(content=make_json_safe(result))


@router.get("/{analysis_id}/rules/{metric}")
@@ -43,7 +89,7 @@ async def get_metric_rules(analysis_id: str, metric: str):
    result = service.get_rule_results(analysis_id, metric)
    if not result:
        raise HTTPException(status_code=404, detail="Rule results not found")
    return result
    return JSONResponse(content=make_json_safe(result))


@router.get("/{analysis_id}/samples/{metric}")
@@ -56,4 +102,4 @@ async def get_metric_samples(analysis_id: str, metric: str, limit: int = 50):
    result = service.get_samples(analysis_id, metric, limit)
    if not result:
        raise HTTPException(status_code=404, detail="Samples not found")
    return result
    return JSONResponse(content=make_json_safe(result))
+47 −3
Changes for api/services/analysis_service.py: 47 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -2,10 +2,12 @@
분석 실행 서비스 - 기존 pipeline.py의 run_analysis()를 감싸는 계층
"""

import math
import uuid
from typing import Optional, Any, Callable

from fastapi.encoders import jsonable_encoder
import numpy as np
import pandas as pd

from api.database import SessionLocal
from api.model.analysis_history import AnalysisHistory
@@ -33,6 +35,7 @@ NAME_TO_LEGACY = {
}
LEGACY_TO_NAME = {v: k for k, v in NAME_TO_LEGACY.items()}


# 지표별 detail 키 prefix
METRIC_PREFIX = {
    "completeness": "comp_",
@@ -44,6 +47,47 @@ METRIC_PREFIX = {
}


def make_json_safe(value):
    """Convert pandas/numpy objects into JSON-serializable Python objects."""
    if isinstance(value, pd.DataFrame):
        return value.replace({np.nan: None}).to_dict(orient="records")

    if isinstance(value, pd.Series):
        return value.replace({np.nan: None}).to_dict()

    if isinstance(value, dict):
        return {key: make_json_safe(item) for key, item in value.items()}

    if isinstance(value, list):
        return [make_json_safe(item) for item in value]

    if isinstance(value, tuple):
        return [make_json_safe(item) for item in value]

    if isinstance(value, set):
        return [make_json_safe(item) for item in value]

    if isinstance(value, np.integer):
        return int(value)

    if isinstance(value, np.floating):
        number = float(value)
        if math.isnan(number):
            return None
        return number

    if isinstance(value, np.ndarray):
        return make_json_safe(value.tolist())

    try:
        if pd.isna(value):
            return None
    except (TypeError, ValueError):
        pass

    return value


class AnalysisService:
    """분석 실행 및 결과 관리 서비스"""

@@ -113,8 +157,8 @@ class AnalysisService:

            existing.dataset_id = stored.get("dataset_id")
            existing.dataset_filename = stored.get("dataset_filename")
            existing.results_json = jsonable_encoder(stored.get("results", {}) or {})
            existing.config_json = jsonable_encoder(config_for_history)
            existing.results_json = make_json_safe(stored.get("results", {}) or {})
            existing.config_json = make_json_safe(config_for_history)
            overall_score = weighted_summary.get("overall_score")
            existing.overall_score = str(overall_score) if overall_score is not None else None
            existing.weighting_method = weighted_summary.get("weighting_method")