Skip to content

feat: migrate price_api endpoint to FastAPI (#11999)#12439

Open
shoaib-inamdar wants to merge 5 commits intointernetarchive:masterfrom
shoaib-inamdar:11999/feature/migrate-price-api-to-fastapi
Open

feat: migrate price_api endpoint to FastAPI (#11999)#12439
shoaib-inamdar wants to merge 5 commits intointernetarchive:masterfrom
shoaib-inamdar:11999/feature/migrate-price-api-to-fastapi

Conversation

@shoaib-inamdar
Copy link
Copy Markdown
Contributor

  • Add async GET /prices.json endpoint to openlibrary/fastapi/internal/api.py
  • Uses get_betterworldbooks_metadata_async directly (no sync bridge needed)
  • Falls back from ISBN-10 to ISBN-13 when BWB returns no price
  • Add PriceResponse Pydantic model for schema documentation
  • Mark legacy web.py price_api class as @deprecated('migrated to fastapi')
  • Add 4 tests in openlibrary/tests/fastapi/test_price.py covering: missing params, isbn lookup, asin lookup, isbn-10 fallback to isbn-13

Part of #11999

Closes #11999

Technical

Testing

Screenshot

Stakeholders

Copilot AI review requested due to automatic review settings April 25, 2026 18:26
@github-project-automation github-project-automation Bot moved this to Waiting Review/Merge from Staff in Ray's Project Apr 25, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates the legacy web.py /prices internal endpoint to FastAPI by introducing an async /prices.json route that fetches vendor pricing data and adding initial test coverage.

Changes:

  • Added FastAPI GET /prices.json endpoint and PriceResponse model in openlibrary/fastapi/internal/api.py.
  • Marked legacy web.py price_api as deprecated in openlibrary/plugins/openlibrary/api.py.
  • Added new FastAPI tests for the pricing endpoint in openlibrary/tests/fastapi/test_price.py.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
openlibrary/fastapi/internal/api.py Adds PriceResponse and the new async /prices.json pricing route using Amazon + BetterWorldBooks metadata.
openlibrary/plugins/openlibrary/api.py Deprecates the legacy web.py price_api handler.
openlibrary/tests/fastapi/test_price.py Adds tests covering missing params, ISBN/ASIN lookup, and ISBN-10→ISBN-13 BWB fallback logic.
Comments suppressed due to low confidence (1)

openlibrary/tests/fastapi/test_price.py:76

  • test_price_api_isbn10_bwb_fallback verifies that the second BWB response is used, but it doesn’t assert that the fallback call was actually made with the computed ISBN-13. As written, the test would still pass even if the endpoint retried with the same ISBN-10 or an incorrect conversion. Consider asserting the awaited call args (e.g., first call with the normalized ISBN-10, second with isbn_10_to_isbn_13(...)).
    bwb_mock = AsyncMock(side_effect=[{"price": None}, {"price": "$3.00 (used)"}])
    monkeypatch.setattr(
        "openlibrary.fastapi.internal.api.get_betterworldbooks_metadata_async",
        bwb_mock,
    )

    resp = client.get("/prices.json?isbn=0765319853")  # isbn-10
    assert resp.status_code == 200
    data = resp.json()
    # The fallback isbn-13 result should be used
    assert data["betterworldbooks"]["price"] == "$3.00 (used)"

Comment on lines +291 to +295
response_model=PriceResponse,
description="Returns pricing data for a book from Amazon and BetterWorldBooks.",
)
async def price_api(
isbn: Annotated[str, Query(description="ISBN-10 or ISBN-13")] = "",
Comment on lines +278 to +288
"""Response model for the /prices.json endpoint."""

amazon: dict = Field(default_factory=dict, description="Amazon pricing data")
betterworldbooks: dict = Field(default_factory=dict, description="BetterWorldBooks pricing data")
key: str | None = Field(None, description="OpenLibrary edition key, if found")
ocaid: str | None = Field(None, description="Internet Archive OCAID, if found")
error: str | None = Field(None, description="Error message if request is invalid")

model_config = {"extra": "allow"}


Comment thread openlibrary/fastapi/internal/api.py Outdated
Comment on lines +316 to +318
if id_type == "isbn_10" and not metadata["betterworldbooks"].get("price"):
isbn_13 = isbn_10_to_isbn_13(id_)
if isbn_13:
Comment thread openlibrary/tests/fastapi/test_price.py Outdated
Comment on lines +9 to +12
app = FastAPI()
app.include_router(router)
client = TestClient(app)

Comment on lines +291 to +293
response_model=PriceResponse,
description="Returns pricing data for a book from Amazon and BetterWorldBooks.",
)
- Add async GET /prices.json endpoint to openlibrary/fastapi/internal/api.py
- Uses get_betterworldbooks_metadata_async directly (no sync bridge needed)
- Falls back from ISBN-10 to ISBN-13 when BWB returns no price
- Add PriceResponse Pydantic model for schema documentation
- Mark legacy web.py price_api class as @deprecated('migrated to fastapi')
- Add 4 tests in openlibrary/tests/fastapi/test_price.py covering:
  missing params, isbn lookup, asin lookup, isbn-10 fallback to isbn-13

Part of internetarchive#11999
Pre-work: internetarchive#12404 (make betterworldbooks async)
- Add Any to typing imports for proper dict[str, Any] annotation
- Annotate �wb_data as dict[str, Any] | None to fix mypy var-annotated error
- Use dict[str, Any] for PriceResponse amazon/betterworldbooks fields
- Remove unused key/ocaid fields from PriceResponse (not set by this endpoint)
- Add 
esponse_model_exclude_none=True so null error field is omitted from ok responses
- Simplify isbn-10 BWB fallback using walrus operator (single compound condition)
- Refactor test module: replace module-level globals with a pytest.fixture for TestClient
@shoaib-inamdar shoaib-inamdar force-pushed the 11999/feature/migrate-price-api-to-fastapi branch from 4481fbb to 7cba9a2 Compare April 25, 2026 18:40
ruff SIM102: flatten nested if into a single compound condition using �nd
mypy [assignment]: import BetterWorldBooksMetadata/BetterWorldBooksMetadataError
TypedDicts and annotate bwb_data with the exact return union type;
change else {} to else None to avoid dict[Never,Never] mismatch
@shoaib-inamdar shoaib-inamdar force-pushed the 11999/feature/migrate-price-api-to-fastapi branch from dd5ac7b to a24af39 Compare April 25, 2026 18:46
@mekarpeles mekarpeles added the Needs: Submitter Input Waiting on input from the creator of the issue/pr [managed] label Apr 25, 2026
@mekarpeles
Copy link
Copy Markdown
Member

Thank you for the PR!

@RayBB is assigned to this PR and currently has:

  • 7 open PR(s) of equal or higher priority to review first

Possible improvements for this PR

  • The Testing section of the PR description is empty. Even though automated tests are included, please add a brief note on how you verified the endpoint works in practice — for example, a curl command, a sample request/response, or manual steps a reviewer can follow to confirm the behavior.
PR triage checklist (maintainers / Pam)
  • PR description — not empty; explains what the change does and how to verify it
  • References an issue — PR body contains a #NNN reference
    • Linked issue is triaged — has a Priority: * label (not just Needs: Triage)
    • Linked issue is assigned — has at least one assignee
  • Commit history clean — no WIP/fixup/conflict noise; commit messages are meaningful
  • CI passing — no failing check-runs
  • Test cases present — if the change touches substantive logic, test coverage exists or is explained
  • Proof of testing — PR body includes a description of what was tested, a screenshot, or a video

Note

This comment was automatically generated by Pam, Open Library's Project AI Manager, on behalf of @mekarpeles. Pam is designed to provide status visibility, perform basic project management functions and relevant codebase research, and provide actionable feedback so contributors aren't left waiting.

@shoaib-inamdar
Copy link
Copy Markdown
Contributor Author

@RayBB , this pr is ready for review

Copy link
Copy Markdown
Collaborator

@RayBB RayBB left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok it's deploying to testing now.

Please compare testing and production for several books and ensure results are identical. And then share the links of what you tested with

https://testing.openlibrary.org/_fast/prices.json

The _fast is how to access fastapi routes directly in testing.

Comment thread openlibrary/fastapi/internal/api.py Outdated
Comment on lines +303 to +320
if not (isbn or asin):
return {"error": "isbn or asin required"}

id_ = asin or normalize_isbn(isbn) or isbn
id_type = "asin" if asin else "isbn_" + ("13" if len(id_) == 13 else "10")

bwb_data: BetterWorldBooksMetadata | BetterWorldBooksMetadataError | None = (
await get_betterworldbooks_metadata_async(id_) if id_type.startswith("isbn_") else None
)

metadata: dict[str, Any] = {
"amazon": get_amazon_metadata(id_, id_type=id_type[:4]) or {},
"betterworldbooks": bwb_data or {},
}

# Fallback: if isbn_10 gave no BWB price, retry with isbn_13
if id_type == "isbn_10" and not metadata["betterworldbooks"].get("price") and (isbn_13 := isbn_10_to_isbn_13(id_)):
metadata["betterworldbooks"] = (await get_betterworldbooks_metadata_async(isbn_13)) or {}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We avoid having business logic in the endpoint definition.

Please call get_price_data

@github-actions github-actions Bot added the Needs: Response Issues which require feedback from lead label Apr 26, 2026
…dpoint

Per maintainer feedback: avoid business logic in the endpoint definition.

- Add async get_price_data(isbn, asin) with full docstring containing the
  fetch/fallback logic (BWB isbn-10 -> isbn-13 retry, Amazon lookup)
- price_api endpoint becomes a thin wrapper: validates input then calls
  get_price_data()
- Update tests: add test_price_api_delegates_to_get_price_data to verify
  the endpoint delegation, and convert the 3 logic tests to call
  get_price_data() directly with @pytest.mark.asyncio
@shoaib-inamdar shoaib-inamdar force-pushed the 11999/feature/migrate-price-api-to-fastapi branch from 92a2eb6 to 151cf70 Compare April 26, 2026 10:41
@github-actions github-actions Bot removed the Needs: Submitter Input Waiting on input from the creator of the issue/pr [managed] label Apr 26, 2026
@shoaib-inamdar
Copy link
Copy Markdown
Contributor Author

@RayBB ,
Tested 3 books comparing /_fast/prices.json (FastAPI, testing) vs /prices.json (legacy, production):

Book 1 : Little Brother (ISBN-10: 0765319853)
🆕 FastAPI (testing): https://testing.openlibrary.org/_fast/prices.json?isbn=0765319853
🔁 Legacy (production): https://openlibrary.org/prices.json?isbn=0765319853

Book 2 : The Great Gatsby (ISBN-13: 9780743273565)
🆕 FastAPI (testing): https://testing.openlibrary.org/_fast/prices.json?isbn=9780743273565
🔁 Legacy (production): https://openlibrary.org/prices.json?isbn=9780743273565

Book 3 : The Alchemist (ISBN-10: 0062316095)
🆕 FastAPI (testing): https://testing.openlibrary.org/_fast/prices.json?isbn=0062316095
🔁 Legacy (production): https://openlibrary.org/prices.json?isbn=0062316095

One intentional difference to flag:

  • The new FastAPI endpoint omits the "key" field (e.g. /books/OL10936455M) that the legacy endpoint returns. This is because key requires a web.ctx.site lookup which is not available in the FastAPI context. Please confirm if this is acceptable, or if we should add it back a different way.

  • The betterworldbooks data (price, url, isbn) matches ✅.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs: Response Issues which require feedback from lead

Projects

Status: Waiting Review/Merge from Staff

Development

Successfully merging this pull request may close these issues.

Migrate internal endpoints to FastAPI

4 participants