MHRN-Space / scripts /export_external_review_aggregate.py
ThomasHeisig's picture
Sync GitHub main 100d504c10660702726157355b31aac3bdfe49f5 (part 5)
14ea8cd verified
Raw History Blame Contribute Delete
2.71 kB
#!/usr/bin/env python3
"""Create a non-identifying descriptive aggregate from a private review export.
The input export must remain outside the repository. The output contains only
summary statistics and instrument metadata; raw answers, notes, participant
codes, signatures and withdrawal secrets are never copied.
"""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from review_portal.analysis import summarize
_FORBIDDEN_KEYS = {
"answers",
"notes",
"participant_code",
"withdrawal_token",
"invitation_code",
"signature",
"email",
"name",
}
def _load_records(path: Path) -> list[dict[str, Any]]:
payload: object = json.loads(path.read_text(encoding="utf-8"))
if isinstance(payload, dict):
payload = payload.get("records", [])
if not isinstance(payload, list) or not all(
isinstance(row, dict) for row in payload
):
raise ValueError("Input must be a JSON list or an admin export with records.")
return [dict(row) for row in payload]
def _assert_safe(value: object) -> None:
if isinstance(value, dict):
for key, child in value.items():
if str(key).lower() in _FORBIDDEN_KEYS:
raise ValueError(f"Unsafe field in aggregate: {key}")
_assert_safe(child)
elif isinstance(value, list):
for child in value:
_assert_safe(child)
def build_aggregate(records: list[dict[str, Any]]) -> dict[str, Any]:
aggregate = {
"schema_version": 1,
"classification": "PRIVATE_HUMAN_REVIEW_DESCRIPTIVES_NOT_EVIDENCE",
"generated_at": datetime.now(timezone.utc).isoformat(),
"source_record_count": len(records),
"automatic_evidence_promotion": False,
"raw_answers_included": False,
"summary": summarize(records),
}
_assert_safe(aggregate)
return aggregate
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"input", type=Path, help="Private admin export JSON outside the repository"
)
parser.add_argument("output", type=Path, help="Sanitized aggregate JSON")
args = parser.parse_args()
records = _load_records(args.input)
aggregate = build_aggregate(records)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(
json.dumps(aggregate, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
print(f"Wrote sanitized aggregate for {len(records)} records to {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())