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.
- tablassert-7.5.2/AGENTS.md +57 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/CHANGELOG.md +19 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/PKG-INFO +1 -1
- tablassert-7.5.2/docs/changelog.md +13 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/pyproject.toml +2 -2
- {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/cli.py +18 -14
- {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/lib.py +51 -9
- {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/progress.py +13 -5
- {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_lib.py +165 -1
- tablassert-7.5.2/tests/test_progress.py +48 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/uv.lock +5 -5
- tablassert-7.5.0/AGENTS.md +0 -50
- tablassert-7.5.0/docs/changelog.md +0 -25
- {tablassert-7.5.0 → tablassert-7.5.2}/.github/workflows/docker.yml +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/.github/workflows/docs.yml +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/.github/workflows/pipy.yml +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/.gitignore +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/.pre-commit-config.yaml +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/CITATION.cff +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/CONTRIBUTING.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/Dockerfile +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/LICENSE +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/README.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/api/fullmap.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/api/lib.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/api/qc.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/api/utils.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/cli.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/configuration/advanced-example.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/configuration/graph.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/configuration/table.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/datassert.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/docker.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/examples/tutorial-data.csv +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/examples/tutorial-graph.yaml +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/examples/tutorial-table.yaml +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/examples.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/index.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/installation.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/docs/tutorial.md +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/llms.txt +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/mkdocs.yml +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/__init__.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/downloader.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/enums.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/fullmap.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/ingests.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/log.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/models.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/nlp.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/qc.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/src/tablassert/utils.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/tests/__init__.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/tests/conftest.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/tests/fixtures/invalid_section_missing_source.yaml +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/tests/fixtures/minimal_section.yaml +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/tests/fixtures/minimal_section_with_sections.yaml +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_downloader.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_enums.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_fullmap.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_ingests.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_models.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_nlp.py +0 -0
- {tablassert-7.5.0 → tablassert-7.5.2}/tests/test_qc.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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: {
|
|
73
|
+
f"02 | FAILED VALIDATION | CONFIG: {graph_configuration_file} | IDX: {idx} | HASH: {h} | PYDANTIC: {flatten_pydantic_error(e)}"
|
|
74
74
|
) from e
|
|
75
|
-
advance(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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())
|
|
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
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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(
|
|
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()} |
|
|
115
|
+
self.section_task = self.progress.add_task(description=f"{label.upper()} | STARTING", total=total)
|
|
112
116
|
|
|
113
|
-
def
|
|
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}"
|
|
119
|
+
self.progress.update(self.section_task, description=f"{label.upper()} | {info}")
|
|
116
120
|
|
|
117
|
-
|
|
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.
|
|
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/
|
|
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/
|
|
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.
|
|
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.
|
|
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" },
|
tablassert-7.5.0/AGENTS.md
DELETED
|
@@ -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
|
|
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
|
|
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
|