From 8f40fab588284459ba8898d0a6df347fd2feba9a Mon Sep 17 00:00:00 2001 From: Benjamin Capodanno Date: Thu, 19 Mar 2026 11:32:58 -0700 Subject: [PATCH 1/2] feat: add endpoint for user created calibrations --- src/mavedb/routers/score_calibrations.py | 23 +++- tests/routers/test_score_calibrations.py | 130 +++++++++++++++++++++++ 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/src/mavedb/routers/score_calibrations.py b/src/mavedb/routers/score_calibrations.py index e56a0935..de8618a9 100644 --- a/src/mavedb/routers/score_calibrations.py +++ b/src/mavedb/routers/score_calibrations.py @@ -6,7 +6,7 @@ from mavedb import deps from mavedb.lib.authentication import get_current_user -from mavedb.lib.authorization import require_current_user_with_email +from mavedb.lib.authorization import require_current_user, require_current_user_with_email from mavedb.lib.flexible_model_loader import json_or_form_loader from mavedb.lib.logging import LoggedRoute from mavedb.lib.logging.context import ( @@ -54,6 +54,27 @@ ) +@router.get( + "/me", + status_code=200, + response_model=list[score_calibration.ScoreCalibrationWithScoreSetUrn], + responses={401: {}, 403: {}}, + summary="List my calibrations", +) +def list_my_calibrations( + *, + db: Session = Depends(deps.get_db), + user_data: UserData = Depends(require_current_user), +) -> list[ScoreCalibration]: + """List all score calibrations created by the current user.""" + return ( + db.query(ScoreCalibration) + .filter(ScoreCalibration.created_by_id == user_data.user.id) + .options(selectinload(ScoreCalibration.score_set).selectinload(ScoreSet.contributors)) + .all() + ) + + @router.get( "/{urn}", response_model=score_calibration.ScoreCalibrationWithScoreSetUrn, diff --git a/tests/routers/test_score_calibrations.py b/tests/routers/test_score_calibrations.py index 63c5c43c..fe1aeba7 100644 --- a/tests/routers/test_score_calibrations.py +++ b/tests/routers/test_score_calibrations.py @@ -4884,3 +4884,133 @@ def test_cannot_create_score_calibration_without_email( assert response.status_code == 403 assert "email" in response.json()["detail"].lower() + + +########################################################### +# GET /score-calibrations/me +########################################################### + + +def test_anonymous_user_cannot_list_my_calibrations(client, setup_router_db, anonymous_app_overrides): + with DependencyOverrider(anonymous_app_overrides): + response = client.get("/api/v1/score-calibrations/me") + + assert response.status_code == 401 + + +def test_authenticated_user_with_no_calibrations_returns_empty_list(client, setup_router_db): + response = client.get("/api/v1/score-calibrations/me") + + assert response.status_code == 200 + assert response.json() == [] + + +@pytest.mark.parametrize( + "mock_publication_fetch", + [ + [ + {"dbName": "PubMed", "identifier": TEST_PUBMED_IDENTIFIER}, + {"dbName": "bioRxiv", "identifier": TEST_BIORXIV_IDENTIFIER}, + ] + ], + indirect=["mock_publication_fetch"], +) +def test_authenticated_user_sees_own_calibrations( + client, setup_router_db, mock_publication_fetch, session, data_provider, data_files +): + experiment = create_experiment(client) + score_set = create_seq_score_set_with_mapped_variants( + client, + session, + data_provider, + experiment["urn"], + data_files / "scores.csv", + ) + calibration = create_test_score_calibration_in_score_set_via_client( + client, score_set["urn"], deepcamelize(TEST_BRNICH_SCORE_CALIBRATION_RANGE_BASED) + ) + + response = client.get("/api/v1/score-calibrations/me") + + assert response.status_code == 200 + calibrations = response.json() + assert len(calibrations) == 1 + assert calibrations[0]["urn"] == calibration["urn"] + assert calibrations[0]["scoreSetUrn"] == score_set["urn"] + + +@pytest.mark.parametrize( + "mock_publication_fetch", + [ + [ + {"dbName": "PubMed", "identifier": TEST_PUBMED_IDENTIFIER}, + {"dbName": "bioRxiv", "identifier": TEST_BIORXIV_IDENTIFIER}, + ] + ], + indirect=["mock_publication_fetch"], +) +def test_user_does_not_see_other_users_calibrations( + client, setup_router_db, mock_publication_fetch, session, data_provider, data_files, extra_user_app_overrides +): + experiment = create_experiment(client) + score_set = create_seq_score_set_with_mapped_variants( + client, + session, + data_provider, + experiment["urn"], + data_files / "scores.csv", + ) + create_test_score_calibration_in_score_set_via_client( + client, score_set["urn"], deepcamelize(TEST_BRNICH_SCORE_CALIBRATION_RANGE_BASED) + ) + + with DependencyOverrider(extra_user_app_overrides): + response = client.get("/api/v1/score-calibrations/me") + + assert response.status_code == 200 + assert response.json() == [] + + +@pytest.mark.parametrize( + "mock_publication_fetch", + [ + [ + {"dbName": "PubMed", "identifier": TEST_PUBMED_IDENTIFIER}, + {"dbName": "bioRxiv", "identifier": TEST_BIORXIV_IDENTIFIER}, + ] + ], + indirect=["mock_publication_fetch"], +) +def test_user_sees_calibrations_across_multiple_score_sets( + client, setup_router_db, mock_publication_fetch, session, data_provider, data_files +): + experiment = create_experiment(client) + score_set_1 = create_seq_score_set_with_mapped_variants( + client, + session, + data_provider, + experiment["urn"], + data_files / "scores.csv", + ) + score_set_2 = create_seq_score_set_with_mapped_variants( + client, + session, + data_provider, + experiment["urn"], + data_files / "scores.csv", + ) + cal_1 = create_test_score_calibration_in_score_set_via_client( + client, score_set_1["urn"], deepcamelize(TEST_BRNICH_SCORE_CALIBRATION_RANGE_BASED) + ) + cal_2 = create_test_score_calibration_in_score_set_via_client( + client, score_set_2["urn"], deepcamelize(TEST_BRNICH_SCORE_CALIBRATION_RANGE_BASED) + ) + + response = client.get("/api/v1/score-calibrations/me") + + assert response.status_code == 200 + calibrations = response.json() + assert len(calibrations) == 2 + returned_urns = {c["urn"] for c in calibrations} + assert cal_1["urn"] in returned_urns + assert cal_2["urn"] in returned_urns From e11911d4724c95b7495257370ec1e520d18f41ea Mon Sep 17 00:00:00 2001 From: Benjamin Capodanno Date: Sun, 22 Mar 2026 18:24:00 +1100 Subject: [PATCH 2/2] fix: use shared error responses on calibration endpoints --- src/mavedb/routers/score_calibrations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mavedb/routers/score_calibrations.py b/src/mavedb/routers/score_calibrations.py index de8618a9..e8b31478 100644 --- a/src/mavedb/routers/score_calibrations.py +++ b/src/mavedb/routers/score_calibrations.py @@ -31,6 +31,7 @@ from mavedb.models.score_calibration import ScoreCalibration from mavedb.models.score_calibration_functional_classification import ScoreCalibrationFunctionalClassification from mavedb.models.score_set import ScoreSet +from mavedb.routers.shared import ACCESS_CONTROL_ERROR_RESPONSES, PUBLIC_ERROR_RESPONSES from mavedb.view_models import score_calibration logger = logging.getLogger(__name__) @@ -38,7 +39,7 @@ router = APIRouter( prefix="/api/v1/score-calibrations", tags=["Score Calibrations"], - responses={404: {"description": "Not found"}}, + responses={**PUBLIC_ERROR_RESPONSES}, route_class=LoggedRoute, ) @@ -58,7 +59,7 @@ "/me", status_code=200, response_model=list[score_calibration.ScoreCalibrationWithScoreSetUrn], - responses={401: {}, 403: {}}, + responses={**ACCESS_CONTROL_ERROR_RESPONSES}, summary="List my calibrations", ) def list_my_calibrations(