#!/usr/bin/env python3 """Build BindingDB source records and an exact-Kd gold file from pinned zips. Reads the publisher-verified BindingDB curated-articles export (main TSV), the Reactant_set_id -> ENTRYID_ASSAYID mapping, and the Assays export from their ZIP archives, verifies every archive's SHA-256 against the pinned config, and writes: - ``source_records.jsonl``: one versioned JSON envelope per valid single-chain record/measurement (all measurement types and relations), - ``gold_exact_kd.jsonl``: only exact '=' Kd records with one chain, a sequence, a canonicalizable SMILES, a positive nM value, and an assay join, - ``bindingdb_audit.json``: deterministic audit of counts, coverage, and every rejection reason. Nothing is aggregated across rows or assays, and Ki/IC50/EC50 values are never relabeled as Kd. """ from __future__ import annotations import argparse import json from pathlib import Path from mitointeract_recovery import bindingdb_adapter DEFAULT_CONFIG = Path(__file__).resolve().parents[1] / "config-bindingdb.json" def load_config(path: Path) -> dict: config = json.loads(path.read_text()) files = config.get("files") if not isinstance(files, dict): raise ValueError(f"{path}: config must contain a 'files' object") for key in bindingdb_adapter.INPUT_KEYS: entry = files.get(key) if not isinstance(entry, dict): raise ValueError(f"{path}: config files entry {key!r} missing") for field in ("url", "member", "sha256"): if not entry.get(field): raise ValueError(f"{path}: config files[{key!r}] missing {field!r}") return config def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--main-zip", type=Path, required=True, help="main articles TSV zip" ) parser.add_argument( "--mapping-zip", type=Path, required=True, help="rsid -> ENTRYID_ASSAYID map zip", ) parser.add_argument( "--assays-zip", type=Path, required=True, help="assay names/descriptions zip" ) parser.add_argument( "--config", type=Path, default=DEFAULT_CONFIG, help="pinned release config (URLs, members, SHA-256 checksums)", ) parser.add_argument("--output-dir", type=Path, required=True) parser.add_argument( "--max-source-rows", type=int, default=None, help="process at most this many main-TSV data rows (for smoke runs)", ) args = parser.parse_args() if args.max_source_rows is not None and args.max_source_rows < 1: parser.error("--max-source-rows must be a positive integer") config = load_config(args.config) bindingdb_adapter.run_pipeline( main_zip=args.main_zip, mapping_zip=args.mapping_zip, assays_zip=args.assays_zip, config=config, output_dir=args.output_dir, max_source_rows=args.max_source_rows, ) if __name__ == "__main__": main()