From eb3f8d3ce074ac219a40ae87dda7c8f48f434a18 Mon Sep 17 00:00:00 2001 From: Mesh Date: Mon, 1 Sep 2025 09:13:44 -0500 Subject: [PATCH 1/3] feat: added boxplot endpoint --- src/webapp/routers/data.py | 72 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/src/webapp/routers/data.py b/src/webapp/routers/data.py index 3c02c88d..5d7e0e00 100644 --- a/src/webapp/routers/data.py +++ b/src/webapp/routers/data.py @@ -5,7 +5,7 @@ from databricks.sdk import WorkspaceClient from typing import Annotated, Any, Dict, List, cast, IO, Optional from pydantic import BaseModel, Field -from fastapi import APIRouter, Depends, HTTPException, status, Response +from fastapi import APIRouter, Depends, HTTPException, status, Response, Query from fastapi.responses import FileResponse from sqlalchemy import and_, or_ from sqlalchemy.orm import Session @@ -1283,7 +1283,7 @@ def get_inference_top_features( current_user: Annotated[BaseUser, Depends(get_current_active_user)], sql_session: Annotated[Session, Depends(get_session)], ) -> List[dict[str, Any]]: - """Returns a signed URL for uploading data to a specific institution.""" + """Returns data for a specific institution.""" # raise error at this level instead bc otherwise it's getting wrapped as a 200 has_access_to_inst_or_err(inst_id, current_user) local_session.set(sql_session) @@ -1317,6 +1317,72 @@ def get_inference_top_features( # Return a 400 error with the specific message from ValueError raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) +# Get Box plot values +@router.get("/{inst_id}/inference/features-boxplot-stat/{run_id}") +def get_inference_feature_boxstats( + inst_id: str, + run_id: str, + feature_name: Optional[str] = Query(None, description="If provided, filter by this feature name"), + current_user: Annotated[BaseUser, Depends(get_current_active_user)], + sql_session: Annotated[Session, Depends(get_session)], +) -> List[dict[str, Any]]: + """Returns box-plot stats for an institution/run. If `feature_name` is supplied, + only rows for that feature are returned.""" + # raise error at this level instead bc otherwise it's getting wrapped as a 200 + has_access_to_inst_or_err(inst_id, current_user) + local_session.set(sql_session) + query_result = ( + local_session.get() + .execute(select(InstTable).where(InstTable.id == str_to_uuid(inst_id))) + .all() + ) + if not query_result or len(query_result) == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Institution not found.", + ) + if len(query_result) > 1: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Institution duplicates found.", + ) + + try: + dbc = DatabricksControl() + rows = dbc.fetch_table_data( + catalog_name=env_vars["CATALOG_NAME"], # type: ignore + inst_name=f"{query_result[0][0].name}", + table_name=f"inference_{run_id}_box_plot_table", + warehouse_id=env_vars["SQL_WAREHOUSE_ID"], # type: ignore + ) + if not feature_name: + return rows + + # Helper: extract feature_name from various shapes (top-level or JSON column) + def row_feature_name(row: dict[str, Any]) -> Optional[str]: + # common case: it's a top-level column + if "feature_name" in row and row["feature_name"] is not None: + return str(row["feature_name"]) + # fallback: search any dict-valued column for a 'feature_name' key + for v in row.values(): + if isinstance(v, dict) and "feature_name" in v and v["feature_name"] is not None: + return str(v["feature_name"]) + return None + + filtered = [r for r in rows if row_feature_name(r) == feature_name] + + if not filtered: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Feature '{feature_name}' not found for run_id '{run_id}'.", + ) + + return filtered + + except ValueError as ve: + # Return a 400 error with the specific message from ValueError + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) + # Get SHAP Values for Inference @router.get("/{inst_id}/inference/support-overview/{run_id}") @@ -1576,8 +1642,8 @@ def get_training_support_overview( @router.get("/{inst_id}/training/model-cards/{model_name}") def get_model_cards( - model_name: str, inst_id: str, + model_name: str, current_user: Annotated[BaseUser, Depends(get_current_active_user)], sql_session: Annotated[Session, Depends(get_session)], ) -> FileResponse: From ea9eca609180cfc2aee0ca26c9eb9d0f7039b254 Mon Sep 17 00:00:00 2001 From: Mesh Date: Mon, 1 Sep 2025 09:23:02 -0500 Subject: [PATCH 2/3] fix: linting --- src/webapp/routers/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webapp/routers/data.py b/src/webapp/routers/data.py index 5d7e0e00..b29bffec 100644 --- a/src/webapp/routers/data.py +++ b/src/webapp/routers/data.py @@ -1322,9 +1322,9 @@ def get_inference_top_features( def get_inference_feature_boxstats( inst_id: str, run_id: str, - feature_name: Optional[str] = Query(None, description="If provided, filter by this feature name"), current_user: Annotated[BaseUser, Depends(get_current_active_user)], sql_session: Annotated[Session, Depends(get_session)], + feature_name: Optional[str] = Query(None, description="If provided, filter by this feature name"), ) -> List[dict[str, Any]]: """Returns box-plot stats for an institution/run. If `feature_name` is supplied, only rows for that feature are returned.""" From 324e70686b0d32e2e848421704bb8aea71351698 Mon Sep 17 00:00:00 2001 From: Mesh Date: Mon, 1 Sep 2025 09:25:54 -0500 Subject: [PATCH 3/3] fix: linting --- src/webapp/routers/data.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/webapp/routers/data.py b/src/webapp/routers/data.py index b29bffec..36079908 100644 --- a/src/webapp/routers/data.py +++ b/src/webapp/routers/data.py @@ -1317,6 +1317,7 @@ def get_inference_top_features( # Return a 400 error with the specific message from ValueError raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ve)) + # Get Box plot values @router.get("/{inst_id}/inference/features-boxplot-stat/{run_id}") def get_inference_feature_boxstats( @@ -1324,7 +1325,9 @@ def get_inference_feature_boxstats( run_id: str, current_user: Annotated[BaseUser, Depends(get_current_active_user)], sql_session: Annotated[Session, Depends(get_session)], - feature_name: Optional[str] = Query(None, description="If provided, filter by this feature name"), + feature_name: Optional[str] = Query( + None, description="If provided, filter by this feature name" + ), ) -> List[dict[str, Any]]: """Returns box-plot stats for an institution/run. If `feature_name` is supplied, only rows for that feature are returned.""" @@ -1357,7 +1360,7 @@ def get_inference_feature_boxstats( ) if not feature_name: return rows - + # Helper: extract feature_name from various shapes (top-level or JSON column) def row_feature_name(row: dict[str, Any]) -> Optional[str]: # common case: it's a top-level column @@ -1365,7 +1368,11 @@ def row_feature_name(row: dict[str, Any]) -> Optional[str]: return str(row["feature_name"]) # fallback: search any dict-valued column for a 'feature_name' key for v in row.values(): - if isinstance(v, dict) and "feature_name" in v and v["feature_name"] is not None: + if ( + isinstance(v, dict) + and "feature_name" in v + and v["feature_name"] is not None + ): return str(v["feature_name"]) return None