feat: migrate price_api endpoint to FastAPI (#11999)#12439
feat: migrate price_api endpoint to FastAPI (#11999)#12439shoaib-inamdar wants to merge 5 commits intointernetarchive:masterfrom
Conversation
There was a problem hiding this comment.
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.jsonendpoint andPriceResponsemodel inopenlibrary/fastapi/internal/api.py. - Marked legacy web.py
price_apias deprecated inopenlibrary/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_fallbackverifies 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 withisbn_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)"
| 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")] = "", |
| """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"} | ||
|
|
||
|
|
| if id_type == "isbn_10" and not metadata["betterworldbooks"].get("price"): | ||
| isbn_13 = isbn_10_to_isbn_13(id_) | ||
| if isbn_13: |
| app = FastAPI() | ||
| app.include_router(router) | ||
| client = TestClient(app) | ||
|
|
| 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
4481fbb to
7cba9a2
Compare
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
dd5ac7b to
a24af39
Compare
|
Thank you for the PR! @RayBB is assigned to this PR and currently has:
Possible improvements for this PR
PR triage checklist (maintainers / Pam)
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. |
|
@RayBB , this pr is ready for review |
RayBB
left a comment
There was a problem hiding this comment.
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.
| 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 {} |
There was a problem hiding this comment.
We avoid having business logic in the endpoint definition.
Please call get_price_data
…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
92a2eb6 to
151cf70
Compare
for more information, see https://pre-commit.ci
|
@RayBB , Book 1 : Little Brother (ISBN-10: 0765319853) Book 2 : The Great Gatsby (ISBN-13: 9780743273565) Book 3 : The Alchemist (ISBN-10: 0062316095) One intentional difference to flag:
|
/prices.jsonendpoint toopenlibrary/fastapi/internal/api.pyopenlibrary/tests/fastapi/test_price.pycovering: missing params, isbn lookup, asin lookup, isbn-10 fallback to isbn-13Part of #11999
Closes #11999
Technical
Testing
Screenshot
Stakeholders