| """Project the confirmed case records onto the canonical concept list and render the cohort export |
| bundle (#101, docs/prd.md sections 3, 8.1). |
| |
| The column spec is the canonical concept list (`mapping.concept_list()`, #91), not the built-in CAP |
| checklist: the export speaks concepts, so a second laboratory targeting the same concepts re-derives the |
| same table. `mapping.cap_checklist_coverage()` (#95) is the bridge that says which confirmed CAP field |
| fills which concept column. A concept no CAP field covers, and no derivation reaches, is absent, and an |
| absent value is never a blank: it carries a coded reason in a parallel `<concept>__absent_reason` column, |
| from the field-state vocabulary docs/prd.md section 4.3 defines (`not_applicable`, `not_stated`, |
| `indeterminate`, `determined_by_join`, `constrained`). |
| |
| The two invariants this projection serves: |
| |
| 1. Numbers stay numbers. The stored quantity survives into its own column, and a derived category sits |
| beside it in a separate column, recomputed from that quantity by the registry's named rule |
| (`taxonomy.recompute`, #35), never stored in its place. A confirmed `myometrial_invasion_percent` of |
| 60.0 and the `half_or_more` category it derives to are two columns, so the export can still answer the |
| other convention's question. |
| 2. No value without its evidence. The field-level provenance carries each value's verbatim evidence quote, |
| its page, and the pass that read it, and the bundle carries a checksum over every cited span per case, |
| so a second centre auditing the row reads the same source material. |
| |
| The bundle carries the schema the second centre re-derives against (the concept list and its checksum), |
| the version of the rule set and the extraction model that touched the values, the staging edition each |
| stage speaks (docs/prd.md section 4.2), and a per-case source-document reference with an evidence |
| checksum. The rendered CSV, XLSX, and JSON come from one projection, so the three files and the on-screen |
| preview never disagree. |
| |
| ## Relates to |
| |
| - #40 owns the target-schema selector and the `derive_stage` set projection: choosing an arbitrary |
| confirmed dictionary as the target, and rendering a `constrained` stage as the candidate set with the |
| variable whose absence blocks narrowing. This module names the staging edition it emitted and reserves |
| the `constrained` code for that projection; it does not itself enumerate the candidate set. |
| - #39 owns the deeper export provenance: the byte-level checksum of each scanned report and the positional |
| region of every evidence span. This module carries the source-document reference and a checksum over the |
| cited spans, the reproducible provenance available from the confirmed record alone. |
| - #35 owns the per-value coding-system resolution beyond the FIGO edition; this module cites each concept |
| to the registry via its `cited_to` in the column spec. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import base64 |
| import csv |
| import hashlib |
| import io |
| import json |
| from dataclasses import dataclass |
| from datetime import datetime, timezone |
| from typing import Any, Optional |
|
|
| from sqlalchemy import Connection, text |
|
|
| from endopath import llm_extraction, longitudinal, mapping, projection, taxonomy |
| from endopath.mapping import AssertionType, CompiledConcept |
| from endopath.schema import ChecklistField, FieldStatus |
| from endopath.validation import FieldState |
|
|
| |
| |
| |
| |
| _JOIN_SOURCED_CATEGORY = "molecular" |
|
|
| ABSENT_REASON_SUFFIX = "__absent_reason" |
|
|
|
|
| @dataclass(frozen=True) |
| class _Projected: |
| """One concept's value on one case: a concrete value with no reason, or a coded absence with no |
| value. `origin` records how the value was reached, for the column-spec descriptor.""" |
|
|
| value: Optional[Any] |
| absent_reason: Optional[str] |
| origin: str |
|
|
|
|
| def _primitive(value: object) -> object: |
| """An enum to its value, everything else as-is, so the export carries JSON primitives.""" |
| return value.value if hasattr(value, "value") else value |
|
|
|
|
| def _concept_labels() -> dict[str, str]: |
| """Concept id to human label, read from the dependency-graph nodes the concept ids key on.""" |
| data = json.loads(mapping.DEPENDENCY_GRAPH_PATH.read_text(encoding="utf-8")) |
| return {node["id"]: node["label"] for node in data["nodes"]} |
|
|
|
|
| def _field_value_and_state(field: ChecklistField) -> tuple[Optional[Any], FieldState]: |
| """Reduce one confirmed CAP field to a value and a field state. A gated field is `not_applicable`; a |
| flagged field was searched for and confirmed absent (`not_stated`); an applicable field with no value |
| is `not_stated`; a field carrying a value is present. Silence is never the response to a miss.""" |
| if not field.applicable: |
| return None, FieldState.NOT_APPLICABLE |
| if field.status is FieldStatus.FLAGGED: |
| return None, FieldState.NOT_STATED |
| value = _primitive(field.value) |
| if value is None: |
| return None, FieldState.NOT_STATED |
| return value, FieldState.PRESENT |
|
|
|
|
| def _project_case( |
| checklist, |
| *, |
| concepts: list[CompiledConcept], |
| coverage: dict[str, str], |
| ) -> dict[str, _Projected]: |
| """Project one confirmed checklist onto the concept list: seed the covered concepts from their CAP |
| fields, recompute a derived concept whose inputs are all present, and code every remaining concept as |
| a coded absence (invariant: no value without a reason).""" |
| concept_to_field = {concept_id: field_name for field_name, concept_id in coverage.items()} |
| projected: dict[str, _Projected] = {} |
|
|
| |
| |
| for concept in concepts: |
| field_name = concept_to_field.get(concept.concept_id) |
| if field_name is None: |
| continue |
| value, state = _field_value_and_state(getattr(checklist, field_name)) |
| if state is FieldState.PRESENT: |
| projected[concept.concept_id] = _Projected(value, None, "confirmed") |
| else: |
| projected[concept.concept_id] = _Projected(None, state.value, "absent") |
|
|
| |
| for concept in concepts: |
| cid = concept.concept_id |
| if cid in projected: |
| continue |
| if concept.assertion_type is AssertionType.DERIVED and concept.derived is not None: |
| projected[cid] = _derive(concept, projected) |
| else: |
| reason = ( |
| FieldState.DETERMINED_BY_JOIN |
| if concept.category == _JOIN_SOURCED_CATEGORY |
| else FieldState.NOT_STATED |
| ) |
| projected[cid] = _Projected(None, reason.value, "absent") |
| return projected |
|
|
|
|
| def _derive(concept: CompiledConcept, projected: dict[str, _Projected]) -> _Projected: |
| """Recompute a derived concept from its inputs by its registry-owned rule (`taxonomy.recompute`, |
| #35), the same discipline the B1 validation pass runs: recompute only when every graph input is |
| present, so a derived category is computed from the primitive rather than trusted as stored. A |
| derivation whose input a gate removed inherits `not_applicable`; one whose input is merely absent, or |
| that the per-case pass does not recompute, is `not_stated`.""" |
| assert concept.derived is not None |
| inputs = [projected.get(input_id) for input_id in concept.derived.inputs] |
| if any(item is not None and item.absent_reason == FieldState.NOT_APPLICABLE.value for item in inputs): |
| return _Projected(None, FieldState.NOT_APPLICABLE.value, "absent") |
| if all(item is not None and item.value is not None for item in inputs): |
| recomputed = taxonomy.recompute(concept.concept_id, [item.value for item in inputs]) |
| if recomputed is not None: |
| return _Projected(recomputed, None, "derived") |
| return _Projected(None, FieldState.NOT_STATED.value, "absent") |
|
|
|
|
| def _evidence_checksum(checklist) -> str: |
| """A SHA-256 over the case's cited evidence: the field, verbatim quote, page, and pass of every value |
| that carries a span, in a canonical order. A second centre confirming the same values against the same |
| reports reads the same spans and computes the same checksum (invariant 2).""" |
| spans = [] |
| for field_name, field in sorted(checklist.all_fields().items()): |
| if field.evidence is not None: |
| spans.append( |
| { |
| "field": field_name, |
| "quote": field.evidence.quote, |
| "page_number": field.evidence.page_number, |
| "source": field.evidence.source, |
| } |
| ) |
| canonical = json.dumps(spans, sort_keys=True, ensure_ascii=False) |
| return hashlib.sha256(canonical.encode("utf-8")).hexdigest() |
|
|
|
|
| def _columns(concepts: list[CompiledConcept], labels: dict[str, str]) -> list[dict]: |
| """The column spec: the case-level identifiers, then, per concept in concept-list order, a value |
| column and its parallel absent-reason column. The descriptor carries the concept's category, |
| assertion type, cardinality, and registry citation so the bundle and the screen render the spec |
| without re-deriving it.""" |
| columns: list[dict] = [ |
| {"key": "case_barcode", "label": "Case barcode", "kind": "identifier"}, |
| {"key": "report_date", "label": "Report date", "kind": "identifier"}, |
| {"key": "staging_edition", "label": "Staging edition", "kind": "identifier"}, |
| {"key": "status", "label": "Case status", "kind": "identifier"}, |
| ] |
| for concept in concepts: |
| cid = concept.concept_id |
| label = labels.get(cid, cid) |
| columns.append( |
| { |
| "key": cid, |
| "label": label, |
| "kind": "value", |
| "concept_id": cid, |
| "category": concept.category, |
| "assertion_type": concept.assertion_type.value, |
| "cardinality": concept.cardinality.value, |
| "cited_to": concept.cited_to, |
| "derived": concept.derived.model_dump(mode="json") if concept.derived else None, |
| "absent_reason_key": f"{cid}{ABSENT_REASON_SUFFIX}", |
| } |
| ) |
| columns.append( |
| { |
| "key": f"{cid}{ABSENT_REASON_SUFFIX}", |
| "label": f"{label} absent reason", |
| "kind": "absent_reason", |
| "of": cid, |
| } |
| ) |
| columns.extend(_outcome_columns()) |
| return columns |
|
|
|
|
| def _outcome_columns() -> list[dict]: |
| """The join-sourced outcome columns, appended after the concept columns: the disease outcome joined from |
| the GDC clinical record rather than read from the pathology report (`longitudinal.py`). Each value column |
| is marked `sourced_via: "join"` and paired with its parallel absent-reason column, so the same renderer |
| and the same coded-absence discipline apply as for a concept column. The block keeps the report-concept |
| schema and its checksum meaning "what the report answers".""" |
| columns: list[dict] = [] |
| for spec in longitudinal.OUTCOME_COLUMNS: |
| key = spec["key"] |
| columns.append( |
| { |
| "key": key, |
| "label": spec["label"], |
| "kind": "value", |
| "category": longitudinal.OUTCOME_CATEGORY, |
| "assertion_type": spec["assertion_type"], |
| "sourced_via": "join", |
| "join_source": longitudinal.JOIN_SOURCE, |
| "derived": spec.get("derived"), |
| "note": spec.get("note"), |
| "absent_reason_key": f"{key}{ABSENT_REASON_SUFFIX}", |
| } |
| ) |
| columns.append( |
| { |
| "key": f"{key}{ABSENT_REASON_SUFFIX}", |
| "label": f"{spec['label']} absent reason", |
| "kind": "absent_reason", |
| "of": key, |
| } |
| ) |
| return columns |
|
|
|
|
| def _load_longitudinal(conn: Connection) -> dict[str, list[longitudinal.Record]]: |
| """Every longitudinal record grouped by participant, in one read (issue #131's batched discipline). The |
| outcome projection joins by participant barcode, and a per-case query would reintroduce the N+1 the |
| cohort export was restructured to avoid. The table holds a few thousand small rows, so one unfiltered |
| read is cheaper than a query per case.""" |
| by_participant: dict[str, list[longitudinal.Record]] = {} |
| rows = conn.execute( |
| text( |
| "SELECT participant_id, record_id, record_type, structured_data FROM longitudinal_records" |
| ) |
| ).mappings().fetchall() |
| for row in rows: |
| data: dict = {} |
| if row["structured_data"]: |
| try: |
| data = json.loads(row["structured_data"]) |
| except (json.JSONDecodeError, TypeError): |
| data = {} |
| by_participant.setdefault(row["participant_id"], []).append( |
| longitudinal.Record( |
| record_type=row["record_type"], record_id=row["record_id"], data=data |
| ) |
| ) |
| return by_participant |
|
|
|
|
| def _load_longitudinal_joins(conn: Connection) -> dict[str, list[dict]]: |
| """Every reviewer join decision grouped by case barcode, in one read (issue #131's batched discipline). |
| #180 writes these from the Case Review panel: a per-record disposition (`confirmed` / `rejected`) with |
| the confirmer's identity and role. The export and the cohort endpoint read them to drop rejected records |
| and to attribute the confirmed linkages. A record with no row here has not been reviewed, not rejected.""" |
| by_case: dict[str, list[dict]] = {} |
| rows = conn.execute( |
| text( |
| "SELECT case_barcode, record_id, disposition, confirmed_by, role, confirmed_at " |
| "FROM longitudinal_joins" |
| ) |
| ).mappings().fetchall() |
| for row in rows: |
| by_case.setdefault(row["case_barcode"], []).append( |
| { |
| "record_id": row["record_id"], |
| "disposition": row["disposition"], |
| "confirmed_by": row["confirmed_by"], |
| "role": row["role"], |
| "confirmed_at": row["confirmed_at"], |
| } |
| ) |
| return by_case |
|
|
|
|
| def _rejected_record_ids(case_joins: list[dict]) -> set[str]: |
| """The record ids a reviewer explicitly rejected for this case. The barcode join is deterministic, so a |
| record with no join row is unreviewed, not rejected: only an explicit `rejected` disposition excludes a |
| record from the projection, leaving the default (no decision) projecting exactly as before.""" |
| return {j["record_id"] for j in case_joins if j.get("disposition") == "rejected"} |
|
|
|
|
| def _bundle( |
| concepts: list[CompiledConcept], |
| sources: list[dict], |
| *, |
| staging_edition: Optional[str], |
| per_case_editions: list[str], |
| ) -> dict: |
| """The provenance bundle (docs/prd.md section 3): the schema the second centre re-derives against and |
| its checksum, the rule-set and extraction-model versions that touched the values, the staging edition |
| each stage speaks, the coding systems, and the per-case source references.""" |
| schema_checksum = hashlib.sha256(mapping.concept_list_json().encode("utf-8")).hexdigest() |
| rule_set_checksum = hashlib.sha256( |
| mapping.DEPENDENCY_GRAPH_PATH.read_bytes() |
| ).hexdigest() |
| named_rules = sorted( |
| { |
| concept.derived.rule |
| for concept in concepts |
| if concept.derived is not None |
| } |
| ) |
| return { |
| "target_schema": { |
| "name": "CAP/ICCR endometrial carcinoma synoptic, projected onto the canonical concept list", |
| "concept_count": len(concepts), |
| "schema_checksum": schema_checksum, |
| }, |
| "staging_edition": { |
| "selected": staging_edition or "as_recorded", |
| "per_case_editions": sorted(set(per_case_editions)), |
| }, |
| "generated_at": datetime.now(timezone.utc).isoformat(), |
| "rule_versions": { |
| "dependency_graph_checksum": rule_set_checksum, |
| "named_rules": named_rules, |
| }, |
| "model_versions": {"extraction": llm_extraction.MODEL}, |
| "coding_systems": { |
| "stage": "FIGO; the edition each stage speaks is the per-case staging_edition column", |
| "value_sets": ( |
| "CAP, ICCR, WHO, and AJCC reference standards; each concept cites its registry source " |
| "in the column spec's cited_to" |
| ), |
| }, |
| "sources": sources, |
| "reproduction": ( |
| "A laboratory holding the same reports and confirming the same values against this concept " |
| "list re-derives an identical table. Every absent value carries a coded reason from " |
| "docs/prd.md section 4.3, so a missing value is never a bare blank." |
| ), |
| } |
|
|
|
|
| def _provenance(case_barcode: str, checklist) -> list[dict]: |
| """One field-level provenance record per checklist field: the value, its confidence and status, the |
| coded-absence markers, and the verbatim evidence span (docs/prd.md section 3 case record).""" |
| records: list[dict] = [] |
| for field_name, field in checklist.all_fields().items(): |
| evidence = field.evidence |
| records.append( |
| { |
| "case_barcode": case_barcode, |
| "field_name": field_name, |
| "value": _primitive(field.value), |
| "confidence": field.confidence, |
| "status": field.status.value, |
| "applicable": field.applicable, |
| "not_applicable_reason": field.not_applicable_reason, |
| "evidence_quote": evidence.quote if evidence else None, |
| "evidence_char_start": evidence.char_start if evidence else None, |
| "evidence_char_end": evidence.char_end if evidence else None, |
| "evidence_page": evidence.page_number if evidence else None, |
| "evidence_source": evidence.source if evidence else None, |
| } |
| ) |
| return records |
|
|
|
|
| |
| |
| |
| EXPORTABLE_STATUSES: tuple[str, ...] = ("confirmed", "exported") |
|
|
|
|
| def build_export( |
| conn: Connection, |
| *, |
| staging_edition: Optional[str] = None, |
| project: Optional[str] = None, |
| statuses: Optional[tuple[str, ...]] = None, |
| ) -> dict: |
| """Project every case in the store onto the concept list and assemble the cohort export. |
| |
| Returns the column spec, one projected row per case, the field-level provenance, and the reproduction |
| bundle. `staging_edition` names the edition the researcher chose at export (docs/prd.md section 4.2); |
| the base projection carries the edition each case recorded, and #40 owns re-projecting a stage under a |
| chosen edition as a candidate set. `project` scopes the export to one project's cases (issue #149); |
| omitted, every case in the store is projected. `statuses`, when given, keeps only cases in those |
| statuses; the API passes EXPORTABLE_STATUSES so the served export is the confirmed dataset (#231).""" |
| from endopath import storage |
|
|
| concepts = mapping.compile_concepts() |
| coverage = mapping.cap_checklist_coverage() |
| labels = _concept_labels() |
|
|
| rows: list[dict] = [] |
| provenance: list[dict] = [] |
| sources: list[dict] = [] |
| join_sources: list[dict] = [] |
| per_case_editions: list[str] = [] |
|
|
| |
| |
| longitudinal_by_participant = _load_longitudinal(conn) |
| |
| |
| longitudinal_joins_by_case = _load_longitudinal_joins(conn) |
|
|
| |
| |
| |
| for case, patient_filename in storage.load_all_cases(conn, project=project): |
| if statuses is not None and case.status.value not in statuses: |
| continue |
| projected = _project_case(case.checklist, concepts=concepts, coverage=coverage) |
|
|
| row: dict[str, Any] = { |
| "case_barcode": case.case_barcode, |
| "report_date": case.report_date.isoformat() if case.report_date else None, |
| "staging_edition": case.staging_edition.value, |
| "status": case.status.value, |
| } |
| for concept in concepts: |
| item = projected[concept.concept_id] |
| row[concept.concept_id] = item.value |
| row[f"{concept.concept_id}{ABSENT_REASON_SUFFIX}"] = item.absent_reason |
|
|
| |
| |
| |
| target_edition = staging_edition or case.staging_edition.value |
| stage_verdict = projection.project_stage(case.checklist, target_edition) |
| stage_value, stage_reason = projection.stage_cell(stage_verdict) |
| row["figo_stage"] = stage_value |
| row[f"figo_stage{ABSENT_REASON_SUFFIX}"] = stage_reason |
| row["figo_stage__projection"] = stage_verdict |
|
|
| |
| |
| |
| participant_id = ( |
| case.case_barcode[:12] if len(case.case_barcode) >= 12 else case.case_barcode |
| ) |
| outcome_records = longitudinal_by_participant.get(participant_id, []) |
| |
| |
| |
| case_joins = longitudinal_joins_by_case.get(case.case_barcode, []) |
| rejected = _rejected_record_ids(case_joins) |
| if rejected: |
| outcome_records = [r for r in outcome_records if r.record_id not in rejected] |
| outcomes = longitudinal.project_outcomes(outcome_records) |
| for key in longitudinal.OUTCOME_COLUMN_KEYS: |
| cell = outcomes[key] |
| row[key] = cell.value |
| row[f"{key}{ABSENT_REASON_SUFFIX}"] = cell.absent_reason |
| source_entry = { |
| "case_barcode": case.case_barcode, |
| "participant_id": participant_id, |
| "record_ids": longitudinal.contributing_record_ids(outcome_records), |
| } |
| |
| |
| if case_joins: |
| source_entry["linkage_decisions"] = case_joins |
| join_sources.append(source_entry) |
| rows.append(row) |
|
|
| provenance.extend(_provenance(case.case_barcode, case.checklist)) |
| sources.append( |
| { |
| "case_barcode": case.case_barcode, |
| "source_document": patient_filename, |
| "evidence_checksum": _evidence_checksum(case.checklist), |
| } |
| ) |
| per_case_editions.append(case.staging_edition.value) |
|
|
| from endopath import provenance as provenance_mod |
|
|
| bundle = _bundle( |
| concepts, |
| sources, |
| staging_edition=staging_edition, |
| per_case_editions=per_case_editions, |
| ) |
| |
| |
| bundle["llm_calls"] = provenance_mod.summary(conn) |
| |
| |
| |
| bundle["join_sources"] = { |
| "source": longitudinal.JOIN_SOURCE, |
| "outcome_columns": longitudinal.OUTCOME_COLUMN_KEYS, |
| "derivation_rules": [longitudinal.RECURRENCE_RULE, longitudinal.OVERALL_SURVIVAL_RULE], |
| "data_quality": ( |
| "therapeutic_agents reads Tamoxifen for nearly every case. This is faithful to the GDC/TCGA " |
| "harmonized clinical record, a known source artifact, not an error in this pipeline; it is " |
| "surfaced with its provenance rather than silently dropped. The received_chemotherapy, " |
| "received_radiation, and received_hormone_therapy flags read administration, not record " |
| 'presence: GDC emits a treatment_<type> template record marked treatment_or_therapy = "no" on ' |
| "cases where the therapy was considered but not given (most often Radiation Therapy, NOS), so a " |
| 'flag is True only where a matching record is marked "yes", False on an explicit "no", and ' |
| 'not_stated where the matching records carry only "unknown". Reading presence instead reports ' |
| "radiation on 543/546 cases (99%); administration reports 264 (48%)." |
| ), |
| "per_case": join_sources, |
| } |
| return { |
| "columns": _columns(concepts, labels), |
| "rows": rows, |
| "provenance": provenance, |
| "bundle": bundle, |
| } |
|
|
|
|
| |
|
|
|
|
| def _cell(value: Any) -> Any: |
| """A projected value rendered for a spreadsheet cell: None to an empty string, a bool kept as a bool, |
| everything else as-is. An absent value's cell is empty; its reason rides the parallel column.""" |
| if value is None: |
| return "" |
| return value |
|
|
|
|
| def to_csv(export: dict) -> str: |
| """The projected table as CSV: the column keys as the header, one row per case, an empty cell for an |
| absent value with its reason in the parallel column.""" |
| keys = [column["key"] for column in export["columns"]] |
| buffer = io.StringIO() |
| writer = csv.DictWriter(buffer, fieldnames=keys, extrasaction="ignore") |
| writer.writeheader() |
| for row in export["rows"]: |
| writer.writerow({key: _cell(row.get(key)) for key in keys}) |
| return buffer.getvalue() |
|
|
|
|
| def to_json(export: dict) -> str: |
| """The whole export as self-describing JSON: the bundle, the column spec, the rows, and the field-level |
| provenance, so the file alone re-derives the table.""" |
| return json.dumps( |
| { |
| "bundle": export["bundle"], |
| "columns": export["columns"], |
| "rows": export["rows"], |
| "provenance": export["provenance"], |
| }, |
| indent=2, |
| default=str, |
| ) |
|
|
|
|
| def to_xlsx_bytes(export: dict) -> bytes: |
| """The projected table as an XLSX workbook: a `cases` sheet of the projected rows, a `provenance` |
| sheet of the field-level evidence, and a `bundle` sheet of the reproduction metadata as key/value |
| rows.""" |
| from openpyxl import Workbook |
|
|
| workbook = Workbook() |
|
|
| cases = workbook.active |
| cases.title = "cases" |
| keys = [column["key"] for column in export["columns"]] |
| cases.append([column["label"] for column in export["columns"]]) |
| for row in export["rows"]: |
| cases.append([_cell(row.get(key)) for key in keys]) |
|
|
| provenance_sheet = workbook.create_sheet("provenance") |
| provenance_headers = [ |
| "case_barcode", |
| "field_name", |
| "value", |
| "confidence", |
| "status", |
| "applicable", |
| "not_applicable_reason", |
| "evidence_quote", |
| "evidence_page", |
| "evidence_source", |
| ] |
| provenance_sheet.append(provenance_headers) |
| for record in export["provenance"]: |
| provenance_sheet.append([_cell(record.get(header)) for header in provenance_headers]) |
|
|
| bundle_sheet = workbook.create_sheet("bundle") |
| bundle_sheet.append(["key", "value"]) |
| for key, value in _flatten(export["bundle"]): |
| bundle_sheet.append([key, value]) |
|
|
| buffer = io.BytesIO() |
| workbook.save(buffer) |
| return buffer.getvalue() |
|
|
|
|
| def _flatten(value: Any, prefix: str = "") -> list[tuple[str, str]]: |
| """Flatten the nested bundle into dotted key/value rows for the XLSX bundle sheet.""" |
| rows: list[tuple[str, str]] = [] |
| if isinstance(value, dict): |
| for key, inner in value.items(): |
| rows.extend(_flatten(inner, f"{prefix}.{key}" if prefix else str(key))) |
| elif isinstance(value, list): |
| if all(not isinstance(item, (dict, list)) for item in value): |
| rows.append((prefix, ", ".join(str(item) for item in value))) |
| else: |
| for index, item in enumerate(value): |
| rows.extend(_flatten(item, f"{prefix}[{index}]")) |
| else: |
| rows.append((prefix, "" if value is None else str(value))) |
| return rows |
|
|
|
|
| def render_files(export: dict) -> dict: |
| """The three download files from one projection: CSV and JSON as text, XLSX as base64, each with its |
| filename and media type. One projection feeds the preview and all three files, so they never diverge.""" |
| return { |
| "csv": { |
| "filename": "endometrial_cohort.csv", |
| "media_type": "text/csv", |
| "content": to_csv(export), |
| }, |
| "json": { |
| "filename": "endometrial_cohort.json", |
| "media_type": "application/json", |
| "content": to_json(export), |
| }, |
| "xlsx": { |
| "filename": "endometrial_cohort.xlsx", |
| "media_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", |
| "content_base64": base64.b64encode(to_xlsx_bytes(export)).decode("ascii"), |
| }, |
| } |
|
|