From 9b18c88852cad893468029969892254fd9941e02 Mon Sep 17 00:00:00 2001 From: jakeross Date: Mon, 3 Nov 2025 17:53:27 -0700 Subject: [PATCH 1/2] fix: make lexicon read-only via API by deprecating modification endpoints --- api/lexicon.py | 39 +++++++++++++++++++++++++++++++++++---- tests/test_lexicon.py | 16 ++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/api/lexicon.py b/api/lexicon.py index 739d97925..1c8ada798 100644 --- a/api/lexicon.py +++ b/api/lexicon.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # =============================================================================== -from fastapi import APIRouter, Query +from fastapi import APIRouter, Query, HTTPException, Depends from fastapi_pagination.ext.sqlalchemy import paginate from sqlalchemy import select, func from sqlalchemy.exc import ProgrammingError @@ -93,12 +93,18 @@ def database_error_handler( raise PydanticStyleException(status_code=HTTP_409_CONFLICT, detail=[detail]) +def disabled_endpoint(): + raise HTTPException(status_code=404, detail="Endpoint disabled") + + # POST ========================================================================= @router.post( "/category", status_code=HTTP_201_CREATED, + deprecated=True, + dependencies=[Depends(disabled_endpoint)], ) async def add_category( category_data: CreateLexiconCategory, @@ -115,6 +121,8 @@ async def add_category( "/term", summary="Add term", status_code=HTTP_201_CREATED, + deprecated=True, + dependencies=[Depends(disabled_endpoint)], ) async def add_term( term_data: CreateLexiconTerm, @@ -134,6 +142,8 @@ async def add_term( "/triple", summary="Add triple", status_code=HTTP_201_CREATED, + deprecated=True, + dependencies=[Depends(disabled_endpoint)], ) async def add_triple( triple_data: CreateLexiconTriple, @@ -150,7 +160,12 @@ async def add_triple( # PATCH ======================================================================== -@router.patch("/term/{term_id}", status_code=HTTP_200_OK) +@router.patch( + "/term/{term_id}", + status_code=HTTP_200_OK, + deprecated=True, + dependencies=[Depends(disabled_endpoint)], +) async def update_lexicon_term( term_id: int, term_data: UpdateLexiconTerm, @@ -161,7 +176,12 @@ async def update_lexicon_term( return model_patcher(session, LexiconTerm, term_id, term_data, user=user) -@router.patch("/category/{category_id}", status_code=HTTP_200_OK) +@router.patch( + "/category/{category_id}", + status_code=HTTP_200_OK, + deprecated=True, + dependencies=[Depends(disabled_endpoint)], +) async def update_lexicon_category( category_id: int, category_data: UpdateLexiconCategory, @@ -173,7 +193,12 @@ async def update_lexicon_category( ) -@router.patch("/triple/{triple_id}", status_code=HTTP_200_OK) +@router.patch( + "/triple/{triple_id}", + status_code=HTTP_200_OK, + deprecated=True, + dependencies=[Depends(disabled_endpoint)], +) async def update_lexicon_triple( triple_id: int, triple_data: UpdateLexiconTriple, @@ -282,6 +307,8 @@ async def get_lexicon_triple( "/term/{term_id}", summary="Delete a lexicon term by ID", status_code=HTTP_204_NO_CONTENT, + deprecated=True, + dependencies=[Depends(disabled_endpoint)], ) async def delete_lexicon_term( session: session_dependency, user: lexicon_admin_dependency, term_id: int @@ -293,6 +320,8 @@ async def delete_lexicon_term( "/category/{category_id}", summary="Delete a lexicon category by ID", status_code=HTTP_204_NO_CONTENT, + deprecated=True, + dependencies=[Depends(disabled_endpoint)], ) async def delete_lexicon_category( session: session_dependency, user: lexicon_admin_dependency, category_id: int @@ -304,6 +333,8 @@ async def delete_lexicon_category( "/triple/{triple_id}", summary="Delete a lexicon triple by ID", status_code=HTTP_204_NO_CONTENT, + deprecated=True, + dependencies=[Depends(disabled_endpoint)], ) async def delete_lexicon_triple( session: session_dependency, user: lexicon_admin_dependency, triple_id: int diff --git a/tests/test_lexicon.py b/tests/test_lexicon.py index 4e9db24b3..9b072d42a 100644 --- a/tests/test_lexicon.py +++ b/tests/test_lexicon.py @@ -83,6 +83,7 @@ def test_add_lexicon_term_with_new_categories(): cleanup_post_test(LexiconCategory, data["categories"][0]["id"]) +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_add_lexicon_term_with_existing_categories(): payload = { "term": "test_term_existing_categories", @@ -108,6 +109,7 @@ def test_add_lexicon_term_with_existing_categories(): cleanup_post_test(LexiconTerm, data["id"]) +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_add_lexicon_category(): payload = {"name": "test category name", "description": "test category description"} response = client.post("/lexicon/category", json=payload) @@ -223,6 +225,7 @@ def test_add_lexicon_triple_existing_terms(lexicon_term, second_lexicon_term): # PATCH tests ================================================================== +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_patch_term(lexicon_term): payload = {"term": "patched term", "definition": "patched definition"} response = client.patch(f"/lexicon/term/{lexicon_term.id}", json=payload) @@ -234,6 +237,7 @@ def test_patch_term(lexicon_term): cleanup_patch_test(LexiconTerm, payload, lexicon_term) +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_patch_term_404_not_found(lexicon_term): bad_id = 99999 payload = {"term": "patched term", "definition": "patched definition"} @@ -243,6 +247,7 @@ def test_patch_term_404_not_found(lexicon_term): assert data["detail"] == f"LexiconTerm with ID {bad_id} not found." +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_patch_category(lexicon_category): payload = {"name": "patched name", "description": "patched description"} response = client.patch(f"/lexicon/category/{lexicon_category.id}", json=payload) @@ -254,6 +259,7 @@ def test_patch_category(lexicon_category): cleanup_patch_test(LexiconCategory, payload, lexicon_category) +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_patch_category_404_not_found(lexicon_category): bad_id = 99999 payload = {"name": "patched name", "definition": "patched definition"} @@ -263,6 +269,7 @@ def test_patch_category_404_not_found(lexicon_category): assert data["detail"] == f"LexiconCategory with ID {bad_id} not found." +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_patch_triple(lexicon_triple, third_lexicon_term, fourth_lexicon_term): payload = { "subject": third_lexicon_term.term, @@ -279,6 +286,7 @@ def test_patch_triple(lexicon_triple, third_lexicon_term, fourth_lexicon_term): cleanup_patch_test(LexiconTriple, payload, lexicon_triple) +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_patch_triple_404_not_found( lexicon_triple, third_lexicon_term, fourth_lexicon_term ): @@ -294,6 +302,7 @@ def test_patch_triple_404_not_found( assert data["detail"] == f"LexiconTriple with ID {bad_id} not found." +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_patch_triple_409_bad_subject(lexicon_triple, third_lexicon_term): bad_subject = "nonexistent subject" payload = { @@ -310,6 +319,7 @@ def test_patch_triple_409_bad_subject(lexicon_triple, third_lexicon_term): assert data["detail"][0]["input"] == {"subject": bad_subject} +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_patch_triple_409_bad_object(lexicon_triple, third_lexicon_term): bad_object = "nonexistent object" payload = { @@ -458,6 +468,7 @@ def test_get_lexicon_triple_by_id_404_not_found(lexicon_triple): # DELETE tests ================================================================= +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_delete_lexicon_term(second_lexicon_term): response = client.delete(f"/lexicon/term/{second_lexicon_term.id}") assert response.status_code == 204 @@ -469,6 +480,7 @@ def test_delete_lexicon_term(second_lexicon_term): assert data["detail"] == f"LexiconTerm with ID {second_lexicon_term.id} not found." +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_delete_lexicon_term_404_not_found(second_lexicon_term): bad_id = 999999 response = client.delete(f"/lexicon/term/{bad_id}") @@ -477,6 +489,7 @@ def test_delete_lexicon_term_404_not_found(second_lexicon_term): assert data["detail"] == f"LexiconTerm with ID {bad_id} not found." +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_delete_lexicon_category(second_lexicon_category): response = client.delete(f"/lexicon/category/{second_lexicon_category.id}") assert response.status_code == 204 @@ -491,6 +504,7 @@ def test_delete_lexicon_category(second_lexicon_category): ) +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_delete_lexicon_category_404_not_found(second_lexicon_category): bad_id = 999999 response = client.delete(f"/lexicon/category/{bad_id}") @@ -499,6 +513,7 @@ def test_delete_lexicon_category_404_not_found(second_lexicon_category): assert data["detail"] == f"LexiconCategory with ID {bad_id} not found." +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_delete_lexicon_triple(second_lexicon_triple): response = client.delete(f"/lexicon/triple/{second_lexicon_triple.id}") assert response.status_code == 204 @@ -512,6 +527,7 @@ def test_delete_lexicon_triple(second_lexicon_triple): ) +@pytest.mark.skip(reason="Lexicon is readonly via API now") def test_delete_lexicon_triple_404_not_found(second_lexicon_triple): bad_id = 999999 response = client.delete(f"/lexicon/triple/{bad_id}") From f28f7d5d2c9b0446fb39cd812226d4b317a04e41 Mon Sep 17 00:00:00 2001 From: jakeross Date: Tue, 4 Nov 2025 12:54:57 -0700 Subject: [PATCH 2/2] fix: update disabled endpoint to return 410 status code for better clarity --- api/lexicon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/lexicon.py b/api/lexicon.py index 1c8ada798..933fb7a08 100644 --- a/api/lexicon.py +++ b/api/lexicon.py @@ -94,7 +94,7 @@ def database_error_handler( def disabled_endpoint(): - raise HTTPException(status_code=404, detail="Endpoint disabled") + raise HTTPException(status_code=410, detail="Endpoint disabled") # POST =========================================================================