Files
paper-atlas/paper_atlas/collectors/peerread.py
codex-bot 1c18cbe03a
Metadata integrity checks / metadata (push) Successful in 1m12s
Package Paper Atlas with organized modules and a unified CLI
2026-11-06 17:32:24 +08:00

76 lines
4.2 KiB
Python

"""Collect explicit public ICLR decisions from the authors' PeerRead release.
Review bodies and parsed full paper text are deliberately not persisted.
"""
import argparse
import json
import time
import urllib.request
from collections import defaultdict
from pathlib import Path
from paper_atlas.io.http import fetch
def collect(cache,out):
root = Path(out)
root.mkdir(parents=True,exist_ok=True)
commit = json.loads(fetch('https://api.github.com/repos/allenai/PeerRead/commits/master',cache))['sha']
tree = json.loads(fetch(f'https://api.github.com/repos/allenai/PeerRead/git/trees/{commit}?recursive=1',cache))
if tree.get('truncated'):
raise RuntimeError('Truncated repository tree')
paths = [v['path'] for v in tree['tree'] if v['path'].startswith('data/iclr_2017/') and '/reviews/' in v['path'] and v['path'].endswith('.json')]
if not paths:
raise RuntimeError('No explicit ICLR2017 metadata files found')
cache_dir = root/'peerread-normalized'/commit
cache_dir.mkdir(parents=True,exist_ok=True)
grouped = defaultdict(list)
for number,path in enumerate(paths,1):
external = '/'.join(path.split('/')[1:])
normalized_cache = cache_dir/(path.replace('/','_'))
if normalized_cache.exists():
record = json.loads(normalized_cache.read_text())
else:
raw_url = f'https://raw.githubusercontent.com/allenai/PeerRead/{commit}/{path}'
with urllib.request.urlopen(urllib.request.Request(raw_url,headers={'User-Agent':'PaperAtlas/0.1 metadata research'}),timeout=60) as response:
payload = response.read(3*1024**2+1)
if len(payload)>3*1024**2:
raise RuntimeError('Unexpected metadata size')
data = json.loads(payload)
conference = data.get('conference','')
if 'iclr' not in conference.lower() or '2017' not in conference:
raise RuntimeError('Unexpected conference metadata: '+conference)
accepted = data.get('accepted')
status = ('accepted' if accepted else 'rejected') if isinstance(accepted,bool) else 'unknown'
authors = data.get('authors') or []
if isinstance(authors,str):
authors = [author.strip() for author in authors.split(',') if author.strip()]
record = {'source':'peerread','external_id':external,'url':f'https://github.com/allenai/PeerRead/blob/{commit}/{path}','title':data['title'],'abstract':data.get('abstract'),'authors':authors,'identifiers':{},'venue':'ICLR','year':2017,'track':'workshop' if 'workshop' in conference.lower() else 'main','status':status,'raw_decision':json.dumps({'accepted':accepted}),'metadata':{'conference':conference,'dataset_commit':commit,'dataset_path':path,'original_id':data.get('id'),'accepted':accepted,'decision_provenance':'PeerRead authors public release; explicit label, not inferred from publication absence'}}
normalized_cache.write_text(json.dumps(record,ensure_ascii=False))
time.sleep(0.25)
grouped[record['track']].append(record)
if number%25==0:
print(json.dumps({'source':'peerread','collected':number,'expected':len(paths)}),flush=True)
for track,records in grouped.items():
target = root/f'peerread-iclr2017-{track}.jsonl'
part = target.with_suffix('.part')
with part.open('w') as output:
for record in records:
output.write(json.dumps(record,ensure_ascii=False)+'\n')
part.replace(target)
manifest = {'source':'peerread','venue':'ICLR','year':2017,'track':track,'count':len(records),'list_url':f'https://github.com/allenai/PeerRead/tree/{commit}/data/iclr_2017','dataset_commit':commit,'file':str(target),'decision_counts':{status:sum(r['status']==status for r in records) for status in ('accepted','rejected','unknown')}}
target.with_suffix('.manifest.json').write_text(json.dumps(manifest,indent=2))
print(json.dumps(manifest),flush=True)
def main(argv=None, *, prog=None):
parser = argparse.ArgumentParser(prog=prog)
parser.add_argument('--cache',required=True)
parser.add_argument('--out',required=True)
args = parser.parse_args(argv)
collect(args.cache,args.out)
if __name__ == '__main__':
main()