Skip to content

Commit 143220e

Browse files
committed
fix fetch_patents.py: correct ODP API endpoint, auth header, and request format
- Switch from GET to POST for search requests (per ODP spec) - Fix endpoint path: /patent/file-wrapper/search → /api/v1/patent/applications/search - Fix auth header: X-API-KEY → x-api-key (case-sensitive) - Fix response parsing: use patentFileWrapperDataBag key - Fix query field names per ODP spec (applicationMetaData.patentNumber, applicationMetaData.inventorBag) - Add verify=False + urllib3 warning suppression for SSL cert mismatch - mode_discover: return both filed and granted applications; fix skip logic to use applicationNumberText fallback for filed (no patentNumber) - extract_manual_entries: always use regex extractor; bibtexparser v1 silently drops @patent entries
1 parent 5d682a3 commit 143220e

1 file changed

Lines changed: 41 additions & 31 deletions

File tree

‎scripts/fetch_patents.py‎

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@
5353
HOLDER = "Toyota Research Institute"
5454

5555
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"
5858

5959
HEADERS = {
6060
"Accept": "application/json",
@@ -110,15 +110,12 @@ def backup_bib(src: Path, dst: Path) -> None:
110110
def extract_manual_entries(bib_path: Path) -> list[str]:
111111
"""
112112
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.
114115
"""
115116
if not bib_path.exists():
116117
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)
122119

123120

124121
def _extract_manual_bibtexparser(bib_path: Path) -> list[str]:
@@ -153,20 +150,25 @@ def _extract_manual_regex(bib_path: Path) -> list[str]:
153150
def fetch_patent_odp(patent_number: str, api_key: str) -> dict | None:
154151
"""
155152
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.
157155
"""
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},
161159
}
162-
headers = {**HEADERS, "X-API-KEY": api_key}
160+
headers = {**HEADERS, "Content-Type": "application/json", "x-api-key": api_key}
163161
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)
165164
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)
170172
except Exception as e:
171173
print(f" [warn] ODP query failed for {patent_number}: {e}")
172174
return None
@@ -433,41 +435,49 @@ def mode_discover(dry_run: bool) -> None:
433435
"""Search USPTO ODP by inventor name for patents not in KNOWN_PATENT_NUMBERS."""
434436
print(f"Searching USPTO ODP for patents by {INVENTOR_FIRST} {INVENTOR_LAST}...")
435437
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},
439442
}
440-
headers = {**HEADERS, "X-API-KEY": api_key}
443+
headers = {**HEADERS, "Content-Type": "application/json", "x-api-key": api_key}
441444
try:
442-
r = requests.get(
445+
r = requests.post(
443446
ODP_SEARCH_URL,
444-
params=params,
447+
json=payload,
445448
headers=headers,
446449
timeout=20,
447450
verify=False,
448451
)
449452
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]
452457
except Exception as e:
453458
print(f"[error] USPTO ODP discovery search failed: {e}")
454459
sys.exit(1)
455460

456461
print(f" Found {len(docs)} total records.")
457462
new_entries = []
458463
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:
461472
continue
462473
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]}")
464475
normalized = parse_patent_record(doc, num)
465476
suggested_holder = (doc.get("assigneeEntityName") or
466477
doc.get("assigneeName") or
467478
doc.get("applicantName") or
468479
HOLDER)
469-
suggested_status = ("Filed" if not doc.get("patentNumber")
470-
else "Granted")
480+
suggested_status = status_hint
471481
if not dry_run:
472482
holder = confirm_holder(num, title, suggested=suggested_holder)
473483
status = confirm_status(num, title, suggested=suggested_status)

0 commit comments

Comments
 (0)