tablassert 14.0.0__tar.gz → 15.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {tablassert-14.0.0 → tablassert-15.0.0}/PKG-INFO +1 -1
  2. {tablassert-14.0.0 → tablassert-15.0.0}/pyproject.toml +1 -1
  3. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/agent.py +6 -12
  4. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/biolink.py +4 -9
  5. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/lib.py +24 -4
  6. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/models.py +41 -1
  7. {tablassert-14.0.0 → tablassert-15.0.0}/LICENSE +0 -0
  8. {tablassert-14.0.0 → tablassert-15.0.0}/README.md +0 -0
  9. {tablassert-14.0.0 → tablassert-15.0.0}/rust/Cargo.lock +0 -0
  10. {tablassert-14.0.0 → tablassert-15.0.0}/rust/Cargo.toml +0 -0
  11. {tablassert-14.0.0 → tablassert-15.0.0}/rust/examples/count_tables.rs +0 -0
  12. {tablassert-14.0.0 → tablassert-15.0.0}/rust/src/fullmap.rs +0 -0
  13. {tablassert-14.0.0 → tablassert-15.0.0}/rust/src/json.rs +0 -0
  14. {tablassert-14.0.0 → tablassert-15.0.0}/rust/src/lib.rs +0 -0
  15. {tablassert-14.0.0 → tablassert-15.0.0}/rust/src/ndjson.rs +0 -0
  16. {tablassert-14.0.0 → tablassert-15.0.0}/rust/src/uuid.rs +0 -0
  17. {tablassert-14.0.0 → tablassert-15.0.0}/rust/tests/build_golden.rs +0 -0
  18. {tablassert-14.0.0 → tablassert-15.0.0}/rust/tests/common/mod.rs +0 -0
  19. {tablassert-14.0.0 → tablassert-15.0.0}/rust/tests/extract_prebuilt.rs +0 -0
  20. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/__init__.py +0 -0
  21. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/_lazy.py +0 -0
  22. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/cli.py +0 -0
  23. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/coerce.py +0 -0
  24. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/enums.py +0 -0
  25. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/errors.py +0 -0
  26. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/extras.py +0 -0
  27. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/fullmap.py +0 -0
  28. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/graph_target.py +0 -0
  29. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/ingests.py +0 -0
  30. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/log.py +0 -0
  31. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/nlp.py +0 -0
  32. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/progress.py +0 -0
  33. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/qc.py +0 -0
  34. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/rig.py +0 -0
  35. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/rs.pyi +0 -0
  36. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/study.py +0 -0
  37. {tablassert-14.0.0 → tablassert-15.0.0}/src/tablassert/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tablassert
3
- Version: 14.0.0
3
+ Version: 15.0.0
4
4
  Classifier: License :: OSI Approved :: Apache Software License
5
5
  Classifier: Development Status :: 5 - Production/Stable
6
6
  Classifier: Intended Audience :: Science/Research
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tablassert"
3
- version = "14.0.0"
3
+ version = "15.0.0"
4
4
  description = "Extract knowledge assertions from tabular data into NCATS Translator-compliant KGX NDJSON — declaratively, with entity resolution and quality control built in."
5
5
  authors = [
6
6
  { name = "Skye Lane Goetz", email = "sgoetz@isbscience.org" }
@@ -1299,11 +1299,11 @@ def _biolink_report(nodes: Path, edges: Path) -> dict[str, object]:
1299
1299
  Wraps ``biolink.validate_kgx`` (the same check ``tablassert validate-kgx`` runs) into the
1300
1300
  flat, JSON-safe keys ``build_and_audit`` returns, plus the ``_notes`` list the caller
1301
1301
  folds into its own. ``biolink_valid_pct`` excludes the known-pending fields Tablassert
1302
- emits on purpose (``approval_ids`` as a translator-ingest pass-through and the KGX
1303
- denormalized carryovers -- the set is derived, and emptied itself of ``effect_size`` /
1304
- ``effect_type`` when biolink-model 4.4.4 shipped them as real Association slots) so the
1305
- scored number reflects the agent's decisions rather than a deliberate gap;
1306
- ``biolink_valid_pct_strict`` keeps that gap visible.
1302
+ emits on purpose (the KGX denormalized carryovers -- the set is derived, and emptied
1303
+ itself of ``approval_ids`` when the curated pass-through override was removed and of
1304
+ ``effect_size`` / ``effect_type`` when biolink-model 4.4.4 shipped them as real
1305
+ Association slots) so the scored number reflects the agent's decisions rather than a
1306
+ deliberate gap; ``biolink_valid_pct_strict`` keeps that gap visible.
1307
1307
 
1308
1308
  Never raises: an unreadable or unparseable artifact degrades to ``None`` metrics and a
1309
1309
  note, exactly like the coverage measurement above it.
@@ -2349,19 +2349,13 @@ qualifier and evidence slot the specific class declared. build_and_audit reports
2349
2349
  `effect_type`), `q value` / `padj` -> `adjusted_p_value`. Names nothing claims
