Phase 2: Work Migration — Subject Classification Pipeline - #41
Conversation
…IME SKIP tag creation test script'
| # Phase 1 - Scan dump for matched work keys | ||
| # --------------------------------------------------------------------------- | ||
| def phase1(dump_path: str, tag_type: str): |
There was a problem hiding this comment.
We could name according to the functionality def scan_dump_for_matched_keys
There was a problem hiding this comment.
get_work_keys_of_migration_candidates_from_monthly_dump
| #--------------------------------------------------------------------------- | ||
| # Phase 2 - Fetch,. migrate, save | ||
| # --------------------------------------------------------------------------- | ||
| def phase2(keys_path: str, tag_type: str, dry_run: bool, batch_size: int = 50): |
There was a problem hiding this comment.
Q: do we want dry_run to have some default kwarg value? e.g. dry_run: bool = True
|
For reference, we have a similar pipeline for fixing or updating entries in our Reading Log database so that every few days we ensure that, when multiple works get merged, reading logs are updated to point to the latest correct work: It similarly uses batches, API calls and other patterns that may be useful for reference (this is in no way implying our PR is doing anything wrong, it's just useful context) |
| cfg = Config().get_config() | ||
| s3 = cfg["s3"] | ||
| ol = OpenLibrary(credentials=Credentials(access=s3[0], secret=s3[1])) | ||
| ol.session.headers.update({"Content-Type": "application/json"}) |
There was a problem hiding this comment.
I believe this session is now handled by the ol object (it should now set the content type for us internally)
| for i, key in enumerate(keys): | ||
| # Fetch the work JSON from Open Library | ||
| try: | ||
| resp = requests.get(f"https://openlibrary.org{key}.json") | ||
| resp.raise_for_status() | ||
| work = resp.json() | ||
| except Exception as e: | ||
| logger.error(f"Error fetching {key}: {e}") | ||
| continue |
There was a problem hiding this comment.
This may actually be quite slow and produce a lot of traffic for the API;
We have an endpoint called /query.json (to fetch all 50 docs at once)
|
|
||
| if dry_run: | ||
| # Preview mode: log what we would write | ||
| logger.info(f"{key}: {tag_type} = {tag_keys}") |
There was a problem hiding this comment.
Q: Do we want to run this either way? dry_run
| r = ol.save_many(batch, f"backfill {tag_type} tags from subject mapping") | ||
| if r.status_code == 200: | ||
| updated += len(batch) | ||
| else: | ||
| logger.error(f"save_many error: {r.status_code} {r.text[:200]}") | ||
| batch = [] |
There was a problem hiding this comment.
This block can likely be DRY'd up for the batch case and the final case (i.e. share code)
| cfg = Config().get_config() | ||
| s3 = cfg["s3"] | ||
| ol = OpenLibrary(credentials=Credentials(access=s3[0], secret=s3[1])) | ||
| ol.session.headers.update({"Content-Type": "application/json"}) |
There was a problem hiding this comment.
We might want to DRY up the fetching of an authenticated ol object (i.e. just have a single function rather than repeat this across files)
e.g.
def get_ol_session():
cfg = Config().get_config()
s3 = cfg["s3"]
ol = OpenLibrary(credentials=Credentials(access=s3[0], secret=s3[1]))
ol.session.headers.update({"Content-Type": "application/json"})
return ol
| cfg = Config().get_config() | ||
| s3 = cfg["s3"] | ||
| ol = OpenLibrary(credentials=Credentials(access=s3[0], secret=s3[1])) | ||
| ol.session.headers.update({"Content-Type": "application/json"}) |
There was a problem hiding this comment.
| ol.session.headers.update({"Content-Type": "application/json"}) |
This should now be merged into olclient
Address PR #41 review: shared auth helper, remove olclient Content-Type, rename functions
Part of the Phase 2 work for #14.
What this PR adds
Three new files that form the core of the tag migration pipeline:
tags/utils.py— slug_to_tag_key() helperscripts/migrate_work.py— WorkMigrator class + CLIbackfill_genre_tags.py— Two-phase batch backfill script (Phase 1: dump scan, Phase 2: fetch + migrate + save)Together they implement the mapping chain that transforms a Work's messy subject strings into clean OL Tag keys.
The mapping chain
subject "science fiction"
→ mappings.json → slug "sci-fi" (messy → canonical)
→ vocabulary.json → key "/tags/OL179T" (slug → Tag key)
→ work.genres → ["/tags/OL179T"] (to be written)
File 1: tags/utils.py
A pure function that reads a type's vocabulary.json and returns the Tag key for a given slug.
How to test:
Output:
File 2: scripts/migrate_work.py
A
class WorkMigratorthat loads all controlled type mappings fromtag_types/<name>/mappings.json, classifies every subject in a Work, converts matching slugs to Tag keys viaslug_to_tag_key(), and returns a dict of typed key lists.Also includes a CLI for testing on real OL works.
How to test:
python scripts/migrate_work.py --work OL82563W --dry-runOutput:
=== OL82563W: Harry Potter and the Philosopher's Stone ===
genres:
- /tags/OL169T # Fantasy
- /tags/OL164T # Adventure
- /tags/OL175T # Mystery
- /tags/OL179T # Sci-Fi
(Duplicate keys are deduplicated — multiple subjects mapping to the same Tag key only appear once in the final list.)
File 3: scripts/backfill_tags.py
A two-phase batch backfill script that finds works with genre-matching subjects and writes the corresponding Tag keys to their typed fields.
Phase 1 — Scan dump (no API calls):
python scripts/backfill_tags.py --dump ol_dump_works_latest.txt.gz --type genres > work_keys.txtReads the gzipped OL works dump line by line (tab-separated, 3rd field is JSON). For each work, checks its subjects against our genre mappings. If any subject matches, prints the work key (e.g. /works/OL82563W) to stdout. Just a filter — no API calls, no auth needed.
Phase 2 — Fetch, migrate, save:
python scripts/backfill_tags.py --keys work_keys.txt --type genres --dry-runReads work keys from Phase 1 output. For each work:
CLI reference:
Test output:
Remaining for Phase B:
genres(and eventuallysubgenres) field to the Work schema (work.schema.json) and Infogami type registry (/type/work).--type subgenressupport to the same script.Prerequisites
~/.config/ol.inifor theopenlibrarytagsbotaccountopenlibrary-clientinstalled (pip install -e /path/to/openlibrary-client)Caveats / open questions
Reviewers:
@mekarpeles