From 0450c5c22704746cd06f2cf592966634b3f8945b Mon Sep 17 00:00:00 2001 From: harshgupta2125 Date: Sat, 16 May 2026 22:12:07 +0530 Subject: [PATCH 1/8] feat: migrate list seeds json endpoints to fastapi --- openlibrary/fastapi/lists.py | 93 +++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/openlibrary/fastapi/lists.py b/openlibrary/fastapi/lists.py index 916a681e375..5cf3f767b77 100644 --- a/openlibrary/fastapi/lists.py +++ b/openlibrary/fastapi/lists.py @@ -1,14 +1,13 @@ -from __future__ import annotations - from typing import TYPE_CHECKING, Annotated, Literal -from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request, status +from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request, status, Body from infogami.infobase import client from openlibrary.accounts import get_current_user from openlibrary.fastapi.auth import AuthenticatedUser, require_authenticated_user -from openlibrary.plugins.openlibrary.lists import ListEditionsModel, ListSubjectsModel, get_list, get_list_editions, get_list_subjects +from openlibrary.plugins.openlibrary.lists import ListEditionsModel, ListSubjectsModel, get_list, get_list_editions, get_list_subjects, get_list_seeds from openlibrary.plugins.openlibrary.lists import lists_delete as _LegacyListsDelete +from openlibrary.plugins.openlibrary.lists import list_seeds as _LegacyListSeeds from openlibrary.utils.request_context import site, web_ctx_ip if TYPE_CHECKING: @@ -114,8 +113,90 @@ async def lists_json(): pass -async def list_seeds(): - pass +def _get_list_seeds_or_404(key: str) -> dict: + lst = get_list_seeds(key) + if lst is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List or Series not found") + return lst + + +def _update_list_seeds(key: str, payload: dict) -> dict: + s = site.get() + lst = s.get(key) + + if lst is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List or Series not found") + + if not s.can_write(key): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Permission denied.") + + try: + # Pass the payload safely directly to the legacy processor + data = { + "add": payload.get("add", []), + "remove": payload.get("remove", []) + } + return _LegacyListSeeds.process_seeds_update(lst, data, key) + except ValueError as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + + +@router.get("/people/{username}/lists/{olid}/seeds.json") +def list_seeds_json_user(username: UsernamePath, olid: ListOLID) -> dict: + return _get_list_seeds_or_404(f"/people/{username}/lists/{olid}") + + +@router.get("/people/{username}/series/{olid}/seeds.json") +def series_seeds_json_user(username: UsernamePath, olid: ListOLID) -> dict: + return _get_list_seeds_or_404(f"/people/{username}/series/{olid}") + + +@router.get("/lists/{olid}/seeds.json") +def list_seeds_json_public(olid: ListOLID) -> dict: + return _get_list_seeds_or_404(f"/lists/{olid}") + + +@router.get("/series/{olid}/seeds.json") +def series_seeds_json_public(olid: ListOLID) -> dict: + return _get_list_seeds_or_404(f"/series/{olid}") + + +@router.post("/people/{username}/lists/{olid}/seeds.json") +def update_list_seeds_json_user( + username: UsernamePath, + olid: ListOLID, + payload: Annotated[dict, Body(...)], + _: Annotated[AuthenticatedUser, Depends(require_authenticated_user)] +) -> dict: + return _update_list_seeds(f"/people/{username}/lists/{olid}", payload) + + +@router.post("/people/{username}/series/{olid}/seeds.json") +def update_series_seeds_json_user( + username: UsernamePath, + olid: ListOLID, + payload: Annotated[dict, Body(...)], + _: Annotated[AuthenticatedUser, Depends(require_authenticated_user)] +) -> dict: + return _update_list_seeds(f"/people/{username}/series/{olid}", payload) + + +@router.post("/lists/{olid}/seeds.json") +def update_list_seeds_json_public( + olid: ListOLID, + payload: Annotated[dict, Body(...)], + _: Annotated[AuthenticatedUser, Depends(require_authenticated_user)] +) -> dict: + return _update_list_seeds(f"/lists/{olid}", payload) + + +@router.post("/series/{olid}/seeds.json") +def update_series_seeds_json_public( + olid: ListOLID, + payload: Annotated[dict, Body(...)], + _: Annotated[AuthenticatedUser, Depends(require_authenticated_user)] +) -> dict: + return _update_list_seeds(f"/series/{olid}", payload) class GetListEditionsParams: From fd855f9cc9a51f53934091afad0dd0504c0c6a8b Mon Sep 17 00:00:00 2001 From: harshgupta2125 Date: Sat, 16 May 2026 22:12:35 +0530 Subject: [PATCH 2/8] feat: migrate list seeds json endpoints to fastapi --- openlibrary/plugins/openlibrary/lists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlibrary/plugins/openlibrary/lists.py b/openlibrary/plugins/openlibrary/lists.py index 1314f418905..1a8e6a10eb6 100644 --- a/openlibrary/plugins/openlibrary/lists.py +++ b/openlibrary/plugins/openlibrary/lists.py @@ -709,7 +709,7 @@ class list_view_yaml(list_view_json): def get_list_seeds(key): - if lst := web.ctx.site.get(key): + if lst := site.get().get(key): seeds = [seed.dict() for seed in lst.get_seeds()] return { "links": {"self": key + "/seeds", "list": key}, From 1f971e9743a9c8d13c852a550f4e8b19283df1fd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 16:52:31 +0000 Subject: [PATCH 3/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- openlibrary/fastapi/lists.py | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/openlibrary/fastapi/lists.py b/openlibrary/fastapi/lists.py index 5cf3f767b77..c9ed605fb6a 100644 --- a/openlibrary/fastapi/lists.py +++ b/openlibrary/fastapi/lists.py @@ -1,13 +1,13 @@ from typing import TYPE_CHECKING, Annotated, Literal -from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request, status, Body +from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request, status from infogami.infobase import client from openlibrary.accounts import get_current_user from openlibrary.fastapi.auth import AuthenticatedUser, require_authenticated_user -from openlibrary.plugins.openlibrary.lists import ListEditionsModel, ListSubjectsModel, get_list, get_list_editions, get_list_subjects, get_list_seeds -from openlibrary.plugins.openlibrary.lists import lists_delete as _LegacyListsDelete +from openlibrary.plugins.openlibrary.lists import ListEditionsModel, ListSubjectsModel, get_list, get_list_editions, get_list_seeds, get_list_subjects from openlibrary.plugins.openlibrary.lists import list_seeds as _LegacyListSeeds +from openlibrary.plugins.openlibrary.lists import lists_delete as _LegacyListsDelete from openlibrary.utils.request_context import site, web_ctx_ip if TYPE_CHECKING: @@ -123,7 +123,7 @@ def _get_list_seeds_or_404(key: str) -> dict: def _update_list_seeds(key: str, payload: dict) -> dict: s = site.get() lst = s.get(key) - + if lst is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List or Series not found") @@ -132,10 +132,7 @@ def _update_list_seeds(key: str, payload: dict) -> dict: try: # Pass the payload safely directly to the legacy processor - data = { - "add": payload.get("add", []), - "remove": payload.get("remove", []) - } + data = {"add": payload.get("add", []), "remove": payload.get("remove", [])} return _LegacyListSeeds.process_seeds_update(lst, data, key) except ValueError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @@ -163,38 +160,28 @@ def series_seeds_json_public(olid: ListOLID) -> dict: @router.post("/people/{username}/lists/{olid}/seeds.json") def update_list_seeds_json_user( - username: UsernamePath, - olid: ListOLID, - payload: Annotated[dict, Body(...)], - _: Annotated[AuthenticatedUser, Depends(require_authenticated_user)] + username: UsernamePath, olid: ListOLID, payload: Annotated[dict, Body(...)], _: Annotated[AuthenticatedUser, Depends(require_authenticated_user)] ) -> dict: return _update_list_seeds(f"/people/{username}/lists/{olid}", payload) @router.post("/people/{username}/series/{olid}/seeds.json") def update_series_seeds_json_user( - username: UsernamePath, - olid: ListOLID, - payload: Annotated[dict, Body(...)], - _: Annotated[AuthenticatedUser, Depends(require_authenticated_user)] + username: UsernamePath, olid: ListOLID, payload: Annotated[dict, Body(...)], _: Annotated[AuthenticatedUser, Depends(require_authenticated_user)] ) -> dict: return _update_list_seeds(f"/people/{username}/series/{olid}", payload) @router.post("/lists/{olid}/seeds.json") def update_list_seeds_json_public( - olid: ListOLID, - payload: Annotated[dict, Body(...)], - _: Annotated[AuthenticatedUser, Depends(require_authenticated_user)] + olid: ListOLID, payload: Annotated[dict, Body(...)], _: Annotated[AuthenticatedUser, Depends(require_authenticated_user)] ) -> dict: return _update_list_seeds(f"/lists/{olid}", payload) @router.post("/series/{olid}/seeds.json") def update_series_seeds_json_public( - olid: ListOLID, - payload: Annotated[dict, Body(...)], - _: Annotated[AuthenticatedUser, Depends(require_authenticated_user)] + olid: ListOLID, payload: Annotated[dict, Body(...)], _: Annotated[AuthenticatedUser, Depends(require_authenticated_user)] ) -> dict: return _update_list_seeds(f"/series/{olid}", payload) From 837e914e89a0a91dde95c8a9437e306a1d15a597 Mon Sep 17 00:00:00 2001 From: harshgupta2125 Date: Sat, 16 May 2026 22:37:11 +0530 Subject: [PATCH 4/8] fix(linters): remove redundant response_model arguments per FAST001 --- openlibrary/fastapi/lists.py | 16 ++++----- openlibrary/tests/fastapi/test_list_seeds.py | 35 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 openlibrary/tests/fastapi/test_list_seeds.py diff --git a/openlibrary/fastapi/lists.py b/openlibrary/fastapi/lists.py index c9ed605fb6a..a6a276b9c4b 100644 --- a/openlibrary/fastapi/lists.py +++ b/openlibrary/fastapi/lists.py @@ -208,7 +208,7 @@ def _get_editions_response(key: str, params: GetListEditionsParams) -> ListEditi CommonPagination = Annotated[GetListEditionsParams, Depends()] -@router.get("/lists/{olid}/editions.json", response_model=ListEditionsModel) +@router.get("/lists/{olid}/editions.json") def list_editions_json(olid: ListOLID, params: CommonPagination) -> ListEditionsModel: """ Get paginated editions for a public list. @@ -217,7 +217,7 @@ def list_editions_json(olid: ListOLID, params: CommonPagination) -> ListEditions return _get_editions_response(key, params) -@router.get("/people/{username}/lists/{olid}/editions.json", response_model=ListEditionsModel) +@router.get("/people/{username}/lists/{olid}/editions.json") def list_editions_json_people(username: UsernamePath, olid: ListOLID, params: CommonPagination) -> ListEditionsModel: """ Get paginated editions for a specific user's list. @@ -226,7 +226,7 @@ def list_editions_json_people(username: UsernamePath, olid: ListOLID, params: Co return _get_editions_response(key, params) -@router.get("/series/{olid}/editions.json", response_model=ListEditionsModel) +@router.get("/series/{olid}/editions.json") def series_editions_json(olid: ListOLID, params: CommonPagination) -> ListEditionsModel: """ Get paginated editions for a specific series. @@ -235,7 +235,7 @@ def series_editions_json(olid: ListOLID, params: CommonPagination) -> ListEditio return _get_editions_response(key, params) -@router.get("/people/{username}/series/{olid}/editions.json", response_model=ListEditionsModel) +@router.get("/people/{username}/series/{olid}/editions.json") def series_editions_json_people(username: UsernamePath, olid: ListOLID, params: CommonPagination) -> ListEditionsModel: """ Get paginated editions for a specific user's series. @@ -247,7 +247,7 @@ def series_editions_json_people(username: UsernamePath, olid: ListOLID, params: CommonSubjectsLimit = Annotated[int, Query(ge=0, description="Number of subjects to return")] -@router.get("/people/{username}/lists/{olid}/subjects.json", response_model=ListSubjectsModel) +@router.get("/people/{username}/lists/{olid}/subjects.json") def list_subjects_json_user(username: UsernamePath, olid: ListOLID, limit: CommonSubjectsLimit = 20) -> ListSubjectsModel: key = f"/people/{username}/lists/{olid}" if data := get_list_subjects(key, limit): @@ -255,7 +255,7 @@ def list_subjects_json_user(username: UsernamePath, olid: ListOLID, limit: Commo raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List not found") -@router.get("/people/{username}/series/{olid}/subjects.json", response_model=ListSubjectsModel) +@router.get("/people/{username}/series/{olid}/subjects.json") def list_subjects_json_user_series(username: UsernamePath, olid: ListOLID, limit: CommonSubjectsLimit = 20) -> ListSubjectsModel: key = f"/people/{username}/series/{olid}" if data := get_list_subjects(key, limit): @@ -263,7 +263,7 @@ def list_subjects_json_user_series(username: UsernamePath, olid: ListOLID, limit raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List not found") -@router.get("/lists/{olid}/subjects.json", response_model=ListSubjectsModel) +@router.get("/lists/{olid}/subjects.json") def list_subjects_json_public(olid: ListOLID, limit: CommonSubjectsLimit = 20) -> ListSubjectsModel: key = f"/lists/{olid}" if data := get_list_subjects(key, limit): @@ -271,7 +271,7 @@ def list_subjects_json_public(olid: ListOLID, limit: CommonSubjectsLimit = 20) - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List not found") -@router.get("/series/{olid}/subjects.json", response_model=ListSubjectsModel) +@router.get("/series/{olid}/subjects.json") def list_subjects_json_series(olid: ListOLID, limit: CommonSubjectsLimit = 20) -> ListSubjectsModel: key = f"/series/{olid}" if data := get_list_subjects(key, limit): diff --git a/openlibrary/tests/fastapi/test_list_seeds.py b/openlibrary/tests/fastapi/test_list_seeds.py new file mode 100644 index 00000000000..33bd73e955b --- /dev/null +++ b/openlibrary/tests/fastapi/test_list_seeds.py @@ -0,0 +1,35 @@ +import pytest + +def test_get_list_seeds_public_success(fastapi_client, monkeypatch): + """Test fetching seeds for a known public list.""" + def mock_get_list_seeds(key): + return {"entries": [{"url": "/works/OL123W", "type": "work"}], "size": 1} + monkeypatch.setattr("openlibrary.fastapi.lists.get_list_seeds", mock_get_list_seeds) + + response = fastapi_client.get("/lists/OL3L/seeds.json") + assert response.status_code == 200 + data = response.json() + assert "entries" in data + assert data["size"] == 1 + +def test_get_list_seeds_not_found(fastapi_client, monkeypatch): + """Test fetching seeds for a list that does not exist.""" + def mock_get_list_seeds(key): + return None + monkeypatch.setattr("openlibrary.fastapi.lists.get_list_seeds", mock_get_list_seeds) + + response = fastapi_client.get("/lists/OL999999L/seeds.json") + assert response.status_code == 404 + assert response.json() == {"detail": "List or Series not found"} + +def test_post_list_seeds_unauthorized(fastapi_client, mock_site): + """Test that unauthorized users cannot mutate seeds.""" + payload = {"add": ["/works/OL123W"], "remove": []} + response = fastapi_client.post("/lists/OL3L/seeds.json", json=payload) + assert response.status_code in [401, 403] + +def test_post_list_seeds_authorized(fastapi_client, mock_authenticated_user, mock_site): + """Test that a logged-in user successfully hits the permission logic.""" + payload = {"add": ["/works/OL123W"], "remove": []} + response = fastapi_client.post("/lists/OL3L/seeds.json", json=payload) + assert response.status_code in [200, 403, 404] From 75fa1b09f6ae61aa19c37ca76b309392d1158254 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 17:43:01 +0000 Subject: [PATCH 5/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- openlibrary/tests/fastapi/test_list_seeds.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openlibrary/tests/fastapi/test_list_seeds.py b/openlibrary/tests/fastapi/test_list_seeds.py index 33bd73e955b..9abd3b14dc0 100644 --- a/openlibrary/tests/fastapi/test_list_seeds.py +++ b/openlibrary/tests/fastapi/test_list_seeds.py @@ -1,9 +1,9 @@ -import pytest - def test_get_list_seeds_public_success(fastapi_client, monkeypatch): """Test fetching seeds for a known public list.""" + def mock_get_list_seeds(key): return {"entries": [{"url": "/works/OL123W", "type": "work"}], "size": 1} + monkeypatch.setattr("openlibrary.fastapi.lists.get_list_seeds", mock_get_list_seeds) response = fastapi_client.get("/lists/OL3L/seeds.json") @@ -12,22 +12,27 @@ def mock_get_list_seeds(key): assert "entries" in data assert data["size"] == 1 + def test_get_list_seeds_not_found(fastapi_client, monkeypatch): """Test fetching seeds for a list that does not exist.""" + def mock_get_list_seeds(key): return None + monkeypatch.setattr("openlibrary.fastapi.lists.get_list_seeds", mock_get_list_seeds) response = fastapi_client.get("/lists/OL999999L/seeds.json") assert response.status_code == 404 assert response.json() == {"detail": "List or Series not found"} + def test_post_list_seeds_unauthorized(fastapi_client, mock_site): """Test that unauthorized users cannot mutate seeds.""" payload = {"add": ["/works/OL123W"], "remove": []} response = fastapi_client.post("/lists/OL3L/seeds.json", json=payload) assert response.status_code in [401, 403] + def test_post_list_seeds_authorized(fastapi_client, mock_authenticated_user, mock_site): """Test that a logged-in user successfully hits the permission logic.""" payload = {"add": ["/works/OL123W"], "remove": []} From 595c18cb430f49577013a501bb0c991217cba20f Mon Sep 17 00:00:00 2001 From: harshgupta2125 Date: Sat, 16 May 2026 23:51:39 +0530 Subject: [PATCH 6/8] refactor: add deprecated decorator to legacy list seeds endpoint --- openlibrary/plugins/openlibrary/lists.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlibrary/plugins/openlibrary/lists.py b/openlibrary/plugins/openlibrary/lists.py index 1a8e6a10eb6..049163fa5ad 100644 --- a/openlibrary/plugins/openlibrary/lists.py +++ b/openlibrary/plugins/openlibrary/lists.py @@ -707,7 +707,7 @@ class list_view_yaml(list_view_json): encoding = "yml" content_type = "text/yaml" - +deprecated("migrated to fastapi") def get_list_seeds(key): if lst := site.get().get(key): seeds = [seed.dict() for seed in lst.get_seeds()] @@ -717,7 +717,7 @@ def get_list_seeds(key): "entries": seeds, } - +@deprecated("migrated to fastapi") class list_seeds(delegate.page): path = r"((?:/people/[^/]+)?/(?:lists|series)/OL\d+L)/seeds" encoding = "json" From f74f07d0c72d31ee2d0636ed476183cc41543157 Mon Sep 17 00:00:00 2001 From: harshgupta2125 Date: Sun, 17 May 2026 00:31:15 +0530 Subject: [PATCH 7/8] fix: update deprecated decorator message for list seeds migration to fastapi --- openlibrary/plugins/openlibrary/lists.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openlibrary/plugins/openlibrary/lists.py b/openlibrary/plugins/openlibrary/lists.py index 049163fa5ad..c5eaa1e5171 100644 --- a/openlibrary/plugins/openlibrary/lists.py +++ b/openlibrary/plugins/openlibrary/lists.py @@ -707,7 +707,7 @@ class list_view_yaml(list_view_json): encoding = "yml" content_type = "text/yaml" -deprecated("migrated to fastapi") + def get_list_seeds(key): if lst := site.get().get(key): seeds = [seed.dict() for seed in lst.get_seeds()] @@ -717,6 +717,7 @@ def get_list_seeds(key): "entries": seeds, } + @deprecated("migrated to fastapi") class list_seeds(delegate.page): path = r"((?:/people/[^/]+)?/(?:lists|series)/OL\d+L)/seeds" From dfffec9c7d16970259d807ed98b1cf710647c6d8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 20:09:11 +0000 Subject: [PATCH 8/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- openlibrary/fastapi/lists.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openlibrary/fastapi/lists.py b/openlibrary/fastapi/lists.py index ab47aff7611..58adb8775a9 100644 --- a/openlibrary/fastapi/lists.py +++ b/openlibrary/fastapi/lists.py @@ -1,26 +1,24 @@ -from typing import TYPE_CHECKING, Annotated, Literal - -from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request, status from __future__ import annotations from typing import TYPE_CHECKING, Annotated, Any, Literal -from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request, status +from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request, status from pydantic import BaseModel from infogami.infobase import client from openlibrary.accounts import get_current_user from openlibrary.fastapi.auth import AuthenticatedUser, require_authenticated_user -from openlibrary.plugins.openlibrary.lists import ListEditionsModel, ListSubjectsModel, get_list, get_list_editions, get_list_seeds, get_list_subjects -from openlibrary.plugins.openlibrary.lists import list_seeds as _LegacyListSeeds from openlibrary.plugins.openlibrary import lists as legacy_lists from openlibrary.plugins.openlibrary.lists import ( ListEditionsModel, ListSubjectsModel, SpamListError, + get_list, get_list_editions, + get_list_seeds, get_list_subjects, ) +from openlibrary.plugins.openlibrary.lists import list_seeds as _LegacyListSeeds from openlibrary.plugins.openlibrary.lists import lists_delete as _LegacyListsDelete from openlibrary.utils.request_context import site, web_ctx_ip