2350
2350
  (`fold_change` alone, `z_score`, `lfsr`, `standard_error`, free-form notes) are folded
2351
2351
  into `supporting_text`. Prefer `p_value`, `adjusted_p_value`, `effect_size`,
2352
- `effect_type`, `has_evidence`. For FDA application numbers, `approval_ids` is a deliberate
2353
- translator-ingest pass-through: keep the pipe-joined value as a scalar and do not add
2354
- `split_by`.
2352
+ `effect_type`, `has_evidence`.
2355
2353
  - MULTIVALUED slots (`has_evidence` and friends) take a real JSON array, never a joined string:
2356
2354
  `split_by` is the ONLY multivalued encoding — there is no literal-list method. INSPECT the
2357
2355
  column's cells first (read_table shows them); the separator they ACTUALLY use — `|`, `,`, or
2358
2356
  `;` — is the one you declare: `{method: column, encoding: <letter>, split_by: "<separator>"}`.
2359
2357
  A SINGLE-value cell gets NO `split_by`: its scalar wraps into a one-element array, the correct
2360
2358
  shape. Cells that DO join multiple values but OMIT `split_by` ship as one unusable joined blob.
2361
- - `approval_ids` is a deliberate translator-ingest pass-through extra, EXEMPT from the
2362
- validity score: a `biolink_valid_pct` below 1.0 is never caused by it. (`effect_size` /
2363
- `effect_type` were exempt only until biolink-model 4.4.4 shipped them as real Association
2364
- slots; they now validate like any other slot.)
2365
2359
  - QUALIFIERS: enum-ranged qualifiers take a literal TOKEN, never a CURIE
2366
2360
  (`object_direction_qualifier: increased`, not a UMLS id), and `species_context_qualifier` is
2367
2361
  disabled — never author it as a qualifier or annotation.
@@ -396,11 +396,6 @@ def resolve_association_class(category: str, predicate: str) -> type[Any]:
396
396
  # - ``taxon`` -- a node property; no species-context edge is synthesized from it.
