just_dna_enricher.civic_build¶
just_dna_enricher.civic_build ¶
Build the CIViC snapshot ([dev]) — derived, dated, and the first one that may be published.
CIViC (Griffith et al., Nat Genet 2017; civicdb.org) is a curated knowledgebase of variant
interpretations in cancer. It is a source, of the same kind as ClinVar and PubMind: an
authoritative annotation source. Nothing it produces may enter resolution.csv — CIViC states a
clinical opinion about a locus something else resolved, and resolution.csv's authority column is a
different word for a different thing (@source-vs-authority).
Two published surfaces disagree, and the dated download is the one a snapshot may use. The
GraphQL API defaults to status: NON_REJECTED and serves 11,518 evidence items; the bulk
ClinicalEvidenceSummaries TSV is 4,903 rows and every one is accepted. That is a 2.35x difference
between two faces of one database, declared by neither. A snapshot has to be byte-reproducible from a
pinned input, and only the download side has dated releases (01-Aug-2026/...), so this builder reads
the TSV pair and records the basis explicitly. A figure from here is not comparable with a figure from
the API, and release.json says so.
The direction axis is what CIViC has to offer here, and it is not clin_sig. Measured over the
whole database, the germline subset carries five ACMG-tier calls and zero benign-class ones, so it
can never make a clinical-significance disagreement sayable. What it does carry is
Predisposition/Protectiveness crossed with Supports/Does Not Support — this format's
direction axis (risk/protective). See docs/probes/CIVIC_SURVEY.md for every number.
"Does not support predisposition" is not "protective", and the null is the point. A refutation
removes a claim; it does not establish the opposite one. So a Does Not Support row is kept, its raw
words preserved, and its derived direction left null — the house's three-valued rule, where an
unknown is withheld rather than negated. A drafter withholds those rows; it must not read them as
protective.
Every drop is counted and the counts close. The origin filter alone removes about three quarters
of the source, and a filter whose scope is narrower than its name is the defect this item was filed
against. So input_rows == record_count + sum(dropped.values()) is asserted as an equality over a
walked registry (@registry-completeness), and every reason lands in release.json
(@dont-discard-computed).
Identity comes from what CIViC publishes, never from a liftover. Its coordinates are GRCh37 or
absent — never GRCh38 — but the variant file also carries rsIDs and, for some records, RefSeq NC_
accessions on both builds. An rsID is build-independent and resolves through the ordinary chain,
producing the independent second value resolution._verify cross-examines; a lifted coordinate would
be the row's sole identity with nothing to check it against, which is what RM48 refused. So a record
is kept when it carries an rsID or a GRCh38 accession, and dropped — counted — when it carries
neither. allele_registry_id rides along so the dropped class stays addressable later.
Scoring an accession for build needs the per-chromosome map. NC_000001.11 is GRCh38 while
NC_000002.11 is GRCh37: the version meaning "GRCh38" differs per chromosome. A first pass at this
tested for ".11" or ".12" and overcounted reachable records five-fold.
Builder-only: polars is a guarded [dev] import, exactly as in the sibling builders.
CivicBuildError ¶
Bases: RuntimeError
The build cannot proceed: a missing input, an unreadable file, a column that is not there.
CivicUnavailable ¶
Bases: CivicBuildError
The release could not be fetched — a transport failure or a release that does not exist.
A subclass, so except CivicBuildError keeps catching everything it did, while a caller that
wants to distinguish "the source did not answer" from "the source answered something we cannot
build" can (@client-exception-contract). The subclassing makes a caller's except order
load-bearing, which is why it is stated here rather than left to be discovered.
CivicDownload
dataclass
¶
CivicDownload(
path: Path,
sha256: str | None,
url: str | None = None,
etag: str | None = None,
last_modified: str | None = None,
)
What a download established about one file's bytes — each half None when unstated.
CivicBuildResult
dataclass
¶
CivicBuildResult(
out_dir: Path,
parquet_file: Path,
input_rows: int,
record_count: int,
dropped: dict[str, int] = dict(),
unjoinable_submitted: int = 0,
composite_profile_rows: int = 0,
status_counts: dict[str, int] = dict(),
status_basis: str = CIVIC_BULK_STATUS,
vcf_evidence: dict[str, int] = dict(),
curated_identities: dict[str, int] = dict(),
identity_derivations: dict[str, int] = dict(),
variants: int = 0,
withheld_direction: int = 0,
contested_variants: int = 0,
unresolvable_with_caid: int = 0,
unparsable_hgvs: int = 0,
evidence_sha256: str | None = None,
variant_sha256: str | None = None,
profile_sha256: str | None = None,
dataset: str | None = None,
)
Outcome of a build: the paths, the counts kept, and every count dropped.
civic_release_url ¶
The URL of one file in a dated release.
CIViC's dated releases repeat the date in the filename (01-Aug-2026/01-Aug-2026-<file>), which
is a shape a caller should not have to know.
Source code in enricher/src/just_dna_enricher/civic_build.py
download_civic_file ¶
Stream one release file to dest (atomic .part rename).
Mirrors pubmind_build.download_pubmind_table, including keeping ETag and Last-Modified: a
dated release should be immutable, and recording the headers is what would turn an upstream
revision into a finding rather than a silent change of answer.
Source code in enricher/src/just_dna_enricher/civic_build.py
parse_rsids ¶
The rs-numbers in a variant_aliases cell, lowercased, in source order without duplicates.
CIViC's aliases are a comma-separated free-form list holding rs-numbers beside protein names and legacy labels, so the rs-numbers are selected by shape rather than by position.
Source code in enricher/src/just_dna_enricher/civic_build.py
variant_rsids ¶
Every rs-number CIViC states for one variant, name first, then aliases.
Two fields, because CIViC uses both and neither is the documented one. The obvious place is
variant_aliases, and a first version read only that — missing five variants in the germline
direction set whose name is the rs-number itself (RS2736100, rs681673), sometimes with a
protein alias beside it and sometimes with nothing. Those five needed no registry lookup and no
conversion; the identity was published in plain sight, in the column a reader looks at first.
Name before aliases, because the name is the source's own primary label for the variant while an
alias is a synonym. Order only decides which is stored as rsid; both are parsed either way.
Source code in enricher/src/just_dna_enricher/civic_build.py
parse_grch38_substitution ¶
(chrom, start, ref, alt) from a GRCh38 genomic HGVS substitution, or None.
None covers three different things on purpose — no accession, a GRCh37 accession, and a GRCh38
accession in a form this parser does not read. The caller separates the third with
has_unparsable_grch38, because "the source said nothing" and "the source said something we
cannot hold" are different findings.
Source code in enricher/src/just_dna_enricher/civic_build.py
has_unparsable_grch38 ¶
True when a GRCh38 accession is present but no substitution on it could be parsed.
Source code in enricher/src/just_dna_enricher/civic_build.py
build_snapshot ¶
build_snapshot(
evidence_tsv: Path,
variant_tsv: Path,
profile_tsv: Path,
out_dir: Path,
*,
release: str | None = None,
evidence_sha256: str | None = None,
variant_sha256: str | None = None,
profile_sha256: str | None = None,
vcf: Path | None = None,
vcf_sha256: str | None = None,
) -> CivicBuildResult
Reduce the CIViC release pair to one parquet plus release.json.
Rows are emitted sorted by (chrom in karyotype order, start, ref, alt, variant_id, evidence_id),
so a rebuild from the same release is byte-identical (Principle 7); release.json's built_at is
the only per-run-varying byte and lives outside the parquet, exactly as in pubmind_build. Rows
with no parsed GRCh38 coordinate sort after the placed ones, by variant_id — a deterministic
position rather than wherever the dict landed them.
Every provenance argument defaults to None because only a caller that actually fetched can say
where the bytes came from; a build off local disk records unknown rather than inventing a URL.
vcf widens the status basis and nothing else (RM169). Given the dated
civic_accepted_and_submitted.vcf from the same release, every emitted row gains the
evidence_status CIViC assigned it, and the submitted evidence items join the corpus. The TSV pair
stays primary and every row is still built from TSV columns — the VCF contributes which items
exist and their status, never an identity: its POS is GRCh37 and lifting it is refused (RM48).
Omitted, the build is exactly what it was, on the accepted basis.
Source code in enricher/src/just_dna_enricher/civic_build.py
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 | |
assert_curation_closes ¶
Each curated row landed in exactly one state, and the states account for the whole table.
The same equality-over-a-walked-set the drop registry gets (@registry-completeness). A build
that quietly stopped consulting the table would otherwise look identical to one where every row
happened to be superseded.
Source code in enricher/src/just_dna_enricher/civic_build.py
assert_registry_closes ¶
Every input row is either kept or counted under a reason, and nothing falls between.
An equality over the walked registry, never a floor (@registry-completeness). A filter added
without a counter beside it would let the build truncate silently, and silent truncation reads as
full coverage — which is the defect the whole drop registry exists to prevent.
Its own function so it can be exercised directly: a test that has to contrive a broken build in order to reach a guard usually ends up proving something else instead.