11import logging
22from typing import Optional
33
4- from fastapi import APIRouter , Depends , HTTPException , Query
4+ from fastapi import APIRouter , Depends , File , HTTPException , Query , UploadFile
55from sqlalchemy .orm import Session , selectinload
66
77from mavedb import deps
88from mavedb .lib .authentication import UserData , get_current_user
99from mavedb .lib .authorization import require_current_user
10+ from mavedb .lib .flexible_model_loader import json_or_form_loader
1011from mavedb .lib .logging import LoggedRoute
1112from mavedb .lib .logging .context import (
1213 logging_context ,
2021 modify_score_calibration ,
2122 promote_score_calibration_to_primary ,
2223 publish_score_calibration ,
24+ variant_classification_df_to_dict ,
2325)
26+ from mavedb .lib .score_sets import csv_data_to_df
27+ from mavedb .lib .validation .constants .general import calibration_class_column_name , calibration_variant_column_name
28+ from mavedb .lib .validation .dataframe .calibration import validate_and_standardize_calibration_classes_dataframe
2429from mavedb .models .score_calibration import ScoreCalibration
2530from mavedb .models .score_set import ScoreSet
2631from mavedb .view_models import score_calibration
2934
3035router = APIRouter (
3136 prefix = "/api/v1/score-calibrations" ,
32- tags = ["score-calibrations " ],
37+ tags = ["Score Calibrations " ],
3338 responses = {404 : {"description" : "Not found" }},
3439 route_class = LoggedRoute ,
3540)
3641
42+ # Create dependency loaders for flexible JSON/form parsing
43+ calibration_create_loader = json_or_form_loader (
44+ score_calibration .ScoreCalibrationCreate ,
45+ field_name = "calibration_json" ,
46+ )
47+
48+ calibration_modify_loader = json_or_form_loader (
49+ score_calibration .ScoreCalibrationModify ,
50+ field_name = "calibration_json" ,
51+ )
52+
3753
3854@router .get (
3955 "/{urn}" ,
@@ -162,19 +178,95 @@ async def get_primary_score_calibrations_for_score_set(
162178@router .post (
163179 "/" ,
164180 response_model = score_calibration .ScoreCalibrationWithScoreSetUrn ,
165- responses = {404 : {}},
181+ responses = {404 : {}, 422 : {"description" : "Validation Error" }},
182+ openapi_extra = {
183+ "requestBody" : {
184+ "content" : {
185+ "application/json" : {
186+ "schema" : {"$ref" : "#/components/schemas/ScoreCalibrationCreate" },
187+ },
188+ "multipart/form-data" : {
189+ "schema" : {
190+ "type" : "object" ,
191+ "properties" : {
192+ "calibration_json" : {
193+ "type" : "string" ,
194+ "description" : "JSON string containing the calibration data" ,
195+ "example" : '{"score_set_urn":"urn:mavedb:0000000X-X-X","title":"My Calibration","description":"Functional score calibration","baseline_score":1.0}' ,
196+ },
197+ "classes_file" : {
198+ "type" : "string" ,
199+ "format" : "binary" ,
200+ "description" : "CSV file containing variant classifications" ,
201+ },
202+ },
203+ }
204+ },
205+ },
206+ "description" : "Score calibration data. Can be sent as JSON body or multipart form data" ,
207+ }
208+ },
166209)
167210async def create_score_calibration_route (
168211 * ,
169- calibration : score_calibration .ScoreCalibrationCreate ,
212+ calibration : score_calibration .ScoreCalibrationCreate = Depends (calibration_create_loader ),
213+ classes_file : Optional [UploadFile ] = File (
214+ None ,
215+ description = f"CSV file containing variant classifications. This file must contain two columns: '{ calibration_variant_column_name } ' and '{ calibration_class_column_name } '." ,
216+ ),
170217 db : Session = Depends (deps .get_db ),
171218 user_data : UserData = Depends (require_current_user ),
172219) -> ScoreCalibration :
173220 """
174221 Create a new score calibration.
175222
176- The score set URN must be provided to associate the calibration with an existing score set.
177- The user must have write permission on the associated score set.
223+ This endpoint supports two different request formats to accommodate various client needs:
224+
225+ ## Method 1: JSON Request Body (application/json)
226+ Send calibration data as a standard JSON request body. This method is ideal for
227+ creating calibrations without file uploads.
228+
229+ **Content-Type**: `application/json`
230+
231+ **Example**:
232+ ```json
233+ {
234+ "score_set_urn": "urn:mavedb:0000000X-X-X",
235+ "title": "My Calibration",
236+ "description": "Functional score calibration",
237+ "baseline_score": 1.0
238+ }
239+ ```
240+
241+ ## Method 2: Multipart Form Data (multipart/form-data)
242+ Send calibration data as JSON in a form field, optionally with file uploads.
243+ This method is required when uploading classification files.
244+
245+ **Content-Type**: `multipart/form-data`
246+
247+ **Form Fields**:
248+ - `calibration_json` (string, required): JSON string containing the calibration data
249+ - `classes_file` (file, optional): CSV file containing variant classifications
250+
251+ **Example**:
252+ ```bash
253+ curl -X POST "/api/v1/score-calibrations/" \\
254+ -H "Authorization: Bearer your-token" \\
255+ -F 'calibration_json={"score_set_urn":"urn:mavedb:0000000X-X-X","title":"My Calibration","description":"Functional score calibration","baseline_score":"1.0"}' \\
256+ -F 'classes_file=@variant_classes.csv'
257+ ```
258+
259+ ## Requirements
260+ - The score set URN must be provided to associate the calibration with an existing score set
261+ - User must have write permission on the associated score set
262+ - If uploading a classes_file, it must be a valid CSV with variant classification data
263+
264+ ## File Upload Details
265+ The `classes_file` parameter accepts CSV files containing variant classification data.
266+ The file should have appropriate headers and contain columns for variant urns and class names.
267+
268+ ## Response
269+ Returns the created score calibration with its generated URN and associated score set information.
178270 """
179271 if not calibration .score_set_urn :
180272 raise HTTPException (status_code = 422 , detail = "score_set_urn must be provided to create a score calibration." )
@@ -190,7 +282,22 @@ async def create_score_calibration_route(
190282 # permission to update the score set itself.
191283 assert_permission (user_data , score_set , Action .UPDATE )
192284
193- created_calibration = await create_score_calibration_in_score_set (db , calibration , user_data .user )
285+ if classes_file :
286+ try :
287+ classes_df = csv_data_to_df (classes_file .file , induce_hgvs_cols = False )
288+ except UnicodeDecodeError as e :
289+ raise HTTPException (
290+ status_code = 400 , detail = f"Error decoding file: { e } . Ensure the file has correct values."
291+ )
292+
293+ standardized_classes_df = validate_and_standardize_calibration_classes_dataframe (
294+ db , score_set , calibration , classes_df
295+ )
296+ variant_classes = variant_classification_df_to_dict (standardized_classes_df )
297+
298+ created_calibration = await create_score_calibration_in_score_set (
299+ db , calibration , user_data .user , variant_classes if classes_file else None
300+ )
194301
195302 db .commit ()
196303 db .refresh (created_calibration )
@@ -201,17 +308,99 @@ async def create_score_calibration_route(
201308@router .put (
202309 "/{urn}" ,
203310 response_model = score_calibration .ScoreCalibrationWithScoreSetUrn ,
204- responses = {404 : {}},
311+ responses = {404 : {}, 422 : {"description" : "Validation Error" }},
312+ openapi_extra = {
313+ "requestBody" : {
314+ "content" : {
315+ "application/json" : {
316+ "schema" : {"$ref" : "#/components/schemas/ScoreCalibrationModify" },
317+ },
318+ "multipart/form-data" : {
319+ "schema" : {
320+ "type" : "object" ,
321+ "properties" : {
322+ "calibration_json" : {
323+ "type" : "string" ,
324+ "description" : "JSON string containing the calibration update data" ,
325+ "example" : '{"title":"Updated Calibration","description":"Updated description","baseline_score":2.0}' ,
326+ },
327+ "classes_file" : {
328+ "type" : "string" ,
329+ "format" : "binary" ,
330+ "description" : "CSV file containing updated variant classifications" ,
331+ },
332+ },
333+ }
334+ },
335+ },
336+ "description" : "Score calibration update data. Can be sent as JSON body or multipart form data" ,
337+ }
338+ },
205339)
206340async def modify_score_calibration_route (
207341 * ,
208342 urn : str ,
209- calibration_update : score_calibration .ScoreCalibrationModify ,
343+ calibration_update : score_calibration .ScoreCalibrationModify = Depends (calibration_modify_loader ),
344+ classes_file : Optional [UploadFile ] = File (
345+ None ,
346+ description = f"CSV file containing variant classifications. This file must contain two columns: '{ calibration_variant_column_name } ' and '{ calibration_class_column_name } '." ,
347+ ),
210348 db : Session = Depends (deps .get_db ),
211349 user_data : UserData = Depends (require_current_user ),
212350) -> ScoreCalibration :
213351 """
214352 Modify an existing score calibration by its URN.
353+
354+ This endpoint supports two different request formats to accommodate various client needs:
355+
356+ ## Method 1: JSON Request Body (application/json)
357+ Send calibration update data as a standard JSON request body. This method is ideal for
358+ modifying calibrations without file uploads.
359+
360+ **Content-Type**: `application/json`
361+
362+ **Example**:
363+ ```json
364+ {
365+ "score_set_urn": "urn:mavedb:0000000X-X-X",
366+ "title": "Updated Calibration Title",
367+ "description": "Updated functional score calibration",
368+ "baseline_score": 1.0
369+ }
370+ ```
371+
372+ ## Method 2: Multipart Form Data (multipart/form-data)
373+ Send calibration update data as JSON in a form field, optionally with file uploads.
374+ This method is required when uploading new classification files.
375+
376+ **Content-Type**: `multipart/form-data`
377+
378+ **Form Fields**:
379+ - `calibration_json` (string, required): JSON string containing the calibration update data
380+ - `classes_file` (file, optional): CSV file containing updated variant classifications
381+
382+ **Example**:
383+ ```bash
384+ curl -X PUT "/api/v1/score-calibrations/{urn}" \\
385+ -H "Authorization: Bearer your-token" \\
386+ -F 'calibration_json={"score_set_urn":"urn:mavedb:0000000X-X-X","title":"My Calibration","description":"Functional score calibration","baseline_score":"1.0"}' \\
387+ -F 'classes_file=@updated_variant_classes.csv'
388+ ```
389+
390+ ## Requirements
391+ - User must have update permission on the calibration
392+ - If changing the score_set_urn, user must have permission on the new score set
393+ - All fields in the update are optional - only provided fields will be modified
394+
395+ ## File Upload Details
396+ The `classes_file` parameter accepts CSV files containing updated variant classification data.
397+ If provided, this will replace the existing classification data for the calibration.
398+ The file should have appropriate headers and follow the expected format for variant
399+ classifications within the associated score set.
400+
401+ ## Response
402+ Returns the updated score calibration with all modifications applied and any new
403+ classification data from the uploaded file.
215404 """
216405 save_to_logging_context ({"requested_resource" : urn })
217406
@@ -241,7 +430,22 @@ async def modify_score_calibration_route(
241430
242431 assert_permission (user_data , item , Action .UPDATE )
243432
244- updated_calibration = await modify_score_calibration (db , item , calibration_update , user_data .user )
433+ if classes_file :
434+ try :
435+ classes_df = csv_data_to_df (classes_file .file , induce_hgvs_cols = False )
436+ except UnicodeDecodeError as e :
437+ raise HTTPException (
438+ status_code = 400 , detail = f"Error decoding file: { e } . Ensure the file has correct values."
439+ )
440+
441+ standardized_classes_df = validate_and_standardize_calibration_classes_dataframe (
442+ db , score_set , calibration_update , classes_df
443+ )
444+ variant_classes = variant_classification_df_to_dict (standardized_classes_df )
445+
446+ updated_calibration = await modify_score_calibration (
447+ db , item , calibration_update , user_data .user , variant_classes if classes_file else None
448+ )
245449
246450 db .commit ()
247451 db .refresh (updated_calibration )
0 commit comments