Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e84146c
Find all locations of web.ctx.site.get() and whether they are called …
Winterhuli May 7, 2026
0910ea0
replace per-loan site.get with site.get_many
Winterhuli May 8, 2026
8036a9b
fix(api): replace list comprehension web.ctx.site.get(key) with get_m…
WeiGuang-2099 May 9, 2026
c4a1a99
Merge pull request #1 from WeiGuang-2099/Cheng
Winterhuli May 11, 2026
bb14d0b
Delete N+1 queries.txt
Winterhuli May 11, 2026
0af2fef
Merge pull request #2 from WeiGuang-2099/yuheng
Winterhuli May 11, 2026
ef3d073
Batch load works in get_observations using get_many
Hussain5001 May 12, 2026
11fed38
Merge pull request #3 from WeiGuang-2099/hussain
WeiGuang-2099 May 13, 2026
3dd00c0
Batch note page lookups in mybooks
MuchiniGun May 15, 2026
6c42073
Merge pull request #4 from WeiGuang-2099/owen
Hussain5001 May 15, 2026
b97ff2e
Merge branch 'master' into finalsubmit
WeiGuang-2099 May 16, 2026
c0478d9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 16, 2026
53172e8
Update openlibrary/plugins/openlibrary/api.py
WeiGuang-2099 May 19, 2026
7e2dbd8
Implemented the mybooks suggestions:
MuchiniGun May 21, 2026
6d96026
Updating the remaining changes
MuchiniGun May 21, 2026
8286b6a
fix: avoid relying on get_many result order in mybooks
Winterhuli May 22, 2026
a4807d6
Avoid N+1 work fetches in bulk tag and reading log stats
WeiGuang-2099 May 23, 2026
22e2a59
Merge pull request #7 from WeiGuang-2099/yuheng
WeiGuang-2099 May 23, 2026
54ae068
Merge branch 'master' into finalsubmit
WeiGuang-2099 May 23, 2026
17a9045
Merge branch 'finalsubmit' into Cheng
WeiGuang-2099 May 23, 2026
a457177
Merge pull request #8 from WeiGuang-2099/Cheng
WeiGuang-2099 May 23, 2026
85822dc
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion openlibrary/plugins/openlibrary/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,17 @@ def POST(self):
if not edition_keys:
raise web.HTTPError("404 Not Found", {"Content-Type": "application/json"})

editions = [web.ctx.site.get(key) for key in edition_keys]
editions = web.ctx.site.get_many(edition_keys)
if len(editions) > 1:
raise web.HTTPError(
"409 Conflict",
{"Content-Type": "application/json"},
data=json.dumps(
{"error": "Multiple editions associated with given ocaid"}
),
)

edition = editions[0]
Comment thread
WeiGuang-2099 marked this conversation as resolved.
Outdated

# Update records
try:
Expand Down
53 changes: 41 additions & 12 deletions openlibrary/plugins/upstream/mybooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,13 @@ def render_template(self, mb: "MyBooksTemplate") -> TemplateResult:
if mb.me:
myloans = get_loans_of_user(mb.me.key)
loans = web.Storage({"docs": [], "total_results": len(myloans)})
# TODO: should do in one web.ctx.get_many fetch
for loan in myloans:

book_keys = [loan["book"] for loan in myloans]
books = web.ctx.site.get_many(book_keys)

for loan, book in zip(myloans, books):
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.

This looks like it will incorrectly match loan data to editions. There is no guarantee that get_many will return editions in the same order as they are in myloans.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Will update this to avoid relying on get_many() result order.
We can build a books_by_key map from the batched results and match each loan using loan["book"]

# Book will be None if no OL edition exists for the book
if book := web.ctx.site.get(loan["book"]):
if book:
book.loan = loan
loans.docs.append(book)
docs["loans"] = loans
Expand Down Expand Up @@ -575,26 +578,52 @@ def __init__(self, user: User) -> None:

