tablassert 7.5.0__tar.gz → 7.5.2__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 (65) hide show
  1. tablassert-7.5.2/AGENTS.md +57 -0
  2. {tablassert-7.5.0 → tablassert-7.5.2}/CHANGELOG.md +19 -0
  3. {tablassert-7.5.0 → tablassert-7.5.2}/PKG-INFO +1 -1
  4. tablassert-7.5.2/docs/changelog.md +13 -0
  5. {tablassert-7.5.0 → tablassert-7.5.2}/pyproject.toml +2 -2
  6. {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/cli.py +18 -14
  7. {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/lib.py +51 -9
  8. {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/progress.py +13 -5
  9. {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_lib.py +165 -1
  10. tablassert-7.5.2/tests/test_progress.py +48 -0
  11. {tablassert-7.5.0 → tablassert-7.5.2}/uv.lock +5 -5
  12. tablassert-7.5.0/AGENTS.md +0 -50
  13. tablassert-7.5.0/docs/changelog.md +0 -25
  14. {tablassert-7.5.0 → tablassert-7.5.2}/.github/workflows/docker.yml +0 -0
  15. {tablassert-7.5.0 → tablassert-7.5.2}/.github/workflows/docs.yml +0 -0
  16. {tablassert-7.5.0 → tablassert-7.5.2}/.github/workflows/pipy.yml +0 -0
  17. {tablassert-7.5.0 → tablassert-7.5.2}/.gitignore +0 -0
  18. {tablassert-7.5.0 → tablassert-7.5.2}/.pre-commit-config.yaml +0 -0
  19. {tablassert-7.5.0 → tablassert-7.5.2}/CITATION.cff +0 -0
  20. {tablassert-7.5.0 → tablassert-7.5.2}/CONTRIBUTING.md +0 -0
  21. {tablassert-7.5.0 → tablassert-7.5.2}/Dockerfile +0 -0
  22. {tablassert-7.5.0 → tablassert-7.5.2}/LICENSE +0 -0
  23. {tablassert-7.5.0 → tablassert-7.5.2}/README.md +0 -0
  24. {tablassert-7.5.0 → tablassert-7.5.2}/docs/api/fullmap.md +0 -0
  25. {tablassert-7.5.0 → tablassert-7.5.2}/docs/api/lib.md +0 -0
  26. {tablassert-7.5.0 → tablassert-7.5.2}/docs/api/qc.md +0 -0
  27. {tablassert-7.5.0 → tablassert-7.5.2}/docs/api/utils.md +0 -0
  28. {tablassert-7.5.0 → tablassert-7.5.2}/docs/cli.md +0 -0
  29. {tablassert-7.5.0 → tablassert-7.5.2}/docs/configuration/advanced-example.md +0 -0
  30. {tablassert-7.5.0 → tablassert-7.5.2}/docs/configuration/graph.md +0 -0
  31. {tablassert-7.5.0 → tablassert-7.5.2}/docs/configuration/table.md +0 -0
  32. {tablassert-7.5.0 → tablassert-7.5.2}/docs/datassert.md +0 -0
  33. {tablassert-7.5.0 → tablassert-7.5.2}/docs/docker.md +0 -0
  34. {tablassert-7.5.0 → tablassert-7.5.2}/docs/examples/tutorial-data.csv +0 -0
  35. {tablassert-7.5.0 → tablassert-7.5.2}/docs/examples/tutorial-graph.yaml +0 -0
  36. {tablassert-7.5.0 → tablassert-7.5.2}/docs/examples/tutorial-table.yaml +0 -0
  37. {tablassert-7.5.0 → tablassert-7.5.2}/docs/examples.md +0 -0
  38. {tablassert-7.5.0 → tablassert-7.5.2}/docs/index.md +0 -0
  39. {tablassert-7.5.0 → tablassert-7.5.2}/docs/installation.md +0 -0
  40. {tablassert-7.5.0 → tablassert-7.5.2}/docs/tutorial.md +0 -0
  41. {tablassert-7.5.0 → tablassert-7.5.2}/llms.txt +0 -0
  42. {tablassert-7.5.0 → tablassert-7.5.2}/mkdocs.yml +0 -0
  43. {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/__init__.py +0 -0
  44. {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/downloader.py +0 -0
  45. {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/enums.py +0 -0
  46. {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/fullmap.py +0 -0
  47. {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/ingests.py +0 -0
  48. {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/log.py +0 -0
  49. {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/models.py +0 -0
  50. {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/nlp.py +0 -0
  51. {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/qc.py +0 -0
  52. {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/utils.py +0 -0
  53. {tablassert-7.5.0 → tablassert-7.5.2}/tests/__init__.py +0 -0
  54. {tablassert-7.5.0 → tablassert-7.5.2}/tests/conftest.py +0 -0
  55. {tablassert-7.5.0 → tablassert-7.5.2}/tests/fixtures/invalid_section_missing_source.yaml +0 -0
  56. {tablassert-7.5.0 → tablassert-7.5.2}/tests/fixtures/minimal_section.yaml +0 -0
  57. {tablassert-7.5.0 → tablassert-7.5.2}/tests/fixtures/minimal_section_with_sections.yaml +0 -0
  58. {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_downloader.py +0 -0
  59. {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_enums.py +0 -0
  60. {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_fullmap.py +0 -0
  61. {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_ingests.py +0 -0
  62. {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_models.py +0 -0
  63. {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_nlp.py +0 -0
  64. {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_qc.py +0 -0
  65. {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_utils.py +0 -0
@@ -0,0 +1,57 @@
1
+ # AGENTS.md — Tablassert
2
+
3
+ ## Fast Start
4
+
5
+ - Python package, not a monorepo. Main code lives in `src/tablassert/`; tests live in `tests/`.
6
+ - Install with `uv sync`. Three optional extras:
7
+ - `--extra qc` / `--extra qc-cuda` — installs `onnxruntime` / `onnxruntime-gpu` for QC (strict runtime behavior below).
8
+ - `--extra rt` — runtime-compatible Polars build for CPUs missing required SIMD instructions.
9
+ - CLI entrypoint is `tablassert.cli:APP`. Real user commands:
10
+ - `uv run tablassert build <graph.yaml>` — 6 pipeline stages.
11
+ - `uv run tablassert validate <table.yaml>` — 3 stages, syntax-only.
12
+
13
+ ## Source of Truth
14
+
15
+ - When prose docs and code disagree, `src/tablassert/models.py` and `src/tablassert/cli.py` are authoritative. Docs drift: some pages claim shard files are `{0..11}.duckdb` or that only the `rt` extra exists; the code uses `SHARDS = 10` (so `0..9`) and exposes `qc`/`qc-cuda`/`rt` extras.
16
+
17
+ ## Verify Changes
18
+
19
+ - Match the repo hooks before finishing: `uv run ruff check --fix .`, `uv run ruff format .`, `uv run pyright`, `uv run pytest`.
20
+ - Full hook run: `uv run pre-commit run --all-files` (ruff, ruff-format, pyright, pytest).
21
+ - Focused test runs:
22
+ - Single test: `uv run pytest tests/test_lib.py::test_name`
23
+ - By keyword: `uv run pytest -k "pattern"`
24
+ - With print output: `uv run pytest -s tests/test_lib.py`
25
+ - Docs build: `uv run --group dev mkdocs build`.
26
+
27
+ ## High-Value Structure
28
+
29
+ - `src/tablassert/cli.py` is the wiring layer: `build()` → `build_pipeline()`, `validate()` → `validate_pipeline()`.
30
+ - `src/tablassert/ingests.py` loads YAML and expands table configs into section dicts.
31
+ - `src/tablassert/lib.py` is the core pipeline:
32
+ - `Tcode.collect()` builds the per-section operation list.
33
+ - `compile_subgraph()` executes that list into parquet.
34
+ - `compile_graph()` aggregates subgraph parquets into KGX NDJSON.
35
+ - `resolve_many()` is the direct library API for batch entity resolution.
36
+ - Entity resolution uses DuckDB shard files at `<datassert>/data/{0..9}.duckdb`, opened read-only. `datassert` is a required `Path` field on the `Graph` model (not a fixed location); `src/tablassert/fullmap.py` hardcodes `SHARDS = 10`.
37
+
38
+ ## Repo-Specific Gotchas
39
+
40
+ - Heavy dependencies are lazy-loaded per module with `TYPE_CHECKING` + `lazy_loader`. Follow the existing pattern instead of importing heavy packages eagerly. Lazy-loaded: polars, duckdb, orjson, xxhash, polars_hash, yaml, httpx, pyexcel, onnxruntime, sentence_transformers.
41
+ - `tests/conftest.py` autouse-mocks `httpx.head`, so model-URL validation tests never hit the network unless a test opts in.
42
+ - Network-dependent tests are marked `@pytest.mark.network`; GPU QC tests are marked with both `network` and `gpu` in `tests/test_qc.py`.
43
+ - QC runtime selection is strict in `src/tablassert/qc.py`: if `onnxruntime-gpu` is installed but `CUDAExecutionProvider` is unavailable, the code raises (error 06) instead of falling back to CPU. Install `tablassert[qc]` for CPU-only.
44
+ - Downloader behavior in `src/tablassert/downloader.py` is two-path: `direct()` via `httpx` for known file URLs, `browser()` via Playwright fallback for browser-only sources. Keep payload-validation and cleanup tests intact when changing it.
45
+
46
+ ## Conventions That Matter Here
47
+
48
+ - Start every module with `from __future__ import annotations`.
49
+ - Annotate every variable, including locals.
50
+ - Use `Optional[T]` / `Union[...]`, not `T | None`. Use `Path`, not `str`, for filesystem paths. Use `# pyright: ignore` to silence lazy-load false positives.
51
+ - No docstrings on functions. Use comment markers on the line above: `# ?` description, `# !` warning, `# *` pipeline stage, `# TODO:` todo.
52
+ - Pydantic models inherit `TablaBase` (`extra="forbid"`, `validate_assignment=True`). Required fields use `Field(...)`; optional use `Optional[T] = Field(None)`. Enums extend `str, Enum` and live in `enums.py`.
53
+ - Per-module logger: `from tablassert.log import cat; logger = cat("MODULE")` (e.g. `cat("FULLMAP")`). The CLI uses the root `from tablassert.log import logger`. Raise `RuntimeError` for failures; `logger.warning()` for non-fatal issues.
54
+
55
+ ## Side Effects
56
+
57
+ - The package writes working artifacts to hidden dirs in the repo root, each defined in code: `.storassert/` (`utils.STORE`), `.logassert/` (`log.LOGASSERT`), `.cachassert/` (`models.CACHE`), `.onnxassert/` (`qc.MODEL`).
@@ -2,6 +2,25 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 7.5.2 - 2026-07-01
6
+
7
+ ### Changed
8
+ - `sig()` in `lib.py` now emits a third significance label, `"INCONCLUSIVE"`, for p-values that fall between the existing significance `cutoff` (default `0.05`, inclusive) and a new `threshold` parameter (default `0.10`, exclusive). A p-value `p` is now mapped as: null → `"UNSURE"`; `p <= cutoff` → `"YES"`; `cutoff < p < threshold` → `"INCONCLUSIVE"`; `p >= threshold` → `"NO"`. The upper bound is exclusive so `0.10` (and the `0.1` `NO` cases in the existing tests) continue to map to `"NO"`. `threshold` is added to `sig()`'s signature alongside `cutoff`; the function remains wired into `Tcode.collect()` at its default arguments, so builds are unaffected unless a caller overrides the new bound.
9
+
10
+ ### Added
11
+ - One regression test in `test_lib.py` (`test_sig_marks_inconclusive_band`) asserting all four bands in a single frame: a value at/below cutoff (`YES`), a value in the inconclusive range (`INCONCLUSIVE`), and a value at the exclusive threshold (`NO`).
12
+
13
+ ## 7.5.1 - 2026-07-01
14
+
15
+ ### Changed
16
+ - Numeric annotation columns are now coerced and emitted as controlled-notation strings in NDJSON output instead of raw values. Two new pipeline steps wired into `Tcode.collect()` (`lib.py`): `clean_numeric()` lazily casts matching columns to `Float64` with `strict=False` (non-numeric entries drop to null), and `format_numeric()` renders them as strings — p-value columns (any name containing `"p value"`, case-insensitive) in scientific notation (`{:.4e}`), and `relationship strength` / `sample size` in decimal general format (`{:.4g}`, ≥4 significant figures). Non-matching columns are left untouched, and nulls are subsequently dropped by `strip_nulls()`. `math_op()` now also casts with `strict=False` so it tolerates residual junk in numeric annotation columns. `format_numeric()` formats via numpy-backed batch conversion rather than `map_elements` for throughput.
17
+ - Removed dead `pl.Config(set_fmt_float=...)` and `pl.Config(float_precision=...)` context managers from `compile_graph()` (`lib.py`); they were no-ops for NDJSON serialization (`write_ndjson` emits raw f64 via serde shortest-repr and ignores float display options), and the `fmt`/`precision` parameters of `compile_graph()` were removed alongside them.
18
+ - Fixed an off-by-one in the build/validate progress bar so each section loop now shows the configuration currently being processed instead of the last-completed one. `PipelineProgress.section_loop()` (`progress.py`) previously returned a single `advance(info)` callback that set the description and ticked the completed counter together, called after each item's work — so while section *K* ran the bar still displayed section *K−1*. It now returns a `(start, advance)` pair: `start(info)` updates the description to the in-flight item without incrementing, and `advance()` ticks the counter afterwards (so the counter never claims an in-flight item is complete). All five call sites in `cli.py` (TCode build, Collect, Subgraph, Graph, Validate) were updated to `start(...)` before the work and `advance()` after; the long-running Collect and Subgraph stages continue to show the full `format_section_oneline()` summary (including the `CONFIG` name) of the in-flight section.
19
+
20
+ ### Added
21
+ - Fifteen regression tests in `test_lib.py` covering `numeric_columns()` detection (p-value substring, exact-name match, case-insensitivity), `clean_numeric()` (parse/coerce numeric and scientific notation, null out non-numeric junk, leave non-matching columns untouched, noop, idempotent on Float64), `format_numeric()` (scientific notation for p-value, decimal general format for relationship strength/sample size, null preservation, floating-point-noise cleaning, noop), null-stripped NDJSON rows, `compile_graph()` NDJSON emission, and `sig()` operating over a cleaned Float64 p-value column.
22
+ - Three regression tests in `tests/test_progress.py` pinning the new two-callback contract: `start` shows the in-flight item with the counter still at zero, `advance` ticks the counter without altering the description, and a `start`/`advance` cycle keeps the description synced to the current item rather than the previous one.
23
+
5
24
  ## 7.5.0 - 2026-07-01
6
25
 
7
26
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tablassert
3
- Version: 7.5.0
3
+ Version: 7.5.2
4
4
  Summary: Extract knowledge assertions from tabular data into NCATS Translator-compliant KGX NDJSON — declaratively, with entity resolution and quality control built in.
5
5
  Project-URL: Homepage, https://github.com/SkyeAv/Tablassert
6
6
  Project-URL: Source, https://github.com/SkyeAv/Tablassert
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ The canonical release history lives in the repository root at [`CHANGELOG.md`](https://github.com/SkyeAv/Tablassert/blob/main/CHANGELOG.md).
4
+
5
+ ## Current Release Notes
6
+
7
+ ## 7.5.2 - 2026-07-01
8
+
9
+ ### Changed
10
+ - `sig()` in `lib.py` now emits a third significance label, `"INCONCLUSIVE"`, for p-values that fall between the existing significance `cutoff` (default `0.05`, inclusive) and a new `threshold` parameter (default `0.10`, exclusive). A p-value `p` is now mapped as: null → `"UNSURE"`; `p <= cutoff` → `"YES"`; `cutoff < p < threshold` → `"INCONCLUSIVE"`; `p >= threshold` → `"NO"`. The upper bound is exclusive so `0.10` (and the `0.1` `NO` cases in the existing tests) continue to map to `"NO"`. `threshold` is added to `sig()`'s signature alongside `cutoff`; the function remains wired into `Tcode.collect()` at its default arguments, so builds are unaffected unless a caller overrides the new bound.
11
+
12
+ ### Added
13
+ - One regression test in `test_lib.py` (`test_sig_marks_inconclusive_band`) asserting all four bands in a single frame: a value at/below cutoff (`YES`), a value in the inconclusive range (`INCONCLUSIVE`), and a value at the exclusive threshold (`NO`).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tablassert"
3
- version = "7.5.0"
3
+ version = "7.5.2"
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" }
@@ -92,7 +92,7 @@ qc-cuda = [
92
92
  dev = [
93
93
  "mkdocs>=1.6.1",
94
94
  "pre-commit>=4.5.1",
95
- "pyright>=1.1.408",
95
+ "pyright>=1.1.411",
96
96
  "pytest>=9.0.2",
97
97
  "pytest-cov>=7.1.0",
98
98
  "ruff>=0.15.6",
@@ -59,20 +59,20 @@ def build_pipeline(graph_configuration_file: Path, progress: "PipelineProgress")
59
59
 
60
60
  # * Build TCode (3/6)
61
61
  progress.stage(f"Building TCode | Sections: {n}")
62
- advance = progress.section_loop(n, "TCode")
62
+ start, advance = progress.section_loop(n, "TCode")
63
63
  tcode: list[Tcode] = []
64
64
  for idx, s in enumerate(sections, start=1):
65
+ h: str = mkhash(s)
66
+ start(f"#{idx} | CONFIG: {Path(s['config']).name} | HASH: {h}")
65
67
  try:
66
68
  tcode.append(
67
- Tcode.model_validate(
68
- {**s, "number": idx, "store": (STORE / f"{mkhash(s)}.parquet"), "log": g.log, "qc": g.qc}
69
- )
69
+ Tcode.model_validate({**s, "number": idx, "store": (STORE / f"{h}.parquet"), "log": g.log, "qc": g.qc})
70
70
  )
71
71
  except pydantic.ValidationError as e:
72
72
  raise RuntimeError(
73
- f"02 | FAILED VALIDATION | CONFIG: {graph_configuration_file} | IDX: {idx} | HASH: {mkhash(s)} | PYDANTIC: {flatten_pydantic_error(e)}"
73
+ f"02 | FAILED VALIDATION | CONFIG: {graph_configuration_file} | IDX: {idx} | HASH: {h} | PYDANTIC: {flatten_pydantic_error(e)}"
74
74
  ) from e
75
- advance(format_section_oneline(tcode[-1]))
75
+ advance()
76
76
 
77
77
  with ExitStack() as stack:
78
78
  conns: list[object] = [
@@ -82,25 +82,28 @@ def build_pipeline(graph_configuration_file: Path, progress: "PipelineProgress")
82
82
 
83
83
  # * Collect Instructions (4/6)
84
84
  progress.stage(f"Collecting Instructions | Sections: {n}")
85
- advance = progress.section_loop(n, "Collect")
85
+ start, advance = progress.section_loop(n, "Collect")
86
86
  instructions: list[Any] = []
87
87
  for x in tcode:
88
+ start(format_section_oneline(x))
88
89
  instructions.append(x.collect(conns, g.pubmed_db, g.pmc_db)) # pyright: ignore
89
- advance(format_section_oneline(x))
90
+ advance()
90
91
 
91
92
  # * Build Subgraphs (5/6)
92
93
  progress.stage(f"Building Subgraphs | Sections: {n}")
93
- advance = progress.section_loop(n, "Subgraph")
94
+ start, advance = progress.section_loop(n, "Subgraph")
94
95
  subgraphs: list[Path] = []
95
96
  for x, op in zip(tcode, instructions):
97
+ start(format_section_oneline(x))
96
98
  subgraphs.append(op if isinstance(op, Path) else compile_subgraph(op))
97
- advance(format_section_oneline(x))
99
+ advance()
98
100
 
99
101
  # * Compile Graph (6/6)
100
102
  progress.stage(f"Compiling Graph | Sections: {n}")
101
- advance = progress.section_loop(1, "Graph")
103
+ start, advance = progress.section_loop(1, "Graph")
104
+ start(f"NAME: {g.name} | VERSION: {g.version}")
102
105
  compile_graph(subgraphs, g.name, g.version)
103
- advance(f"NAME: {g.name} | VERSION: {g.version}")
106
+ advance()
104
107
 
105
108
  logger.info(f"BUILD DONE | SECTIONS: {n} | NAME: {g.name} | VERSION: {g.version}")
106
109
 
@@ -123,16 +126,17 @@ def validate_pipeline(table_configuration_file: Path, progress: "PipelineProgres
123
126
 
124
127
  # * Validate Section Syntax (3/3)
125
128
  progress.stage(f"Validating Section Syntax | Sections: {n}")
126
- advance = progress.section_loop(n, "Validate")
129
+ start, advance = progress.section_loop(n, "Validate")
127
130
  for idx, s in enumerate(sections, start=1):
128
131
  h: str = mkhash(s)
132
+ start(f"#{idx} | HASH: {h}")
129
133
  try:
130
134
  Tcode.model_validate({**s, "number": idx, "store": (STORE / f"{h}.parquet")})
131
135
  except pydantic.ValidationError as e:
132
136
  raise RuntimeError(
133
137
  f"02 | FAILED VALIDATION | CONFIG: {table_configuration_file} | IDX: {idx} | HASH: {h} | PYDANTIC: {flatten_pydantic_error(e)}"
134
138
  ) from e
135
- advance(f"#{idx} | HASH: {h}")
139
+ advance()
136
140
 
137
141
  logger.info(f"VALIDATE DONE | SECTIONS: {n} | CONFIG: {table_configuration_file.name}")
138
142
 
@@ -5,7 +5,7 @@ import operator
5
5
  from collections.abc import Iterable
6
6
  from contextlib import ExitStack
7
7
  from functools import reduce
8
- from operator import add, eq, le
8
+ from operator import add, eq, le, lt
9
9
  from os.path import basename
10
10
  from pathlib import Path
11
11
  from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Self, Union
@@ -24,11 +24,13 @@ from tablassert.utils import namespace_uuid
24
24
 
25
25
  if TYPE_CHECKING:
26
26
  import duckdb
27
+ import numpy as np
27
28
  import orjson
28
29
  import polars as pl
29
30
  import xxhash
30
31
  else:
31
32
  duckdb = Lazy.load("duckdb")
33
+ np = Lazy.load("numpy")
32
34
  orjson = Lazy.load("orjson")
33
35
  pl = Lazy.load("polars")
34
36
  xxhash = Lazy.load("xxhash")
@@ -56,8 +58,9 @@ def math_op(
56
58
  ) -> pl.LazyFrame:
57
59
  # ? Transform Values In A Column With The Math Module
58
60
  # ! Collection Point: Required For map_elements
61
+ # * strict=False tolerates residual non numeric junk in numeric annotation columns
59
62
  df: pl.DataFrame = lf.collect()
60
- expr: pl.Expr = pl.col(col).cast(pl.Float64)
63
+ expr: pl.Expr = pl.col(col).cast(pl.Float64, strict=False)
61
64
  attr: Callable[[Any], Any] = getattr(math, func)
62
65
  df = df.with_columns(
63
66
  expr.map_elements(
@@ -67,6 +70,38 @@ def math_op(
67
70
  return df.lazy()
68
71
 
69
72
 
73
+ def numeric_columns(names: list[str]) -> list[str]:
74
+ # ? Returns Column Names That Should Be Coerced And Formatted As Numbers
75
+ # * P Value Columns By Substring Plus Exact Relationship Strength And Sample Size
76
+ exact: set[str] = {"relationship strength", "sample size"}
77
+ return [c for c in names if ("p value" in c.lower()) or (c in exact)]
78
+
79
+
80
+ def clean_numeric(lf: pl.LazyFrame) -> pl.LazyFrame:
81
+ # ? Coerces Numeric Annotation Columns To Float64 Dropping Non Numeric Values To Null
82
+ # * Only Touches P Value Relationship Strength And Sample Size Columns
83
+ cols: list[str] = numeric_columns(lf.collect_schema().names())
84
+ if not cols:
85
+ return lf
86
+ return lf.with_columns([pl.col(c).cast(pl.Float64, strict=False) for c in cols])
87
+
88
+
89
+ def format_numeric(lf: pl.LazyFrame) -> pl.LazyFrame:
90
+ # ? Formats Numeric Annotation Columns As Strings With Controlled Notation
91
+ # ! Collection Point: numpy Batch Formatting Required For Notation Control
92
+ # * P Value Columns Use Scientific Notation Others Use Decimal General Format
93
+ df: pl.DataFrame = lf.collect()
94
+ cols: list[str] = numeric_columns(df.columns)
95
+ for c in cols:
96
+ df = df.with_columns(pl.col(c).cast(pl.Float64, strict=False).alias(c))
97
+ mask: object = df[c].is_null().to_numpy()
98
+ arr: object = df[c].to_numpy()
99
+ fmt: str = "{:.4e}" if "p value" in c.lower() else "{:.4g}"
100
+ formatted: list[Optional[str]] = [None if m else fmt.format(float(v)) for v, m in zip(arr, mask)] # pyright: ignore
101
+ df = df.with_columns(pl.Series(c, formatted))
102
+ return df.lazy()
103
+
104
+
70
105
  def prefix(lf: pl.LazyFrame, col: str, prefix: str) -> pl.LazyFrame:
71
106
  expr: pl.Expr = add(pl.lit(prefix), pl.col(col).cast(pl.String))
72
107
  return lf.with_columns(expr.alias(col))
@@ -97,6 +132,7 @@ def explode(lf: pl.LazyFrame, col: str, delimiter: str) -> pl.LazyFrame:
97
132
  def sig(
98
133
  lf: pl.LazyFrame,
99
134
  cutoff: float = 0.05, # pyright: ignore
135
+ threshold: float = 0.10,
100
136
  col: str = "p value",
101
137
  out: str = "significant",
102
138
  ) -> pl.LazyFrame:
@@ -110,7 +146,13 @@ def sig(
110
146
  expr: pl.Expr = pl.col(chosen).cast(pl.Float64, strict=False)
111
147
  cond: pl.Expr = le(expr, cutoff)
112
148
  cutoff: pl.Expr = (
113
- pl.when(expr.is_null()).then(pl.lit("UNSURE")).when(cond).then(pl.lit("YES")).otherwise(pl.lit("NO"))
149
+ pl.when(expr.is_null())
150
+ .then(pl.lit("UNSURE"))
151
+ .when(cond)
152
+ .then(pl.lit("YES"))
153
+ .when(lt(expr, threshold))
154
+ .then(pl.lit("INCONCLUSIVE"))
155
+ .otherwise(pl.lit("NO"))
114
156
  )
115
157
  return lf.with_columns(cutoff.alias(out))
116
158
  else:
@@ -347,6 +389,7 @@ class Tcode(Section):
347
389
  if self.source.reindex
348
390
  else None,
349
391
  [op for x in self.annotations for op in self.encoding(x, x.annotation)] if self.annotations else None,
392
+ (clean_numeric, ()),
350
393
  self.node(self.statement.subject, "subject", conns),
351
394
  self.node(self.statement.object, "object", conns),
352
395
  (value, ("predicate", add("biolink:", self.statement.predicate))),
@@ -367,6 +410,7 @@ class Tcode(Section):
367
410
  (with_captions, (pmc_db, self.provenance.publication, str(self.source.url))) if pmc_db else None,
368
411
  (sig, ()),
369
412
  (trim, ()),
413
+ (format_numeric, ()),
370
414
  (to_store, (self.store, self.config.name)),
371
415
  ]
372
416
  return self.clean(tcode)
@@ -462,7 +506,7 @@ def dedup_stream(p_in: Path, is_edges: bool) -> None:
462
506
  p_in.unlink()
463
507
 
464
508
 
465
- def compile_graph(subgraphs: list[Path], name: str, version: str, fmt: str = "mixed", precision: int = 4) -> None:
509
+ def compile_graph(subgraphs: list[Path], name: str, version: str) -> None:
466
510
  # ? Aggregates Parquets For NDJSON KGX Export Using Lazy Scan
467
511
  p: Path = Path(f"./{name}_{version}.tmp")
468
512
 
@@ -497,11 +541,9 @@ def compile_graph(subgraphs: list[Path], name: str, version: str, fmt: str = "mi
497
541
  eagernode.write_ndjson(f)
498
542
 
499
543
  with e.open("a") as f:
500
- with pl.Config(set_fmt_float=fmt): # pyright: ignore
501
- with pl.Config(float_precision=precision):
502
- for subedge in subedges:
503
- eageredge: pl.DataFrame = subedge.collect().unique()
504
- eageredge.write_ndjson(f)
544
+ for subedge in subedges:
545
+ eageredge: pl.DataFrame = subedge.collect().unique()
546
+ eageredge.write_ndjson(f)
505
547
 
506
548
  dedup_stream(e, is_edges=True)
507
549
  dedup_stream(n, is_edges=False)
@@ -106,15 +106,23 @@ class PipelineProgress(AbstractContextManager["PipelineProgress"]):
106
106
  )
107
107
  self.end_section_task()
108
108
 
109
- def section_loop(self: "PipelineProgress", total: int, label: str) -> Callable[[str], None]:
109
+ def section_loop(
110
+ self: "PipelineProgress", total: int, label: str
111
+ ) -> tuple[Callable[[str], None], Callable[[], None]]:
112
+ # ? Returns A start Callback To Mark The In Flight Item And An advance Callback To Tick The Counter
113
+ # ! start Updates The Description Without Incrementing So The Bar Shows The Item Being Processed
110
114
  self.end_section_task()
111
- self.section_task = self.progress.add_task(description=f"{label.upper()} | WORKING", total=total)
115
+ self.section_task = self.progress.add_task(description=f"{label.upper()} | STARTING", total=total)
112
116
 
113
- def advance(info: str) -> None:
117
+ def start(info: str) -> None:
114
118
  assert self.section_task is not None
115
- self.progress.update(self.section_task, description=f"{label.upper()} | {info}", advance=1)
119
+ self.progress.update(self.section_task, description=f"{label.upper()} | {info}")
116
120
 
117
- return advance
121
+ def advance() -> None:
122
+ assert self.section_task is not None
123
+ self.progress.update(self.section_task, advance=1)
124
+
125
+ return start, advance
118
126
 
119
127
  def end_section_task(self: "PipelineProgress") -> None:
120
128
  if self.section_task is not None:
@@ -7,7 +7,7 @@ import polars as pl
7
7
 
8
8
  import tablassert.lib as lib
9
9
  from tablassert.ingests import from_yaml
10
- from tablassert.lib import Tcode, idxname, label_edge, strip_nulls
10
+ from tablassert.lib import Tcode, clean_numeric, format_numeric, idxname, label_edge, numeric_columns, strip_nulls
11
11
 
12
12
 
13
13
  # ? idxname Converts Single Letter Columns
@@ -356,3 +356,167 @@ def test_sig_marks_null_as_unsure() -> None:
356
356
  lf: pl.LazyFrame = pl.DataFrame({"p value": [None, 0.01, 0.1]}).lazy()
357
357
  result: pl.DataFrame = lib.sig(lf).collect()
358
358
  assert list(result["significant"]) == ["UNSURE", "YES", "NO"]
359
+
360
+
361
+ # ? sig Marks P-Values Between Cutoff And Threshold As INCONCLUSIVE
362
+ def test_sig_marks_inconclusive_band() -> None:
363
+ lf: pl.LazyFrame = pl.DataFrame({"p value": [0.01, 0.07, 0.1]}).lazy()
364
+ result: pl.DataFrame = lib.sig(lf).collect()
365
+ assert list(result["significant"]) == ["YES", "INCONCLUSIVE", "NO"]
366
+
367
+
368
+ # ? numeric_columns Matches Any Column With P Value In The Name
369
+ def test_numeric_columns_matches_p_value_substring() -> None:
370
+ names: list[str] = ["p value", "adjusted p value", "log p value", "subject"]
371
+ result: list[str] = numeric_columns(names)
372
+ assert result == ["p value", "adjusted p value", "log p value"]
373
+ assert "subject" not in result
374
+
375
+
376
+ # ? numeric_columns Matches Exact Relationship Strength And Sample Size Names
377
+ def test_numeric_columns_matches_exact_names() -> None:
378
+ names: list[str] = ["relationship strength", "sample size", "cohort"]
379
+ result: list[str] = numeric_columns(names)
380
+ assert "relationship strength" in result
381
+ assert "sample size" in result
382
+ assert "cohort" not in result
383
+
384
+
385
+ # ? numeric_columns Is Case Insensitive On The P Value Substring
386
+ def test_numeric_columns_case_insensitive() -> None:
387
+ names: list[str] = ["P Value", "P VALUE"]
388
+ result: list[str] = numeric_columns(names)
389
+ assert result == ["P Value", "P VALUE"]
390
+
391
+
392
+ # ? clean_numeric Coerces Numeric And Scientific Notation Strings To Float64
393
+ def test_clean_numeric_parses_numeric_and_scientific() -> None:
394
+ lf: pl.LazyFrame = pl.DataFrame(
395
+ {"p value": ["1e-8", "0.05", "450"], "sample size": ["1200", "0.42", "-1.2"]}
396
+ ).lazy()
397
+ result: pl.DataFrame = clean_numeric(lf).collect()
398
+ assert result.schema["p value"] == pl.Float64
399
+ assert result.schema["sample size"] == pl.Float64
400
+ assert result["p value"].to_list() == [1e-8, 0.05, 450.0]
401
+ assert result["sample size"].to_list() == [1200.0, 0.42, -1.2]
402
+
403
+
404
+ # ? clean_numeric Drops Non Numeric Entries To Null
405
+ def test_clean_numeric_nulls_non_numeric() -> None:
406
+ lf: pl.LazyFrame = pl.DataFrame(
407
+ {"p value": ["1e-8", "N/A", "", "<0.001", "abc"], "relationship strength": ["0.85", "n/a", "NULL", "x", "y"]}
408
+ ).lazy()
409
+ result: pl.DataFrame = clean_numeric(lf).collect()
410
+ assert result["p value"].to_list() == [1e-8, None, None, None, None]
411
+ assert result["relationship strength"].to_list() == [0.85, None, None, None, None]
412
+
413
+
414
+ # ? clean_numeric Leaves Non Matching Columns Untouched
415
+ def test_clean_numeric_leaves_non_matching_untouched() -> None:
416
+ lf: pl.LazyFrame = pl.DataFrame(
417
+ {"subject": ["BRCA1", "TP53"], "assertion method": ["ANOVA", "t-test"], "p value": ["0.05", "1e-8"]}
418
+ ).lazy()
419
+ result: pl.DataFrame = clean_numeric(lf).collect()
420
+ assert result.schema["subject"] == pl.String
421
+ assert result.schema["assertion method"] == pl.String
422
+ assert result.schema["p value"] == pl.Float64
423
+ assert result["subject"].to_list() == ["BRCA1", "TP53"]
424
+ assert result["assertion method"].to_list() == ["ANOVA", "t-test"]
425
+
426
+
427
+ # ? clean_numeric Is A Noop When No Numeric Columns Are Present
428
+ def test_clean_numeric_noop_without_numeric_columns() -> None:
429
+ lf: pl.LazyFrame = pl.DataFrame({"subject": ["BRCA1"], "cohort": ["adult"]}).lazy()
430
+ result: pl.DataFrame = clean_numeric(lf).collect()
431
+ assert result.schema["subject"] == pl.String
432
+ assert result.schema["cohort"] == pl.String
433
+
434
+
435
+ # ? clean_numeric Is Idempotent On Already Float64 Columns
436
+ def test_clean_numeric_idempotent_on_float64() -> None:
437
+ lf: pl.LazyFrame = pl.DataFrame({"p value": [1e-8, 0.05]}).lazy()
438
+ once: pl.DataFrame = clean_numeric(lf).collect()
439
+ twice: pl.DataFrame = clean_numeric(once.lazy()).collect()
440
+ assert twice["p value"].to_list() == [1e-8, 0.05]
441
+ assert twice.schema["p value"] == pl.Float64
442
+
443
+
444
+ # ? format_numeric Renders P Value Columns In Scientific Notation
445
+ def test_format_numeric_p_value_scientific() -> None:
446
+ lf: pl.LazyFrame = pl.DataFrame(
447
+ {"p value": ["1e-8", "0.05", "0.001"], "adjusted p value": ["0.0001", "0.1", "0.2"]}
448
+ ).lazy()
449
+ result: pl.DataFrame = format_numeric(clean_numeric(lf)).collect()
450
+ assert result["p value"].to_list() == ["1.0000e-08", "5.0000e-02", "1.0000e-03"]
451
+ assert result["adjusted p value"].to_list() == ["1.0000e-04", "1.0000e-01", "2.0000e-01"]
452
+ assert result.schema["p value"] == pl.String
453
+
454
+
455
+ # ? format_numeric Renders Relationship Strength And Sample Size In Decimal General Format
456
+ def test_format_numeric_decimal_general() -> None:
457
+ lf: pl.LazyFrame = pl.DataFrame(
458
+ {"relationship strength": ["0.85", "0.42", "0.1234"], "sample size": ["450", "1200", "7"]}
459
+ ).lazy()
460
+ result: pl.DataFrame = format_numeric(clean_numeric(lf)).collect()
461
+ assert result["relationship strength"].to_list() == ["0.85", "0.42", "0.1234"]
462
+ assert result["sample size"].to_list() == ["450", "1200", "7"]
463
+
464
+
465
+ # ? format_numeric Preserves Nulls As Null
466
+ def test_format_numeric_preserves_nulls() -> None:
467
+ lf: pl.LazyFrame = pl.DataFrame({"p value": ["1e-8", "N/A", "0.05"]}).lazy()
468
+ result: pl.DataFrame = format_numeric(clean_numeric(lf)).collect()
469
+ assert result["p value"].to_list() == ["1.0000e-08", None, "5.0000e-02"]
470
+
471
+
472
+ # ? format_numeric Cleans Floating Point Noise To Four Significant Figures
473
+ def test_format_numeric_cleans_float_noise() -> None:
474
+ lf: pl.LazyFrame = pl.DataFrame({"relationship strength": ["0.85000000001", "0.41999999999"]}).lazy()
475
+ result: pl.DataFrame = format_numeric(clean_numeric(lf)).collect()
476
+ assert result["relationship strength"].to_list() == ["0.85", "0.42"]
477
+
478
+
479
+ # ? format_numeric Is A Noop When No Numeric Columns Are Present
480
+ def test_format_numeric_noop_without_numeric_columns() -> None:
481
+ lf: pl.LazyFrame = pl.DataFrame({"subject": ["BRCA1"], "cohort": ["adult"]}).lazy()
482
+ result: pl.DataFrame = format_numeric(lf).collect()
483
+ assert result["subject"].to_list() == ["BRCA1"]
484
+ assert result.schema["subject"] == pl.String
485
+
486
+
487
+ # ? Cleaned And Formatted Null Numeric Values Are Stripped From NDJSON Rows
488
+ def test_format_numeric_nulls_stripped_from_ndjson_rows() -> None:
489
+ lf: pl.LazyFrame = pl.DataFrame(
490
+ {"subject": ["BRCA1", "TP53"], "p value": ["1e-8", "N/A"], "relationship strength": ["0.85", "0.42"]}
491
+ ).lazy()
492
+ formatted: pl.DataFrame = format_numeric(clean_numeric(lf)).collect()
493
+ rows: list[dict[str, Any]] = [strip_nulls(r) for r in formatted.iter_rows(named=True)]
494
+ assert rows[0] == {"subject": "BRCA1", "p value": "1.0000e-08", "relationship strength": "0.85"}
495
+ assert "p value" not in rows[1]
496
+ assert rows[1]["subject"] == "TP53"
497
+ assert rows[1]["relationship strength"] == "0.42"
498
+
499
+
500
+ # ? compile_graph Emits Edges And Nodes After Float Formatting Config Removal
501
+ def test_compile_graph_emits_ndjson(monkeypatch: Any, tmp_path: Path) -> None:
502
+ monkeypatch.chdir(tmp_path)
503
+ sub: Path = tmp_path / "sub.parquet"
504
+ pl.DataFrame(
505
+ {"subject": ["A", "B"], "object": ["X", "Y"], "predicate": ["r", "r"], "p value": ["1.0000e-08", "5.0000e-02"]}
506
+ ).write_parquet(sub)
507
+ lib.compile_graph([sub], "smoke", "1.0.0")
508
+ edges: list[str] = (tmp_path / "smoke_1.0.0.edges.ndjson").read_text().strip().splitlines()
509
+ nodes: list[str] = (tmp_path / "smoke_1.0.0.nodes.ndjson").read_text().strip().splitlines()
510
+ assert len(edges) == 2
511
+ assert all('"uuid"' in line for line in edges)
512
+ flat: str = "\n".join(edges)
513
+ assert '"p value":"1.0000e-08"' in flat
514
+ assert len(nodes) >= 1
515
+
516
+
517
+ # ? sig Computes Significance On A Cleaned Float64 P Value Column
518
+ def test_sig_works_on_cleaned_float64() -> None:
519
+ lf: pl.LazyFrame = pl.DataFrame({"p value": ["1e-8", "0.5", "N/A"]}).lazy()
520
+ cleaned: pl.LazyFrame = clean_numeric(lf)
521
+ result: pl.DataFrame = lib.sig(cleaned).collect()
522
+ assert result["significant"].to_list() == ["YES", "NO", "UNSURE"]
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from tablassert.progress import PipelineProgress
6
+
7
+
8
+ # ? Helper To Read The Active Section Task Fields
9
+ def _section_task(p: PipelineProgress) -> Any:
10
+ return next(t for t in p.progress.tasks if t.id == p.section_task)
11
+
12
+
13
+ # ? start Marks The In Flight Item In The Description Without Ticking The Counter
14
+ def test_start_shows_in_flight_item_without_incrementing() -> None:
15
+ p: PipelineProgress = PipelineProgress(total_stages=1)
16
+ start, advance = p.section_loop(3, "Collect")
17
+ task: Any = _section_task(p)
18
+ assert task.completed == 0
19
+ start("CONFIG: my_table.yaml")
20
+ task = _section_task(p)
21
+ assert "my_table.yaml" in task.description
22
+ assert task.completed == 0
23
+ assert callable(advance)
24
+
25
+
26
+ # ? advance Ticks The Counter Without Changing The In Flight Description
27
+ def test_advance_ticks_counter_without_changing_description() -> None:
28
+ p: PipelineProgress = PipelineProgress(total_stages=1)
29
+ start, advance = p.section_loop(3, "Collect")
30
+ start("CONFIG: my_table.yaml")
31
+ advance()
32
+ task: Any = _section_task(p)
33
+ assert task.completed == 1
34
+ assert "my_table.yaml" in task.description
35
+ assert callable(start)
36
+
37
+
38
+ # ? Repeated start advance Cycles Keep Description Synced To The Current Item Not The Last
39
+ def test_start_advance_cycle_shows_current_not_previous() -> None:
40
+ p: PipelineProgress = PipelineProgress(total_stages=1)
41
+ start, advance = p.section_loop(3, "Subgraph")
42
+ start("CONFIG: first.yaml")
43
+ advance()
44
+ start("CONFIG: second.yaml")
45
+ task: Any = _section_task(p)
46
+ assert task.completed == 1
47
+ assert "second.yaml" in task.description
48
+ assert "first.yaml" not in task.description
@@ -1737,15 +1737,15 @@ wheels = [
1737
1737
 
1738
1738
  [[package]]
1739
1739
  name = "pyright"
1740
- version = "1.1.409"
1740
+ version = "1.1.411"
1741
1741
  source = { registry = "https://pypi.org/simple" }
1742
1742
  dependencies = [
1743
1743
  { name = "nodeenv" },
1744
1744
  { name = "typing-extensions" },
1745
1745
  ]
1746
- sdist = { url = "https://files.pythonhosted.org/packages/51/4e/3aa27f74211522dba7e9cbc3e74de779c6d4b654c54e50a4840623be8014/pyright-1.1.409.tar.gz", hash = "sha256:986ee05beca9e077c165758ad123667c679e050059a2546aa02473930394bc93", size = 4430434, upload-time = "2026-04-23T11:02:03.799Z" }
1746
+ sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" }
1747
1747
  wheels = [
1748
- { url = "https://files.pythonhosted.org/packages/16/6b/330d8ebae582b30c2959a1ef4c3bc344ebde48c2ff0c3f113c4710735e11/pyright-1.1.409-py3-none-any.whl", hash = "sha256:aa3ea228cab90c845c7a60d28db7a844c04315356392aa09fafcee98c8c22fb3", size = 6438161, upload-time = "2026-04-23T11:02:01.309Z" },
1748
+ { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" },
1749
1749
  ]
1750
1750
 
1751
1751
  [[package]]
@@ -2340,7 +2340,7 @@ wheels = [
2340
2340
 
2341
2341
  [[package]]
2342
2342
  name = "tablassert"
2343
- version = "7.5.0"
2343
+ version = "7.5.2"
2344
2344
  source = { editable = "." }
2345
2345
  dependencies = [
2346
2346
  { name = "cyclopts" },
@@ -2422,7 +2422,7 @@ provides-extras = ["rt", "qc", "qc-cuda"]
2422
2422
  dev = [
2423
2423
  { name = "mkdocs", specifier = ">=1.6.1" },
2424
2424
  { name = "pre-commit", specifier = ">=4.5.1" },
2425
- { name = "pyright", specifier = ">=1.1.408" },
2425
+ { name = "pyright", specifier = ">=1.1.411" },
2426
2426
  { name = "pytest", specifier = ">=9.0.2" },
2427
2427
  { name = "pytest-cov", specifier = ">=7.1.0" },
2428
2428
  { name = "ruff", specifier = ">=0.15.6" },
@@ -1,50 +0,0 @@
1
- # AGENTS.md — Tablassert
2
-
3
- ## Fast Start
4
-
5
- - Python package, not a monorepo. Main code lives in `src/tablassert/`; tests live in `tests/`.
6
- - Install with `uv sync`. QC is not available unless you install an extra: `uv sync --extra qc` or `uv sync --extra qc-cuda`.
7
- - CLI entrypoint is `tablassert.cli:APP`. Real user commands are:
8
- - `uv run tablassert build <graph.yaml>`
9
- - `uv run tablassert validate <table.yaml>`
10
-
11
- ## Verify Changes
12
-
13
- - Match the repo hooks before finishing: `uv run ruff check --fix .`, `uv run ruff format .`, `uv run pyright`, `uv run pytest`.
14
- - Full hook run: `uv run pre-commit run --all-files`.
15
- - Focused test runs:
16
- - Single test: `uv run pytest tests/test_lib.py::test_name`
17
- - By keyword: `uv run pytest -k "pattern"`
18
- - With print output: `uv run pytest -s tests/test_lib.py`
19
- - Docs build: `uv run --group dev mkdocs build`
20
-
21
- ## High-Value Structure
22
-
23
- - `src/tablassert/cli.py` is the wiring layer: `build()` calls `build_pipeline()`, `validate()` calls `validate_pipeline()`.
24
- - `src/tablassert/ingests.py` loads YAML and expands table configs into section dicts.
25
- - `src/tablassert/lib.py` is the core pipeline:
26
- - `Tcode.collect()` builds the per-section operation list.
27
- - `compile_subgraph()` executes that list into parquet.
28
- - `compile_graph()` aggregates subgraph parquets into KGX NDJSON.
29
- - `resolve_many()` is the direct library API for batch entity resolution.
30
- - Entity resolution uses DuckDB shard files under `<datassert>/data/`. `src/tablassert/fullmap.py` hardcodes `SHARDS = 10`.
31
-
32
- ## Repo-Specific Gotchas
33
-
34
- - Heavy dependencies are lazy-loaded per module with `TYPE_CHECKING` + `lazy_loader`. Follow the existing pattern instead of importing heavy packages eagerly.
35
- - `tests/conftest.py` autouse-mocks `httpx.head`, so model URL validation tests do not hit the network unless a test is explicitly marked otherwise.
36
- - Network-dependent tests are marked `@pytest.mark.network`; GPU QC tests are marked with both `network` and `gpu` in `tests/test_qc.py`.
37
- - QC runtime selection is strict in `src/tablassert/qc.py`: if `onnxruntime-gpu` is installed but `CUDAExecutionProvider` is unavailable, the code raises instead of falling back to CPU.
38
- - Downloader behavior in `src/tablassert/downloader.py` is two-path: direct `httpx` fetch for known file URLs, headless-browser fallback for browser-only sources. Keep tests around payload validation and cleanup intact when changing it.
39
-
40
- ## Conventions That Matter Here
41
-
42
- - Start every module with `from __future__ import annotations`.
43
- - Annotate locals, not just function signatures.
44
- - Use `Optional[T]` / `Union[...]`, not `T | None`.
45
- - Prefer `Path` over raw path strings.
46
- - Function docs are usually `# ?` comments above the code, not docstrings.
47
-
48
- ## Side Effects
49
-
50
- - The package writes working artifacts to hidden directories in the repo root: `.storassert/`, `.logassert/`, `.cachassert/`, and `.onnxassert/`.
@@ -1,25 +0,0 @@
1
- # Changelog
2
-
3
- The canonical release history lives in the repository root at [`CHANGELOG.md`](https://github.com/SkyeAv/Tablassert/blob/main/CHANGELOG.md).
4
-
5
- ## Current Release Notes
6
-
7
- ## 7.4.14 - 2026-06-30
8
-
9
- ### Changes
10
-
11
- - Extended `sig()` in `lib.py` to select a p-value column by fuzzy matching rather than requiring an exact `"p value"` name. All schema columns whose names contain the substring `"p value"` are now considered candidates; `fuzz.ratio` (rapidfuzz) scores each against the literal `"p value"` and the highest-scoring column is used to compute the `"significant"` output. An exact `"p value"` column scores 100 and is always preferred; columns like `"adjusted p value"` or `"log p value"` are used only when no exact match is present. If no column contains the substring the function continues to emit `"UNSURE"` for all rows.
12
- - Added five regression tests in `test_lib.py` covering: exact-match preference, non-exact fallback, closest-match selection among multiple non-exact candidates, no-p-value column (UNSURE), and null value handling.
13
-
14
- ## 7.4.13 - 2026-06-30
15
-
16
- ### Changes
17
-
18
- - Removed the datassert prevalidation failure for unresolved `statement.subject` / `statement.object` literal encodings under `method: value`. Graph builds no longer abort during `Tcode.model_validate(...)` for cases like `"Incertae Sedis XI"`; unresolved literal values are now allowed through config validation so downstream runtime handling can decide whether they map or get filtered.
19
- - Added a regression test at the `Tcode.model_validate(...)` layer covering an unresolved `method: value` subject encoding, matching the build-time validation path reported in the field.
20
-
21
- ## 7.4.12 - 2026-06-29
22
-
23
- ### Changes
24
-
25
- - Expanded the placeholder-term filter regex in `distinct()` (`fullmap.py`) to drop additional non-informative terms during entity resolution. The `bad` pattern now also excludes `not applicable`, `p value`, `variable`, `result`, `exposure`, `expression`, and `symbol` alongside the existing `none`, `nan`, `na`, `null`, and `unknown`, preventing these generic column-header-like values from being sent through resolution and producing spurious CURIE mappings.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes