just_dna_enricher.clinpgx¶
just_dna_enricher.clinpgx ¶
enrich-clinpgx — cross-check drug-response annotations against the ClinPGx snapshot (0.5).
Pass 6, and the offline-capable half of the pharmacogenomics work. pgx.py asks the two nomenclature
authorities about star alleles over the network; this one asks ClinPGx about clinical annotations —
which variant, which drug, at what evidence level — from a local snapshot, exactly as the ClinVar
cross-check does.
The licence comes out of the snapshot, not out of a table. clinpgx_build extracts the
LICENSE.txt ClinPGx bundles inside summaryAnnotations.zip and records its sha256 in
release.json; this pass stamps that hash onto the SourceRow it emits. The recorded terms are
therefore provably the ones shipped with the recorded data, which is the property a static
source→licence map cannot offer — and two halves of such a map went stale inside one release.
What it checks, and what it deliberately does not. It compares the authored evidence_level
against ClinPGx's current one for the same (variant, drug, genotype). That is a currency check, not
an opinion: an evidence level is ClinPGx's own metadata about its own annotation, so ClinPGx is
definitionally right about it and the severity follows the mode ladder. This is unlike the
clin_sig and allele-function checks, which compare two expert judgements and therefore warn in both
modes — here a disagreement means the module is stale, not that two panels differ.
Only for a row compared with the annotation it cites (RM297). A cited id the snapshot does not hold,
and a row citing none, are reported in both modes as withheld and never refused: nothing per row says
the row came from ClinPGx at all, which is RM298's.
It does not check the annotation text, and it does not write pharm_variants.csv. Those tables
are authored _TABLE_KINDS; a network pass filling them would blur the authored/derived line.
ClinPgxEnrichmentError ¶
Bases: RuntimeError
Raised in strict mode when an authored annotation disagrees with the snapshot.
EvidenceConflict
dataclass
¶
EvidenceConflict(
rsid: str | None,
drug: str,
genotype: str | None,
authored: str,
reported: str,
)
An authored evidence level ClinPGx's own record does not support.
WithheldLevel
dataclass
¶
WithheldLevel(
code: str,
rsid: str,
drug: str,
genotype: str | None,
authored: str,
annotation_id: str | None,
held: tuple[str, ...],
)
An authored evidence level this pass could not hold against the record it cites (RM297).
held is what the snapshot carries for the row's (rsid, drug, genotype) — narrowed to its
category where the row states one — sorted, and empty when the snapshot holds nothing there.
load_snapshot ¶
Read the snapshot parquet + its release.json. Returns (rows, release).
Read with duckdb, not polars: the convention is builder in polars, runtime pass in duckdb,
which is what keeps the enricher's declared runtime dependency set honest about what it actually
needs. (Not because polars would be missing — the compiler requires it unconditionally and the
enricher requires the compiler, so it is installed either way. That justification was checked in
the 0.5 audit and is false; the convention stands on its own.) clinvar.py reads its snapshot the
same way.
The column list is the file's, not a subset chosen here. It was hand-written and omitted
gene and annotation_text, both of which clinpgx_build writes for every record — and the
omission then travelled outward as a claim about the source: clinpgx_draft refused --gene
saying "the ClinPGx annotation snapshot carries no gene column" (it does, on 15,331 of 16,087
rows) and synthesized a conclusion restating the row's own key while the published sentence sat
unread in annotation_text (16,087 of 16,087). A hand-kept projection is the same shape as a
hand-kept column list anywhere else in this workspace, with the extra cost that what it drops is
invisible to every reader downstream, who sees only a dict that does not have the key.
Source code in enricher/src/just_dna_enricher/clinpgx.py
enrich_clinpgx ¶
enrich_clinpgx(
spec_dir: Path,
*,
mode: str = "best_effort",
declared_use: str = "unstated",
snapshot: Path | None = None,
offline: bool = False,
download: bool = True,
write: bool = True,
) -> ClinPgxResult
Cross-check pharm_variants.csv against the ClinPGx snapshot and record the terms.
Offline-capable by design: the snapshot is local, so unlike pgx.py this pass needs no network to
read, and the declared-use gate still applies — the terms were accepted when the snapshot was
built, and using it without a declaration is the same act.
Finding a snapshot, though, was the gap (RM38). The builder shipped a release ahead of any
plumbing: there was no locations resolver and no ensure_*, so with no explicit snapshot= this
pass skipped itself and said so — which on a hosted deployment is the check simply never running.
The chain now is: explicit path → resolved cache → provisioned from HuggingFace. The last step is
what offline turns off, and it is why this pass grew an offline parameter it was previously
right not to have: it now has something to decline to do.
Unlike pgx, there is no live fallback and there cannot be — api.pharmgkb.org was retired on
2026-07-20 — which is exactly why provisioning is automatic here and a fallback elsewhere.
Source code in enricher/src/just_dna_enricher/clinpgx.py
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 | |