def get_notes(self, limit: int = RESULTS_PER_PAGE, page: int = 1) -> list:
notes = Booknotes.get_notes_grouped_by_work(self.username, limit=limit, page=page)
if not notes:
return notes
Comment on lines +592 to +593
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.

Why was this added?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

My initial idea was just to avoid making empty get_many([]) calls when there are no notes, but I see it's unnecessary since the normal batch flow handles empty notes. I will remove the guard.


work_keys = [f"/works/OL{entry['work_id']}W" for entry in notes]
works = web.ctx.site.get_many(work_keys)
works_by_key = {work.key: work for work in works}
author_keys = list(dict.fromkeys(a.author.key for work in works for a in work.get("authors", [])))
authors_by_key = {author.key: author for author in web.ctx.site.get_many(author_keys)} if author_keys else {}
work_details_by_key = {
work.key: {
"cover_url": (work.get_cover_url("S") or "https://openlibrary.org/static/images/icons/avatar_book-sm.png"),
"title": work.get("title"),
"authors": [authors_by_key[a.author.key].name for a in work.get("authors", []) if a.author.key in authors_by_key],
"first_publish_year": work.first_publish_year or None,
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.

It looks like this was copied from the PatronBooknotes._get_work_details method. Why wasn't that used here?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I had copied it inline because I was trying to batch the author lookup for get_notes(). I'll use _get_work_details() so it can optionally receive a preloaded authors_by_key map.

}
for work in works
}

for entry in notes:
entry["work_key"] = f"/works/OL{entry['work_id']}W"
entry["work"] = self._get_work(entry["work_key"])
entry["work_details"] = self._get_work_details(entry["work"])
edition_ids = list(dict.fromkeys(note["edition_id"] for entry in notes for note in entry["notes"] if note["edition_id"] != Booknotes.NULL_EDITION_VALUE))
edition_keys_by_id = {edition_id: f"/books/OL{edition_id}M" for edition_id in edition_ids}
editions_by_key = {edition.key: edition for edition in web.ctx.site.get_many(list(edition_keys_by_id.values()))} if edition_keys_by_id else {}

for entry, work_key in zip(notes, work_keys):
entry["work_key"] = work_key
entry["work"] = works_by_key[work_key]
entry["work_details"] = work_details_by_key[work_key]
entry["notes"] = {i["edition_id"]: i["notes"] for i in entry["notes"]}
entry["editions"] = {k: web.ctx.site.get(f"/books/OL{k}M") for k in entry["notes"] if k != Booknotes.NULL_EDITION_VALUE}
entry["editions"] = {k: editions_by_key[edition_keys_by_id[k]] for k in entry["notes"] if k != Booknotes.NULL_EDITION_VALUE}
return notes

def get_observations(self, limit: int = RESULTS_PER_PAGE, page: int = 1) -> list:
observations = Observations.get_observations_grouped_by_work(self.username, limit=limit, page=page)

for entry in observations:
entry["work_key"] = f"/works/OL{entry['work_id']}W"
entry["work"] = self._get_work(entry["work_key"])
entry["work_details"] = self._get_work_details(entry["work"])
work_keys = [f"/works/OL{entry['work_id']}W" for entry in observations]
works = web.ctx.site.get_many(work_keys)

for entry, work_key, work in zip(observations, work_keys, works):
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.

works can be in any order, potentially causing observation data to be assigned to the wrong object.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Will update this as well to avoid assuming get_many() preserves input order.
We can build a works_by_key map and look up each observation’s work by work_key

entry["work_key"] = work_key
entry["work"] = work
entry["work_details"] = self._get_work_details(work)

ids = {}
for item in entry["observations"]:
ids[item["observation_type"]] = item["observation_values"]
entry["observations"] = convert_observation_ids(ids)

return observations

def _get_work(self, work_key: str) -> "Work | None":
Expand Down