|
53 | 53 | HOLDER = "Toyota Research Institute" |
54 | 54 |
|
55 | 55 | PEDS_BASE = "https://ped.uspto.gov/api/queries" # RETIRED March 2025 |
56 | | -ODP_SEARCH_URL = "https://api.uspto.gov/patent/file-wrapper/search" |
57 | | -ODP_APP_URL = "https://api.uspto.gov/patent/file-wrapper/applications/{appnum}/application-data" |
| 56 | +ODP_SEARCH_URL = "https://api.uspto.gov/api/v1/patent/applications/search" |
| 57 | +ODP_APP_URL = "https://api.uspto.gov/api/v1/patent/applications/{appnum}/meta-data" |
58 | 58 |
|
59 | 59 | HEADERS = { |
60 | 60 | "Accept": "application/json", |
@@ -110,15 +110,12 @@ def backup_bib(src: Path, dst: Path) -> None: |
110 | 110 | def extract_manual_entries(bib_path: Path) -> list[str]: |
111 | 111 | """ |
112 | 112 | Parse patents.bib and return raw BibTeX strings for any entry whose |
113 | | - keywords field contains 'manual'. Works with or without bibtexparser. |
| 113 | + keywords field contains 'manual'. |
| 114 | + Always uses regex — bibtexparser v1 silently drops @patent entries. |
114 | 115 | """ |
115 | 116 | if not bib_path.exists(): |
116 | 117 | return [] |
117 | | - |
118 | | - if HAS_BIBTEX: |
119 | | - return _extract_manual_bibtexparser(bib_path) |
120 | | - else: |
121 | | - return _extract_manual_regex(bib_path) |
| 118 | + return _extract_manual_regex(bib_path) |
122 | 119 |
|
123 | 120 |
|
124 | 121 | def _extract_manual_bibtexparser(bib_path: Path) -> list[str]: |
@@ -153,20 +150,25 @@ def _extract_manual_regex(bib_path: Path) -> list[str]: |
153 | 150 | def fetch_patent_odp(patent_number: str, api_key: str) -> dict | None: |
154 | 151 | """ |
155 | 152 | Query the USPTO Open Data Portal for a single patent by grant number. |
156 | | - Uses the search endpoint with patentNumber field. |
| 153 | + Uses POST /api/v1/patent/applications/search with patentNumber field. |
| 154 | + Returns the applicationMetaData sub-dict from the first hit. |
157 | 155 | """ |
158 | | - params = { |
159 | | - "q": f"patentNumber:{patent_number}", |
160 | | - "hits": 1, |
| 156 | + payload = { |
| 157 | + "q": f"applicationMetaData.patentNumber:{patent_number}", |
| 158 | + "pagination": {"offset": 0, "limit": 1}, |
161 | 159 | } |
162 | | - headers = {**HEADERS, "X-API-KEY": api_key} |
| 160 | + headers = {**HEADERS, "Content-Type": "application/json", "x-api-key": api_key} |
163 | 161 | try: |
164 | | - r = requests.get(ODP_SEARCH_URL, params=params, headers=headers, timeout=20, verify=False) |
| 162 | + r = requests.post(ODP_SEARCH_URL, json=payload, headers=headers, |
| 163 | + timeout=20, verify=False) |
165 | 164 | r.raise_for_status() |
166 | | - hits = (r.json() |
167 | | - .get("hits", {}) |
168 | | - .get("hits", [])) |
169 | | - return hits[0].get("_source", {}) if hits else None |
| 165 | + results = r.json() |
| 166 | + bag = (results.get("patentFileWrapperDataBag") or |
| 167 | + results.get("results") or []) |
| 168 | + if not bag: |
| 169 | + return None |
| 170 | + first = bag[0] |
| 171 | + return first.get("applicationMetaData", first) |
170 | 172 | except Exception as e: |
171 | 173 | print(f" [warn] ODP query failed for {patent_number}: {e}") |
172 | 174 | return None |
@@ -433,41 +435,49 @@ def mode_discover(dry_run: bool) -> None: |
433 | 435 | """Search USPTO ODP by inventor name for patents not in KNOWN_PATENT_NUMBERS.""" |
434 | 436 | print(f"Searching USPTO ODP for patents by {INVENTOR_FIRST} {INVENTOR_LAST}...") |
435 | 437 | api_key = get_api_key() |
436 | | - params = { |
437 | | - "q": f"inventors.lastName:{INVENTOR_LAST} AND inventors.firstName:{INVENTOR_FIRST}", |
438 | | - "hits": 50, |
| 438 | + # Search by inventor last name, filter to granted patents only (status "Patented Case") |
| 439 | + payload = { |
| 440 | + "q": f"applicationMetaData.inventorBag:{INVENTOR_LAST}", |
| 441 | + "pagination": {"offset": 0, "limit": 50}, |
439 | 442 | } |
440 | | - headers = {**HEADERS, "X-API-KEY": api_key} |
| 443 | + headers = {**HEADERS, "Content-Type": "application/json", "x-api-key": api_key} |
441 | 444 | try: |
442 | | - r = requests.get( |
| 445 | + r = requests.post( |
443 | 446 | ODP_SEARCH_URL, |
444 | | - params=params, |
| 447 | + json=payload, |
445 | 448 | headers=headers, |
446 | 449 | timeout=20, |
447 | 450 | verify=False, |
448 | 451 | ) |
449 | 452 | r.raise_for_status() |
450 | | - hits = r.json().get("hits", {}).get("hits", []) |
451 | | - docs = [h.get("_source", {}) for h in hits] |
| 453 | + results = r.json() |
| 454 | + bag = (results.get("patentFileWrapperDataBag") or |
| 455 | + results.get("results") or []) |
| 456 | + docs = [h.get("applicationMetaData", h) for h in bag] |
452 | 457 | except Exception as e: |
453 | 458 | print(f"[error] USPTO ODP discovery search failed: {e}") |
454 | 459 | sys.exit(1) |
455 | 460 |
|
456 | 461 | print(f" Found {len(docs)} total records.") |
457 | 462 | new_entries = [] |
458 | 463 | for doc in docs: |
459 | | - num = doc.get("patentNumber", "") |
460 | | - if not num or num in KNOWN_PATENT_NUMBERS: |
| 464 | + patent_num = doc.get("patentNumber", "") |
| 465 | + app_num = doc.get("applicationNumberText", "") |
| 466 | + # Use patent number for granted, application number for filed |
| 467 | + num = patent_num or app_num |
| 468 | + status_hint = "Granted" if patent_num else "Filed" |
| 469 | + if not num: |
| 470 | + continue |
| 471 | + if num in KNOWN_PATENT_NUMBERS: |
461 | 472 | continue |
462 | 473 | title = doc.get("inventionTitle", doc.get("patentTitle", "unknown title")) |
463 | | - print(f"\n NEW: US {num} — {title[:60]}") |
| 474 | + print(f"\n NEW [{status_hint}]: {num} — {title[:60]}") |
464 | 475 | normalized = parse_patent_record(doc, num) |
465 | 476 | suggested_holder = (doc.get("assigneeEntityName") or |
466 | 477 | doc.get("assigneeName") or |
467 | 478 | doc.get("applicantName") or |
468 | 479 | HOLDER) |
469 | | - suggested_status = ("Filed" if not doc.get("patentNumber") |
470 | | - else "Granted") |
| 480 | + suggested_status = status_hint |
471 | 481 | if not dry_run: |
472 | 482 | holder = confirm_holder(num, title, suggested=suggested_holder) |
473 | 483 | status = confirm_status(num, title, suggested=suggested_status) |
|
0 commit comments