397
397
  TABLASERT_EDGE_EXTRAS: frozenset[str] = frozenset(
398
398
  [
399
- # FDA application numbers from translator-ingests (DAKP precedent): the ingest
400
- # emits them as a pipe-joined scalar (e.g. ``"011111|022222"``) with no
401
- # ``split_by``, so the curated extra carries the scalar to the final edge
402
- # verbatim instead of folding it into ``supporting_text``.
403
- "approval_ids",
404
399
  "broad_synonym",
405
400
  "equivalent_identifiers",
406
401
  "evidence_direction",
@@ -574,9 +569,9 @@ against the resolved class is done by ``lib.prune_to_class()``.
574
569
  KNOWN_PENDING_EDGE_FIELDS: frozenset[str] = TABLASERT_EDGE_EXTRAS - frozenset(_association_model_fields())
575
570
  """Curated edge extras the installed Biolink Model does not (yet) declare on any association.
576
571
 
577
- Tablassert emits these deliberately -- ``approval_ids`` as a translator-ingest pass-through,
578
- plus the KGX denormalized carryovers (``synonym``, ``xref``, ``relation``, ...) -- so a Biolink
579
- class rejects them as ``extra_forbidden`` even though the build is behaving as designed. The
572
+ Tablassert emits these deliberately -- the KGX denormalized carryovers (``synonym``,
573
+ ``xref``, ``relation``, ...) -- so a Biolink class rejects them as ``extra_forbidden`` even
574
+ though the build is behaving as designed. The
580
575
  statistical ``effect_size`` / ``effect_type`` pair is no longer pending since
581
576
  biolink-model 4.4.4 shipped PR #1774.
582
577
  :func:`is_pending_problem` uses this set
@@ -765,7 +760,7 @@ def validate_kgx(nodes_path: Path, edges_path: Path, limit: int = 20) -> dict[st
765
760
  Two pass rates are reported. ``valid`` is strict and drives ``ok`` (the CLI's
766
761
  non-zero exit). ``valid_excluding_pending`` additionally counts records whose *every*
767
762
  failure is a :func:`is_pending_problem` -- the score to optimize against, so a
768
- deliberate gap like the curated ``approval_ids`` pass-through is not mistaken for a
763
+ deliberate gap like the KGX denormalized carryovers is not mistaken for a
769
764
  malformed record. The two converge as future model releases absorb curated extras.
770
765
 
771
766
  Args:
@@ -222,7 +222,7 @@ def predicate_options(subject_category: str, object_category: str) -> frozenset[
222
222
  return legal_predicates(derived_edge_category(subject_category, object_category))
223
223
 
224
224
 
225
- def edge_category(lf: pl.LazyFrame, predicate: str | None = None) -> pl.LazyFrame:
225
+ def edge_category(lf: pl.LazyFrame, predicate: str | None = None, category_override: dict[str, str] | None = None) -> pl.LazyFrame:
226
226
  """Add the derived ``category`` column using native polars replace operations.
227
227
 
228
228
  The ``(subject role, object role)`` lookup alone routinely produces a category
@@ -233,10 +233,18 @@ def edge_category(lf: pl.LazyFrame, predicate: str | None = None) -> pl.LazyFram
233
233
  by :func:`biolink.resolve_association_class`, which walks up the association
234
234
  hierarchy only as far as the predicate requires.
235
235
 
236
+ ``category_override`` pins the association class per *object* category (the
237
+ config's ``statement.category_override``): a row whose raw object category --
238
+ before the role rollup that merges e.g. ``Disease`` and ``PhenotypicFeature``
239
+ into one pair key -- appears in the map carries the pinned class instead of the
240
+ pair-derived one. Pinned classes pass through the same predicate reconciliation.
241
+
236
242
  Args:
237
243
  lf: Source LazyFrame with ``subject category`` and ``object category``
238
244
  columns (biolink-prefixed).
239
245
  predicate: Section predicate CURIE used to reconcile the derived category.
246
+ category_override: ``{object category (bare name): biolink: CURIE}`` map
247
+ applied per row in place of the pair lookup.
240
248
 
241
249
  Returns:
242
250
  LazyFrame with a new list-typed ``category`` column containing the
@@ -252,12 +260,20 @@ def edge_category(lf: pl.LazyFrame, predicate: str | None = None) -> pl.LazyFram
252
260
  # small raw-category -> resolved-category remap resolved once at plan time.
253
261
  edge_lookup = {k: f"biolink:{resolve_association_class(v, predicate).__name__}" for k, v in edge_lookup.items()}
254
262
  default = f"biolink:{resolve_association_class(default, predicate).__name__}"
263
+ if category_override:
264
+ category_override = {k: f"biolink:{resolve_association_class(v, predicate).__name__}" for k, v in category_override.items()}
255
265
  names: list[str] = lf.collect_schema().names()
256
266
  subject_col: str = "subject_category" if "subject_category" in names else "subject category"
257
267
  object_col: str = "object_category" if "object_category" in names else "object category"
258
268
  sr: pl.Expr = pl.col(subject_col).str.replace("biolink:", "").replace(cat_role).fill_null("")
259
269
  or_: pl.Expr = pl.col(object_col).str.replace("biolink:", "").replace(cat_role).fill_null("")
260
- return lf.with_columns(pl.concat_list(pl.concat_str([sr, pl.lit("|"), or_]).replace_strict(edge_lookup, default=default)).alias("category"))
270
+ derived: pl.Expr = pl.concat_str([sr, pl.lit("|"), or_]).replace_strict(edge_lookup, default=default)
271
+ if category_override:
272
+ # Key on the RAW object category: the role rollup above collapses exactly the
273
+ # distinction (Disease vs PhenotypicFeature) the override exists to preserve.
274
+ hit: pl.Expr = pl.col(object_col).str.replace("biolink:", "").replace_strict(category_override, default=None, return_dtype=pl.String)
275
+ derived = pl.when(hit.is_not_null()).then(hit).otherwise(derived)
276
+ return lf.with_columns(pl.concat_list(derived).alias("category"))
261
277
 
262
278
 
263
279
  def prune_to_class(lf: pl.LazyFrame) -> pl.LazyFrame:
@@ -1177,7 +1193,7 @@ class Tcode(Section):
1177
1193
  [
1178
1194
  op
1179
1195
  for x in self.annotations
1180
- for op in [*self.encoding(x, x.annotation.lower()), *([(split_list, (x.annotation.lower(), x.split_by))] if x.split_by else [])]
1196
+ for op in [*self.encoding(x, x.annotation), *([(split_list, (x.annotation, x.split_by))] if x.split_by else [])]
1181
1197
  ]
1182
1198
  if self.annotations
1183
1199
  else None,
@@ -1297,9 +1313,13 @@ class Tcode(Section):
1297
1313
  study_name: str | None = sheet if sheet is not None else Path(self.source.local).name
1298
1314
  if study_name == study_id:
1299
1315
  study_name = None
1316
+ # `use_enum_values` stores the override's enum keys/values as plain strings.
1317
+ category_override: dict[str, str] | None = (
1318
+ {obj: f"biolink:{pinned}" for obj, pinned in self.statement.category_override.items()} if self.statement.category_override else None
1319
+ )
1300
1320
  return [
1301
1321
  (value, ("predicate", "biolink:" + self.statement.predicate)),
1302
- (edge_category, ("biolink:" + self.statement.predicate,)),
1322
+ (edge_category, ("biolink:" + self.statement.predicate, category_override)),
1303
1323
  (value, ("knowledge_level", knowledge_level)),
1304
1324
  (value, ("agent_type", agent_type)),
1305
1325
  # Retrieval provenance lives only in the nested `sources` list (Biolink
@@ -17,9 +17,11 @@ from tablassert.biolink import (
17
17
  UNSATISFIABLE_EDGE_FIELDS,
18
18
  AgentTypes,
19
19
  Categories,
20
+ EdgeCategories,
20
21
  KnowledgeLevels,
21
22
  Predicates,
22
23
  Qualifiers,
24
+ resolve_association_class,
23
25
  )
24
26
  from tablassert.coerce import coerced_target
25
27
  from tablassert.enums import (
@@ -435,6 +437,14 @@ class Statement(TablaBase):
435
437
  object: NodeEncoding = Field(..., description="Object node encoding and mapping configuration.")
436
438
  predicate: Predicates = Field(Predicates.RELATED_TO, description="Predicate connecting subject and object nodes.")
437
439
  qualifiers: list[Qualifier] | None = Field(None, description="Optional qualifier nodes attached to the statement.")
440
+ category_override: dict[Categories, EdgeCategories] | None = Field(
441
+ None,
442
+ description=(
443
+ "Optional per-object-category association-class override: maps a resolved object category (bare name, e.g. `Disease`) "
444
+ "to the association category its rows carry (e.g. `EntityToDiseaseAssociation`), used in place of the derived "
445
+ "(subject, object) pair lookup. Rows whose object category is absent from the map derive as before."
446
+ ),
447
+ )
438
448
 
439
449
  @model_validator(mode="after")
440
450
  def reject_duplicate_qualifiers(self: Self) -> Self:
@@ -458,6 +468,30 @@ class Statement(TablaBase):
458
468
  seen.add(key)
459
469
  return self
460
470
 
471
+ @model_validator(mode="after")
472
+ def warn_when_an_override_is_demoted_by_the_predicate(self: Self) -> Self:
473
+ """Warn when a pinned association class cannot carry the section predicate.
474
+
475
+ A ``category_override`` value still passes through
476
+ :func:`biolink.resolve_association_class` at build time: when the pinned class
477
+ restricts its ``predicate`` slot and the section predicate is not in it, the
478
+ emitted category silently walks up to an ancestor -- taking every subclass-only
479
+ slot the author pinned the class *for* with it. The value is never lost (the
480
+ reconciliation keeps the edge valid), so this is a warning, not an error.
481
+ """
482
+ for obj_category, pinned in (self.category_override or {}).items():
483
+ # `use_enum_values` stores both sides as plain strings.
484
+ resolved: str = resolve_association_class(f"biolink:{pinned}", f"biolink:{self.predicate}").__name__
485
+ if resolved != pinned:
486
+ warnings.warn(
487
+ f"category_override pins `{pinned}` for `{obj_category}` objects, but that class does not accept "
488
+ f"predicate `{self.predicate}`, so those rows are emitted as `{resolved}` instead -- any slots only "
489
+ f"`{pinned}` declares are pruned off the edge.",
490
+ BiolinkRelocationWarning,
491
+ stacklevel=2,
492
+ )
493
+ return self
494
+
461
495
 
462
496
  def validate_infores_curie(value: str, code: TablassertErrorCodes) -> str:
463
497
  """Validate an ``infores:`` CURIE used for Biolink knowledge-source fields."""
@@ -674,7 +708,13 @@ class Annotation(Encoding):
674
708
  @field_validator("annotation", mode="after")
675
709
  @classmethod
676
710
  def clean_annotation(cls, annotation: str) -> str:
677
- return annotation.lower().strip()
711
+ cleaned: str = annotation.strip()
712
+ lowered: str = cleaned.lower()
713
+ # Allow-listed slots may carry uppercase (Biolink's ``FDA_regulatory_approvals``):
714
+ # any casing the author declares canonicalizes onto the allow-listed spelling
715
+ # verbatim, so the emitted edge field preserves its case exactly instead of being
716
+ # lowercased into an unknown name.
717
+ return next((field for field in ALLOWED_EDGE_FIELDS if field.lower() == lowered), lowered)
678
718
 
679
719
  @model_validator(mode="after")
680
720
  def reject_disabled_annotations(self: Self) -> Self:
File without changes
File without changes
File without changes
File without changes
File without changes