tablassert 7.4.14__tar.gz → 7.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {tablassert-7.4.14 → tablassert-7.5.0}/CHANGELOG.md +12 -0
  2. {tablassert-7.4.14 → tablassert-7.5.0}/PKG-INFO +1 -1
  3. {tablassert-7.4.14 → tablassert-7.5.0}/docs/api/lib.md +18 -14
  4. {tablassert-7.4.14 → tablassert-7.5.0}/docs/api/qc.md +6 -6
  5. {tablassert-7.4.14 → tablassert-7.5.0}/docs/api/utils.md +26 -7
  6. {tablassert-7.4.14 → tablassert-7.5.0}/docs/cli.md +2 -2
  7. {tablassert-7.4.14 → tablassert-7.5.0}/docs/configuration/advanced-example.md +19 -21
  8. {tablassert-7.4.14 → tablassert-7.5.0}/docs/configuration/graph.md +16 -16
  9. {tablassert-7.4.14 → tablassert-7.5.0}/docs/configuration/table.md +37 -30
  10. {tablassert-7.4.14 → tablassert-7.5.0}/docs/docker.md +6 -4
  11. {tablassert-7.4.14 → tablassert-7.5.0}/pyproject.toml +1 -1
  12. {tablassert-7.4.14 → tablassert-7.5.0}/src/tablassert/lib.py +15 -4
  13. {tablassert-7.4.14 → tablassert-7.5.0}/tests/test_lib.py +53 -0
  14. {tablassert-7.4.14 → tablassert-7.5.0}/uv.lock +1 -1
  15. {tablassert-7.4.14 → tablassert-7.5.0}/.github/workflows/docker.yml +0 -0
  16. {tablassert-7.4.14 → tablassert-7.5.0}/.github/workflows/docs.yml +0 -0
  17. {tablassert-7.4.14 → tablassert-7.5.0}/.github/workflows/pipy.yml +0 -0
  18. {tablassert-7.4.14 → tablassert-7.5.0}/.gitignore +0 -0
  19. {tablassert-7.4.14 → tablassert-7.5.0}/.pre-commit-config.yaml +0 -0
  20. {tablassert-7.4.14 → tablassert-7.5.0}/AGENTS.md +0 -0
  21. {tablassert-7.4.14 → tablassert-7.5.0}/CITATION.cff +0 -0
  22. {tablassert-7.4.14 → tablassert-7.5.0}/CONTRIBUTING.md +0 -0
  23. {tablassert-7.4.14 → tablassert-7.5.0}/Dockerfile +0 -0
  24. {tablassert-7.4.14 → tablassert-7.5.0}/LICENSE +0 -0
  25. {tablassert-7.4.14 → tablassert-7.5.0}/README.md +0 -0
  26. {tablassert-7.4.14 → tablassert-7.5.0}/docs/api/fullmap.md +0 -0
  27. {tablassert-7.4.14 → tablassert-7.5.0}/docs/changelog.md +0 -0
  28. {tablassert-7.4.14 → tablassert-7.5.0}/docs/datassert.md +0 -0
  29. {tablassert-7.4.14 → tablassert-7.5.0}/docs/examples/tutorial-data.csv +0 -0
  30. {tablassert-7.4.14 → tablassert-7.5.0}/docs/examples/tutorial-graph.yaml +0 -0
  31. {tablassert-7.4.14 → tablassert-7.5.0}/docs/examples/tutorial-table.yaml +0 -0
  32. {tablassert-7.4.14 → tablassert-7.5.0}/docs/examples.md +0 -0
  33. {tablassert-7.4.14 → tablassert-7.5.0}/docs/index.md +0 -0
  34. {tablassert-7.4.14 → tablassert-7.5.0}/docs/installation.md +0 -0
  35. {tablassert-7.4.14 → tablassert-7.5.0}/docs/tutorial.md +0 -0
  36. {tablassert-7.4.14 → tablassert-7.5.0}/llms.txt +0 -0
  37. {tablassert-7.4.14 → tablassert-7.5.0}/mkdocs.yml +0 -0
  38. {tablassert-7.4.14 → tablassert-7.5.0}/src/tablassert/__init__.py +0 -0
  39. {tablassert-7.4.14 → tablassert-7.5.0}/src/tablassert/cli.py +0 -0
  40. {tablassert-7.4.14 → tablassert-7.5.0}/src/tablassert/downloader.py +0 -0
  41. {tablassert-7.4.14 → tablassert-7.5.0}/src/tablassert/enums.py +0 -0
  42. {tablassert-7.4.14 → tablassert-7.5.0}/src/tablassert/fullmap.py +0 -0
  43. {tablassert-7.4.14 → tablassert-7.5.0}/src/tablassert/ingests.py +0 -0
  44. {tablassert-7.4.14 → tablassert-7.5.0}/src/tablassert/log.py +0 -0
  45. {tablassert-7.4.14 → tablassert-7.5.0}/src/tablassert/models.py +0 -0
  46. {tablassert-7.4.14 → tablassert-7.5.0}/src/tablassert/nlp.py +0 -0
  47. {tablassert-7.4.14 → tablassert-7.5.0}/src/tablassert/progress.py +0 -0
  48. {tablassert-7.4.14 → tablassert-7.5.0}/src/tablassert/qc.py +0 -0
  49. {tablassert-7.4.14 → tablassert-7.5.0}/src/tablassert/utils.py +0 -0
  50. {tablassert-7.4.14 → tablassert-7.5.0}/tests/__init__.py +0 -0
  51. {tablassert-7.4.14 → tablassert-7.5.0}/tests/conftest.py +0 -0
  52. {tablassert-7.4.14 → tablassert-7.5.0}/tests/fixtures/invalid_section_missing_source.yaml +0 -0
  53. {tablassert-7.4.14 → tablassert-7.5.0}/tests/fixtures/minimal_section.yaml +0 -0
  54. {tablassert-7.4.14 → tablassert-7.5.0}/tests/fixtures/minimal_section_with_sections.yaml +0 -0
  55. {tablassert-7.4.14 → tablassert-7.5.0}/tests/test_downloader.py +0 -0
  56. {tablassert-7.4.14 → tablassert-7.5.0}/tests/test_enums.py +0 -0
  57. {tablassert-7.4.14 → tablassert-7.5.0}/tests/test_fullmap.py +0 -0
  58. {tablassert-7.4.14 → tablassert-7.5.0}/tests/test_ingests.py +0 -0
  59. {tablassert-7.4.14 → tablassert-7.5.0}/tests/test_models.py +0 -0
  60. {tablassert-7.4.14 → tablassert-7.5.0}/tests/test_nlp.py +0 -0
  61. {tablassert-7.4.14 → tablassert-7.5.0}/tests/test_qc.py +0 -0
  62. {tablassert-7.4.14 → tablassert-7.5.0}/tests/test_utils.py +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 7.5.0 - 2026-07-01
6
+
7
+ ### Changed
8
+ - Publication CURIEs in `compile_subgraph()` (`lib.py`) now use the `PMCID:` namespace prefix for PubMed Central sources. A `repo: PMC` section with `publication: PMC11708054` is emitted as `PMCID:PMC11708054` (previously `PMC:PMC11708054`); non-PMC repos such as `PMID` are unaffected and continue to emit `<repo>:<publication>` (e.g., `PMID:11708054`). The `repository` edge column is unchanged and still records the raw `repo` value. Extracted via a new `publication_curie()` helper.
9
+
10
+ ### Added
11
+ - New `<col> table literal value` edge column for subject, object, and qualifier nodes encoded with `method: column`. Unlike the existing `original <col>` column (which snapshots the value *after* all `fill`/`explode_by`/`regex`/`remove`/`prefix`/`suffix`/`transformations`), `<col> table literal value` captures the pristine source-cell value *before* any transformation. Emitted only for column-encoded nodes; annotations and `method: value` nodes are unaffected. Implemented via a `table_literal` flag on `Tcode.encoding()`, enabled by `Tcode.node()`.
12
+ - Four regression tests in `test_lib.py`: `publication_curie()` for PMC and PMID namespaces, and two `Tcode` tcode-inspection tests covering presence/ordering of the table-literal column for column encodings and its absence for value encodings.
13
+
14
+ ### Documentation
15
+ - Comprehensive accuracy pass across the API, configuration, and Docker documentation, reconciling every page against the current codebase. Highlights: corrected invalid examples that would not load (`syntax: TC2`; `publication` integers and missing `PMC` prefixes; a non-existent `Qualifiers` member; `reindex` placed at section level; a subject missing `method: column`), fixed wrong field types (`rows`/`row_slice`/`taxon` → `PositiveInt`, `remove` → regex patterns), corrected the QC fuzzy thresholds (`fuzz.ratio >= 20 OR partial_token_sort_ratio >= 30`), removed a non-existent `uuid:` prefix from `utils.md` return examples, fixed the `resolve_many()` parameter order and added the original-column-capture and optional QC-audit pipeline steps, corrected graph-config path resolution (CWD, not config-relative) and processing-flow ordering, documented the strict QC GPU no-fallback behavior and the `.cachassert/` working directory, and aligned `Categories` enum member names (`GENE`/`PROTEIN`) and Docker CI triggers with the source.
16
+
5
17
  ## 7.4.14 - 2026-06-30
6
18
 
7
19
  ### Changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tablassert
3
- Version: 7.4.14
3
+ Version: 7.5.0
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  The `lib` module exposes `resolve_many()`, a high-level convenience function for resolving an iterable of entity strings to CURIEs without requiring manual LazyFrame construction, NLP preprocessing, or DuckDB shard management.
4
4
 
5
- It wraps the lower-level [`resolve()`](fullmap.md) pipeline — applying `level_one` and `level_two` normalization, opening all 10 DuckDB shard connections, executing entity resolution, and returning results as a plain Python list of row dictionaries.
5
+ It wraps the lower-level [`resolve()`](fullmap.md) pipeline — preserving the original input text, applying `level_one` and `level_two` normalization, opening all 10 DuckDB shard connections, executing entity resolution, optionally running the QC audit (when `qc=True`), and returning results as a plain Python list of row dictionaries.
6
6
 
7
7
  ## resolve_many()
8
8
 
@@ -18,8 +18,8 @@ def resolve_many(
18
18
  taxon: Optional[str] = None,
19
19
  prioritize: Optional[list[Categories]] = None,
20
20
  avoid: Optional[list[Categories]] = None,
21
- column_context: bool = True,
22
21
  qc: bool = False,
22
+ column_context: bool = True,
23
23
  ) -> list[dict[str, Any]]
24
24
  ```
25
25
 
@@ -52,29 +52,29 @@ Each shard contains:
52
52
 
53
53
  Optional NCBI Taxon ID for filtering results to a specific organism.
54
54
 
55
- Example: `"9606"` restricts matches to human-specific entities. When `None`, no taxon filtering is applied and matches from all organisms are returned.
55
+ Example: `"9606"` restricts **gene** matches to human-specific entries; non-gene categories (e.g., diseases, chemicals) are returned regardless of taxon. When `None`, no taxon filtering is applied and matches from all organisms are returned.
56
56
 
57
57
  **`prioritize: Optional[list[Categories]]` (default: `None`)**
58
58
 
59
59
  Optional list of Biolink categories to prefer when multiple matches exist for the same input term. Categories listed here receive higher ranking scores during resolution.
60
60
 
61
- Example: `[Categories.Gene, Categories.Protein]` prefers gene and protein mappings over other categories like diseases or chemicals.
61
+ Example: `[Categories.GENE, Categories.PROTEIN]` prefers gene and protein mappings over other categories like diseases or chemicals.
62
62
 
63
63
  **`avoid: Optional[list[Categories]]` (default: `None`)**
64
64
 
65
65
  Optional list of Biolink categories to exclude from results entirely. Any match belonging to an avoided category is filtered out before ranking.
66
66
 
67
- Example: `[Categories.Gene]` prevents gene mappings from appearing in the output, even if they would otherwise be the best match.
67
+ Example: `[Categories.GENE]` prevents gene mappings from appearing in the output, even if they would otherwise be the best match.
68
68
 
69
69
  **`column_context: bool` (default: `True`)**
70
70
 
71
- Controls category-frequency tie-breaking when multiple matches exist for a term. When `True`, the resolution query adds a category frequency score and prefers the category that appears most frequently across all terms in the batch. When `False`, frequency-based tie-breaking is disabled.
71
+ Controls category-frequency tie-breaking when multiple matches exist for a term. When `True`, the deduplication stage adds a category-frequency score (computed in Polars after the SQL query) and prefers the category that appears most frequently across all matched terms in the batch. When `False`, frequency-based tie-breaking is disabled.
72
72
 
73
73
  This is useful when resolving a column of related entities (e.g., all genes) — the shared context helps disambiguate terms that map to multiple categories.
74
74
 
75
75
  **`qc: bool` (default: `False`)**
76
76
 
77
- When `True`, runs the QC audit stage after entity resolution. The QC pipeline validates mappings through a three-stage audit: exact match, fuzzy matching via rapidfuzz, and BioBERT sentence embeddings with cosine similarity. Requires a QC runtime to be installed (`tablassert[qc]` or `tablassert[qc-cuda]`). The ONNX Runtime provider is auto-detected based on the installed package — CUDA is preferred when `onnxruntime-gpu` is available, otherwise CPU is used.
77
+ When `True`, runs the QC audit stage after entity resolution. The QC pipeline validates mappings through a three-stage audit: exact match, fuzzy matching via rapidfuzz, and BioBERT sentence embeddings with cosine similarity. Mappings that fail all three stages are dropped from the returned list (in addition to the unresolved-entity filtering performed by `resolve()`). Requires a QC runtime to be installed (`tablassert[qc]` or `tablassert[qc-cuda]`). The ONNX Runtime provider is auto-detected based on the installed package — CUDA is preferred when `onnxruntime-gpu` is installed and `CUDAExecutionProvider` is available; if only `onnxruntime` (CPU) is installed, CPU is used. Note: if `onnxruntime-gpu` is installed but `CUDAExecutionProvider` is unavailable, QC raises rather than falling back to CPU.
78
78
 
79
79
  ### Return Value
80
80
 
@@ -101,13 +101,17 @@ Each dictionary contains the following keys (where `{col}` is the value of the `
101
101
 
102
102
  1. **Series construction** — Wraps the input iterable in a `pl.Series` with the given column name, then converts to a single-column `pl.LazyFrame`.
103
103
 
104
- 2. **NLP normalization** — Applies `level_one()` (whitespace stripping + lowercasing) and `level_two()` (non-word character removal via `\W+`) to produce the two normalized columns required by `resolve()`.
104
+ 2. **Original column capture** — Copies the raw input column into `original {col}` via `column(lf, add("original ", col), col)` so the pre-normalization text is preserved in the output.
105
+
106
+ 3. **NLP normalization** — Applies `level_one()` (whitespace stripping + lowercasing) and `level_two()` (non-word character removal via `\W+`) to produce the two normalized columns required by `resolve()`.
107
+
108
+ 4. **DuckDB connection management** — Opens all 10 shard connections inside a `contextlib.ExitStack`, ensuring every connection is properly closed when resolution completes or if an error occurs.
105
109
 
106
- 3. **DuckDB connection management** — Opens all 10 shard connections inside a `contextlib.ExitStack`, ensuring every connection is properly closed when resolution completes or if an error occurs.
110
+ 5. **Entity resolution** — Delegates to `fullmap.resolve()` which queries the sharded DuckDB database, ranks matches by category priority, preferred-name exactness, NLP level, and category frequency, then deduplicates to one CURIE per input string.
107
111
 
108
- 4. **Entity resolution** — Delegates to `fullmap.resolve()` which queries the sharded DuckDB database, ranks matches by category priority, preferred-name exactness, NLP level, and category frequency, then deduplicates to one CURIE per input string.
112
+ 6. **QC audit (optional)** — When `qc=True`, runs `fullmap_audit()` on the resolved LazyFrame. Rows that fail all three audit stages are dropped from the result.
109
113
 
110
- 5. **Collection and conversion** — Collects the lazy result into an eager `pl.DataFrame` and converts to a list of row dictionaries via `to_dicts()`.
114
+ 7. **Collection and conversion** — Collects the lazy result into an eager `pl.DataFrame` and converts to a list of row dictionaries via `to_dicts()`.
111
115
 
112
116
  ### Example Usage
113
117
 
@@ -126,7 +130,7 @@ result: list[dict[str, Any]] = resolve_many(
126
130
  entities=["TP53", "BRCA1", "EGFR", "KRAS"],
127
131
  datassert=datassert,
128
132
  taxon="9606",
129
- prioritize=[Categories.Gene],
133
+ prioritize=[Categories.GENE],
130
134
  )
131
135
 
132
136
  # result[0] → {"original gene": "TP53", "gene": "HGNC:11998", "gene name": "TP53", ...}
@@ -147,7 +151,7 @@ result: list[dict[str, Any]] = resolve_many(
147
151
  col="disease",
148
152
  entities=["diabetes mellitus", "breast cancer", "alzheimer disease"],
149
153
  datassert=datassert,
150
- avoid=[Categories.Gene, Categories.Protein],
154
+ avoid=[Categories.GENE, Categories.PROTEIN],
151
155
  )
152
156
 
153
157
  # result[0] → {"original disease": "diabetes mellitus", "disease": "MONDO:0005015", ...}
@@ -206,7 +210,7 @@ for row in result:
206
210
  | **Connections** | Managed internally via `ExitStack` | Must be opened externally |
207
211
  | **Output** | `list[dict[str, Any]]` | `pl.LazyFrame` |
208
212
  | **Logging** | Uses default (`log=True`) | Configurable |
209
- | **Context params** | Not exposed (`section_hash`, `config_file`, `tag`) | Fully configurable |
213
+ | **Context params** | `column_context` exposed; `section_hash`, `config_file`, `tag` not exposed | Fully configurable |
210
214
  | **Use case** | Standalone batch lookups, scripting, notebooks | Internal pipeline integration |
211
215
 
212
216
  `resolve_many()` is designed for ad-hoc and programmatic use — scripts, notebooks, and one-off lookups. For pipeline integration where you need full control over logging, context metadata, and lazy evaluation, use `resolve()` directly.
@@ -2,7 +2,7 @@
2
2
 
3
3
  The `qc` module validates entity resolution mappings through a multi-stage pipeline: exact matching, fuzzy matching, and BERT semantic similarity.
4
4
 
5
- QC runtime support is optional. Install `tablassert[qc]` for CPU inference or `tablassert[qc-cuda]` for CUDA inference on GPU 0.
5
+ QC runtime support is optional. Install `tablassert[qc]` for CPU inference or `tablassert[qc-cuda]` for CUDA inference on GPU 0. **Strict GPU behavior:** if `onnxruntime-gpu` is installed but `CUDAExecutionProvider` is not available (e.g., a broken CUDA/cuDNN environment), `fullmap_audit()` raises a `RuntimeError` rather than silently falling back to CPU — install `tablassert[qc]` for CPU inference or fix the CUDA environment.
6
6
 
7
7
  ## fullmap_audit()
8
8
 
@@ -84,7 +84,7 @@ original == preferred_name
84
84
 
85
85
  **Performance:** O(1) string comparison
86
86
 
87
- Before fuzzy matching, the function also applies rule-based pass-through checks for known safe patterns (for example CHEBI/PR/UniProtKB CURIE families and selected exception prefixes).
87
+ Before fuzzy matching, the function also applies rule-based pass-through checks: (1) the resolved CURIE matches an exempt prefix (`CHEBI`, `PR`, `UniProtKB`, `NCBIGene`, `UMLS`, `UNII`, `PUBCHEM`, `MONDO`); (2) the original text contains `:` (looks like a CURIE); (3) the preferred name matches an exception prefix (`^LOC` or `^si:`).
88
88
 
89
89
  #### Stage 2: Fuzzy Matching
90
90
 
@@ -94,11 +94,11 @@ Two fuzzy matching algorithms:
94
94
  1. **Ratio:** Overall string similarity
95
95
  2. **Partial token sort ratio:** Combined token/subsequence matching
96
96
 
97
- **Threshold:** 20% similarity
97
+ **Thresholds:** `fuzz.ratio` >= 20 OR `fuzz.partial_token_sort_ratio` >= 30
98
98
 
99
99
  ```python
100
100
  fuzz.ratio(original, preferred) >= 20
101
- or fuzz.partial_token_sort_ratio(original, preferred) >= 20
101
+ or fuzz.partial_token_sort_ratio(original, preferred) >= 30
102
102
  ```
103
103
 
104
104
  **Example passes:**
@@ -116,8 +116,8 @@ or fuzz.partial_token_sort_ratio(original, preferred) >= 20
116
116
  3. **Accept** if similarity >= 0.2 (20%)
117
117
 
118
118
  ```python
119
- embeddings = BIOBERT.encode([original, preferred])
120
- similarity = cosine_similarity([embeddings[0]], [embeddings[1]])[0][0]
119
+ embeddings = get_biobert(provider).encode(originals + preferreds)
120
+ similarity = cosine_similarity(embeddings[:n], embeddings[n:]).diagonal()
121
121
  return similarity >= 0.2
122
122
  ```
123
123
 
@@ -19,10 +19,8 @@ def namespace_uuid(domain: Any, *values: list[Any]) -> str
19
19
  Domain string used to create the namespace UUID.
20
20
 
21
21
  Converted to string internally. Common domains:
22
- - `"TABLASSERT"` - Default domain used for knowledge graph edge IDs
23
- - `"edges"` - Optional custom domain for edge IDs
24
- - `"nodes"` - For custom node IDs
25
- - `"tablassert"` - For application-specific IDs
22
+ - `"TABLASSERT"` - Default domain used for KGX edge IDs (the value used by `label_edge`)
23
+ - Other domain strings (e.g., `"edges"`, `"nodes"`) may be passed for custom namespaces, though only `"TABLASSERT"` is used internally.
26
24
 
27
25
  **`*values: list[Any]`**
28
26
 
@@ -118,7 +116,7 @@ edge_id = namespace_uuid(
118
116
  "MONDO:0005148", # Type 2 diabetes
119
117
  "PMC11708054" # Publication
120
118
  )
121
- # Returns: "uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
119
+ # Returns: "2cfea591-0f8f-33af-a7df-03da531d3359"
122
120
  ```
123
121
 
124
122
  **Benefits:**
@@ -145,7 +143,7 @@ from tablassert.utils import namespace_uuid
145
143
 
146
144
  # Generate edge ID
147
145
  edge_id = namespace_uuid("edges", "subject", "predicate", "object")
148
- print(edge_id) # "uuid:12345678-1234-1234-1234-123456789abc"
146
+ print(edge_id) # "12345678-1234-1234-1234-123456789abc"
149
147
  ```
150
148
 
151
149
  **With qualifiers:**
@@ -183,12 +181,33 @@ Suitable for millions of ID generations per second.
183
181
  **`basespace(domain)`** - Creates namespace UUID from domain
184
182
 
185
183
  ```python
184
+ @cache
186
185
  def basespace(domain: str) -> UUID:
187
186
  namespace = UUID("00000000-0000-0000-0000-000000000000")
188
187
  return uuid3(namespace, domain)
189
188
  ```
190
189
 
191
- Used internally by `namespace_uuid()`.
190
+ Used internally by `namespace_uuid()`. Results are memoized per domain.
191
+
192
+ ## mkhash()
193
+
194
+ Generates an `xxhash` 64-bit digest (hex string, 16 characters) for arbitrary input.
195
+
196
+ ```python
197
+ def mkhash(x: Any) -> str
198
+ ```
199
+
200
+ Used for compact identifier hashing (e.g., config/file identifiers in the CLI). Returns the `xxh64` hex digest of the input converted to a string.
201
+
202
+ ## samphash()
203
+
204
+ Hashes a sampled `polars.DataFrame` for deterministic temp-file naming.
205
+
206
+ ```python
207
+ def samphash(df: pl.DataFrame, n: int = 20) -> str
208
+ ```
209
+
210
+ Samples up to `n` rows (default `20`) from `df`, hashes the sampled bytes via `mkhash()`, and returns the digest string.
192
211
 
193
212
  ## Next Steps
194
213
 
@@ -26,7 +26,7 @@ Prints the installed Tablassert version to stdout and exits. This is a flag on t
26
26
 
27
27
  ## build
28
28
 
29
- Build A KGX Compliant Knowledge Graph From A Graph Configuration File
29
+ Build a knowledge graph from a YAML configuration file.
30
30
 
31
31
  ### Synopsis
32
32
 
@@ -70,7 +70,7 @@ See [Graph Configuration](configuration/graph.md) for details on the YAML schema
70
70
 
71
71
  ## validate
72
72
 
73
- Verify The Syntax Of A Declarative Table Configuration File
73
+ Validate section syntax from a YAML configuration file.
74
74
 
75
75
  ### Synopsis
76
76
 
@@ -23,7 +23,7 @@ template:
23
23
  local: ./DATALAKE/ALAM.XLSX
24
24
  url: https://pmc.ncbi.nlm.nih.gov/articles/instance/11708054/bin/mbio.01679-24-s0006.xlsx
25
25
  row_slice:
26
- - 2 # Skip first row (header)
26
+ - 2 # Skip the first two rows (title + header)
27
27
  - auto # Read to end
28
28
  sheet: all correlations
29
29
 
@@ -39,9 +39,9 @@ template:
39
39
  avoid:
40
40
  - Gene
41
41
 
42
- # Remove invalid entries
42
+ # Strip non-value text in place (rows are not dropped)
43
43
  remove:
44
- - "^NA " # Rows starting with "NA "
44
+ - "^NA " # Cells starting with "NA "
45
45
 
46
46
  # Clean taxonomic names with regex
47
47
  regex:
@@ -66,7 +66,7 @@ template:
66
66
  # Provenance: Publication and curation info
67
67
  provenance:
68
68
  repo: PMC
69
- publication: 11708054
69
+ publication: PMC11708054
70
70
  contributors:
71
71
  - kind: curation
72
72
  name: Skye Lane Goetz
@@ -147,7 +147,7 @@ The subject field uses three regex transformations in sequence:
147
147
  ```
148
148
  `"Lactobacillus sp"` → `"Lactobacillus sp. "`
149
149
 
150
- > **Regex constraint:** Each `pattern` is handed to Polars `str.replace_all()` (Rust `regex` crate). **Capturing groups (`(...)` / `\1`) and lookarounds (`(?=...)`, `(?<=...)`, `(?!...)`, `(?<!...)`) are not allowed** and will fail validation. Express transformations as a sequence of simple anchored / character-class substitutions instead — the pipeline above is a deliberate three-step chain because no single capturing-group pattern is permitted. If the transformation can't be expressed without those features, capture the leftover context in a `miscellaneous notes` annotation rather than fighting the regex engine.
150
+ > **Regex constraint:** Each `pattern` is handed to Polars `str.replace_all()` (Rust `regex` crate). **Backreferences (`\1`, `\2`, …) and lookarounds (`(?=...)`, `(?<=...)`, `(?!...)`, `(?<!...)`) are not allowed** and will fail validation. Plain groups `(...)` and non-capturing groups `(?:...)` *are* supported. Express transformations as a sequence of simple anchored / character-class substitutions where possible — the pipeline above is a deliberate three-step chain. If the transformation can't be expressed without those unsupported features, capture the leftover context in a `miscellaneous notes` annotation rather than fighting the regex engine.
151
151
 
152
152
  ### Taxonomic Filtering
153
153
 
@@ -226,6 +226,7 @@ template:
226
226
 
227
227
  statement:
228
228
  subject:
229
+ method: column
229
230
  encoding: A
230
231
  prioritize: [OrganismTaxon]
231
232
  avoid: [Gene]
@@ -239,18 +240,20 @@ sections:
239
240
  # Section 1: Positive correlations
240
241
  - statement:
241
242
  predicate: positively_correlated_with
242
- reindex:
243
- - column: B # Correlation coefficient
244
- comparison: gt
245
- comparator: 0
243
+ source:
244
+ reindex:
245
+ - column: B # Correlation coefficient
246
+ comparison: gt
247
+ comparator: 0
246
248
 
247
249
  # Section 2: Negative correlations
248
250
  - statement:
249
251
  predicate: negatively_correlated_with
250
- reindex:
251
- - column: B
252
- comparison: lt
253
- comparator: 0
252
+ source:
253
+ reindex:
254
+ - column: B
255
+ comparison: lt
256
+ comparator: 0
254
257
  ```
255
258
 
256
259
  This produces two sets of edges from one table:
@@ -296,14 +299,9 @@ template:
296
299
  - pattern: _
297
300
  replacement: ' ' # "Lactobacillus_rhamnosus" → "Lactobacillus rhamnosus"
298
301
 
299
- qualifiers:
300
- - qualifier: p value
301
- method: column
302
- encoding: E
303
-
304
302
  provenance:
305
303
  repo: PMC
306
- publication: 12345678
304
+ publication: PMC12345678
307
305
  contributors:
308
306
  - kind: curation
309
307
  name: Skye Lane Goetz
@@ -324,7 +322,7 @@ template:
324
322
 
325
323
  **Both nodes from columns:** Setting `method: column` on both subject and object means both undergo entity resolution via `resolve()`. Each gets its own `prioritize` list to guide disambiguation.
326
324
 
327
- **`remove` vs `regex`:** `remove` filters out entire rows matching a pattern before resolution. `regex` transforms the column value in-place before resolution.
325
+ **`remove` vs `regex`:** Both transform cell text in place before resolution (neither drops rows). `remove` strips each listed regex pattern by replacing it with an empty string; `regex` applies an ordered `pattern`→`replacement` substitution list.
328
326
 
329
327
  ---
330
328
 
@@ -364,7 +362,7 @@ template:
364
362
 
365
363
  provenance:
366
364
  repo: PMC
367
- publication: 87654321
365
+ publication: PMC87654321
368
366
  contributors:
369
367
  - kind: curation
370
368
  name: Skye Lane Goetz
@@ -16,7 +16,6 @@ A graph configuration file specifies:
16
16
 
17
17
  | Field | Type | Description |
18
18
  |-------|------|-------------|
19
- | `syntax` | String | Configuration version (must be `"GC2"`) |
20
19
  | `name` | String | Knowledge graph name (used in output filename) |
21
20
  | `version` | String | Knowledge graph version (used in output filename) |
22
21
  | `tables` | List[Path] | Paths to table configuration YAML files |
@@ -26,6 +25,7 @@ A graph configuration file specifies:
26
25
 
27
26
  | Field | Type | Description |
28
27
  |-------|------|-------------|
28
+ | `syntax` | String | Configuration version. Defaults to `"GC2"` (also accepts `"TC3"`); overriding is not recommended. |
29
29
  | `pubmed_db` | Path | Path to SQLite PubMed metadata database |
30
30
  | `pmc_db` | Path | Path to SQLite PMC figure captions database |
31
31
  | `log` | Boolean | Log unmatched entities and audit details during graph builds |
@@ -35,7 +35,7 @@ A graph configuration file specifies:
35
35
 
36
36
  **`syntax: "GC2"`**
37
37
 
38
- Configuration syntax version. Must be `"GC2"`.
38
+ Configuration syntax version. Defaults to `"GC2"` (the type also accepts `"TC3"`); overriding the default is not recommended.
39
39
 
40
40
  **`name: string`**
41
41
 
@@ -61,7 +61,7 @@ This field only controls whether QC runs. Install `tablassert[qc]` or `tablasser
61
61
 
62
62
  **`tables: list[path]`**
63
63
 
64
- List of table configuration file paths. Can be absolute or relative to graph config location.
64
+ List of table configuration file paths. Can be absolute or relative to the current working directory (paths are resolved against the process CWD, not the graph-config file location).
65
65
 
66
66
  Each table config defines:
67
67
  - Data source (Excel/CSV/TSV)
@@ -101,8 +101,7 @@ When provided, this is used when provenance specifies PMC publications.
101
101
 
102
102
  Paths can be:
103
103
  - **Absolute:** `/home/user/data/datassert`
104
- - **Relative to graph config:** `./tables/table1.yaml`
105
- - **Relative to current directory:** `../configs/table.yaml`
104
+ - **Relative to the current working directory:** `./tables/table1.yaml` (note: paths are resolved against the process CWD, not the graph-config file location — there is no config-relative resolver)
106
105
 
107
106
  ## Minimal Example
108
107
 
@@ -139,16 +138,17 @@ pmc_db: /databases/PMCSuppCaptions.db
139
138
  When you run `tablassert build graph.yaml`:
140
139
 
141
140
  1. **Load graph configuration** - Parse YAML, validate schema
142
- 2. **For each table in `tables`:**
143
- - Load table configuration
141
+ 2. **Load table configurations** - Parse each YAML in `tables`
142
+ 3. **Extract sections** - Expand templates into per-section `Tcode` instances
143
+ 4. **Collect instructions (per section):**
144
144
  - Download source file (if URL specified)
145
- - Apply transformations
146
- - Resolve entities using `datassert`
147
- - Validate with QC pipeline when `qc: true`
148
- - Create subgraph parquet file
149
- 3. **Aggregate subgraphs** - Merge all parquet files
150
- 4. **Add provenance (optional)** - Query `pubmed_db` and `pmc_db` for metadata when configured
151
- 5. **Export NDJSON** - Generate `{name}_{version}.nodes.ndjson` and `.edges.ndjson`
145
+ - Apply transformations and resolve entities using `datassert`
146
+ - Validate with the QC audit when `qc: true`
147
+ - Enrich with provenance: query `pubmed_db` (MeSH) and `pmc_db` (captions) when configured
148
+ 5. **Build subgraphs** - Compile each section's resolved data into a parquet file
149
+ 6. **Compile graph** - Aggregate all subgraph parquets and export `{name}_{version}.nodes.ndjson` / `.edges.ndjson`
150
+
151
+ > Note: provenance enrichment (`with_mesh`/`with_captions`) and QC both run during the per-section Collect Instructions stage, *before* subgraphs are built — not as a separate post-aggregation step.
152
152
 
153
153
  ## Output Files
154
154
 
@@ -173,8 +173,8 @@ version: UNSTABLE
173
173
  tables:
174
174
  - /local_raid1/sgoetz/STORE/CONFIG/TABLASSERT/TABLE/V6/ALAMV6.yaml
175
175
  datassert: /local_raid1/sgoetz/CODE/DATASSERT/datassert
176
- pubmed_db: /local_raid1/sgoetz/DBSTORE/local_raid1/sgoetz/DBSTORE/PUBMED/PubMed.db
177
- pmc_db: /local_raid1/sgoetz/DBSTORE/local_raid1/sgoetz/DBSTORE/CAPTIONS/PMCSuppCaptions.db
176
+ pubmed_db: /local_raid1/sgoetz/DBSTORE/PUBMED/PubMed.db
177
+ pmc_db: /local_raid1/sgoetz/DBSTORE/CAPTIONS/PMCSuppCaptions.db
178
178
  ```
179
179
 
180
180
  This processes a single table configuration (ALAMV6.yaml) into a knowledge graph named `MULTIOMICS_KG_UNSTABLE`.
@@ -84,7 +84,7 @@ template:
84
84
  syntax: TC3
85
85
 
86
86
  sections:
87
- - syntax: TC2 # Overrides (not recommended)
87
+ - syntax: GC2 # Valid values are TC3 (default) and GC2; overriding is not recommended
88
88
  ```
89
89
 
90
90
  ### Use Cases
@@ -100,7 +100,7 @@ template:
100
100
  ```yaml
101
101
  template:
102
102
  source: {kind: excel, local: data.xlsx, url: https://example.com/data.xlsx}
103
- provenance: {repo: PMC, publication: 123, contributors: [{name: Example User, date: 27 JAN 2026}]}
103
+ provenance: {repo: PMC, publication: "PMC123", contributors: [{name: Example User, date: 27 JAN 2026}]}
104
104
 
105
105
  sections:
106
106
  - statement: {predicate: treats}
@@ -111,7 +111,7 @@ sections:
111
111
  ```yaml
112
112
  template:
113
113
  source: {kind: text, local: data.csv, url: https://example.com/data.csv}
114
- provenance: {repo: PMID, publication: 456, contributors: [{name: Example User, date: 27 JAN 2026}]}
114
+ provenance: {repo: PMID, publication: "456", contributors: [{name: Example User, date: 27 JAN 2026}]}
115
115
  statement:
116
116
  subject: {method: column, encoding: A}
117
117
 
@@ -141,8 +141,8 @@ Defines the data file location and format.
141
141
  | `local` | Path | Yes | Local file path for caching |
142
142
  | `url` | URL | Yes | Download URL (HTTP/HTTPS). Validated as reachable at parse time. |
143
143
  | `sheet` | String | No | Sheet name. Defaults to `"Sheet1"`. |
144
- | `row_slice` | List[Int\|"auto"] | No | Two-value zero-based crop bounds: `[start, stop]`. Each value may be an integer or `"auto"`. Mutually exclusive with `rows`. |
145
- | `rows` | List[Int] | No | Zero-based row indices to keep after any `row_slice` crop. Mutually exclusive with `row_slice`. |
144
+ | `row_slice` | List[PositiveInt\|"auto"] | No | Two-value zero-based crop bounds: `[start, stop]`. Each value may be a positive integer or `"auto"`. Mutually exclusive with `rows`. |
145
+ | `rows` | List[PositiveInt] | No | Zero-based row indices to keep after any `row_slice` crop. Mutually exclusive with `row_slice`. |
146
146
  | `reindex` | List[Reindex] | No | Conditional row filtering |
147
147
 
148
148
  **Example:**
@@ -165,8 +165,8 @@ source:
165
165
  | `local` | Path | Yes | Local file path for caching |
166
166
  | `url` | URL | Yes | Download URL. Validated as reachable at parse time. |
167
167
  | `delimiter` | String | No | Field delimiter. Defaults to `","`. |
168
- | `row_slice` | List[Int\|"auto"] | No | Two-value zero-based crop bounds: `[start, stop]`. Each value may be an integer or `"auto"`. Mutually exclusive with `rows`. |
169
- | `rows` | List[Int] | No | Zero-based row indices to keep after any `row_slice` crop. Mutually exclusive with `row_slice`. |
168
+ | `row_slice` | List[PositiveInt\|"auto"] | No | Two-value zero-based crop bounds: `[start, stop]`. Each value may be a positive integer or `"auto"`. Mutually exclusive with `rows`. |
169
+ | `rows` | List[PositiveInt] | No | Zero-based row indices to keep after any `row_slice` crop. Mutually exclusive with `row_slice`. |
170
170
  | `reindex` | List[Reindex] | No | Conditional filtering |
171
171
 
172
172
  **Example:**
@@ -185,11 +185,11 @@ source:
185
185
 
186
186
  Filter rows based on column values.
187
187
 
188
- | Field | Type | Description |
189
- |-------|------|-------------|
190
- | `column` | String | Source column letters to evaluate (`A`-`ZZZ`) |
191
- | `comparison` | String | Operator. Defaults to `"ne"`; allowed values are `"eq"`, `"ne"`, `"lt"`, `"le"`, `"gt"`, `"ge"`. |
192
- | `comparator` | String\|Int\|Float | Value to compare against. Must be a string for `"eq"`/`"ne"`, or a number for `"lt"`/`"le"`/`"gt"`/`"ge"`. |
188
+ | Field | Type | Required | Description |
189
+ |-------|------|----------|-------------|
190
+ | `column` | String | Yes | Source column letters to evaluate; constrained to `^[A-Z]{1,3}$` (`A`-`ZZZ`). |
191
+ | `comparison` | String | No | Operator. Defaults to `"ne"`; allowed values are `"eq"`, `"ne"`, `"lt"`, `"le"`, `"gt"`, `"ge"`. |
192
+ | `comparator` | String\|Int\|Float | Yes | Value to compare against. Must be a string for `"eq"`/`"ne"`, or a number for `"lt"`/`"le"`/`"gt"`/`"ge"`. |
193
193
 
194
194
  **Example:**
195
195
  ```yaml
@@ -232,12 +232,12 @@ Defines how to extract and resolve entities.
232
232
  |-------|------|----------|-------------|
233
233
  | `method` | String | No | `"value"` (literal) or `"column"` (source column letters). Defaults to `"value"`. |
234
234
  | `encoding` | String\|Int\|Float | Yes | Literal value or source column letters, depending on `method` |
235
- | `taxon` | Int | No | NCBI Taxon ID for filtering (e.g., `9606` for human) |
235
+ | `taxon` | PositiveInt | No | NCBI Taxon ID for filtering (e.g., `9606` for human) |
236
236
  | `prioritize` | List[String] | No | Preferred Biolink categories (must be valid `Categories` enum values such as `Gene`, `Protein`) |
237
237
  | `avoid` | List[String] | No | Excluded Biolink categories (must be valid `Categories` enum values) |
238
238
  | `regex` | List[Regex] | No | Pattern replacements |
239
239
  | `fill` | String | No | Null-filling strategy: `"forward"`, `"backward"`, `"min"`, `"max"`, `"mean"`, `"zero"`, `"one"` |
240
- | `remove` | List[String] | No | Strings to filter out |
240
+ | `remove` | List[Int\|Float\|String] | No | Regex patterns to remove (replaced with empty string) |
241
241
  | `prefix` | String | No | Add prefix to values |
242
242
  | `suffix` | String | No | Add suffix to values |
243
243
  | `explode_by` | String | No | Delimiter to split multi-value cells |
@@ -314,7 +314,7 @@ Prevents misclassifying organism names as genes.
314
314
 
315
315
  #### Text Transformations
316
316
 
317
- **`regex: list[{pattern, replacement}]`** - Pattern-based replacements
317
+ **`regex: list[{pattern: Int|Float|String, replacement: Int|Float|String}]`** - Pattern-based replacements
318
318
 
319
319
  ```yaml
320
320
  subject:
@@ -328,18 +328,18 @@ subject:
328
328
 
329
329
  Executed in order.
330
330
 
331
- > **Regex dialect:** Patterns are passed directly to Polars `str.replace_all()`, which uses the Rust [`regex`](https://docs.rs/regex/) crate. Only features supported by that engine work — in particular, **capturing groups (`(...)`, `\1`) and lookarounds (`(?=...)`, `(?<=...)`, `(?!...)`, `(?<!...)` are not supported** and will raise an error at parse time. Stick to character classes, anchors (`^`, `$`), quantifiers, alternation (`a|b`), and non-capturing groups (`(?:...)`) if grouping is needed. If a transformation is too complex to express, prefer chaining several simple substitutions or capturing the residual context in a `miscellaneous notes` annotation instead.
331
+ > **Regex dialect:** Patterns are passed directly to Polars `str.replace_all()`, which uses the Rust [`regex`](https://docs.rs/regex/) crate. Only features supported by that engine work — in particular, **backreferences (`\1`, `\2`, …) and lookarounds (`(?=...)`, `(?<=...)`, `(?!...)`, `(?<!...)`) are not supported** and will raise an error at parse time. Plain groups `(...)` and non-capturing groups `(?:...)` *are* supported. Stick to character classes, anchors (`^`, `$`), quantifiers, alternation (`a|b`), and grouping if needed. If a transformation is too complex to express, prefer chaining several simple substitutions or capturing the residual context in a `miscellaneous notes` annotation instead.
332
332
 
333
- **`remove: list[string]`** - Filter out specific strings
333
+ **`remove: list[regex]`** - Regex patterns to remove
334
334
 
335
335
  ```yaml
336
336
  subject:
337
337
  encoding: A
338
338
  remove:
339
- - "^NA " # Remove rows starting with "NA "
339
+ - "^NA " # Strip leading "NA " prefix from cell text
340
340
  ```
341
341
 
342
- Same regex constraints apply as the `regex` field — Polars-compatible patterns only, no capturing groups or lookarounds.
342
+ Each entry is applied as a regex replace-with-empty-string on the cell text in place (rows are not dropped). Same regex constraints apply as the `regex` field — Polars-compatible patterns only, no backreferences or lookarounds.
343
343
 
344
344
  **`prefix` / `suffix`** - Add text
345
345
 
@@ -349,6 +349,13 @@ object:
349
349
  prefix: "CUSTOM:" # "123" → "CUSTOM:123"
350
350
  ```
351
351
 
352
+ **Output columns: `original <col>` vs `<col> table literal value`** - For every subject/object/qualifier node, the pipeline snapshots the cell value into two edge columns at different stages:
353
+
354
+ - `<col> table literal value` - the **pristine source-cell value**, captured immediately after the column is read and *before* any `fill`, `explode_by`, `regex`, `remove`, `prefix`, `suffix`, or `transformations`. Emitted only when `method: column` (a `method: value` node has no table source).
355
+ - `original <col>` - the **fully-transformed value**, captured *after* all of the above, i.e. the same text that is then normalized and resolved to a CURIE. Always present for subject/object/qualifier nodes.
356
+
357
+ Example: with `method: column`, `encoding: A`, `remove: ["^NA "]` over a cell `"NA BRCA1"`, `subject table literal value` is `"NA BRCA1"` while `original subject` is `"BRCA1"`. Annotations never emit a table-literal column.
358
+
352
359
  #### Null Handling
353
360
 
354
361
  **`fill: string`** - Fill null values using a strategy
@@ -400,10 +407,10 @@ Use the `"values"` token to reference column values in transformations.
400
407
 
401
408
  Add context to edges (anatomical location, species, etc.).
402
409
 
403
- | Field | Type | Description |
404
- |-------|------|-------------|
405
- | `qualifier` | String | Biolink qualifier from the `Qualifiers` enum (e.g., `"species_context_qualifier"`) |
406
- | (inherits NodeEncoding) | | All NodeEncoding fields available |
410
+ | Field | Type | Required | Description |
411
+ |-------|------|----------|-------------|
412
+ | `qualifier` | String | Yes | Biolink qualifier from the `Qualifiers` enum (e.g., `"species_context_qualifier"`) |
413
+ | (inherits NodeEncoding) | | | All NodeEncoding fields available |
407
414
 
408
415
  **Example:**
409
416
  ```yaml
@@ -420,7 +427,7 @@ Required metadata about data source.
420
427
  | Field | Type | Required | Description |
421
428
  |-------|------|----------|-------------|
422
429
  | `repo` | String | No | Repository. Defaults to `"PMC"`; allowed values are `"PMC"`, `"PMID"`. |
423
- | `publication` | String | Yes | Repository-local identifier appended to `repo:` (e.g., `"11708054"`, `"123"`). When `repo` is `"PMC"`, the value **must** start with `PMC` followed by digits (e.g., `"PMC11708054"`, `"PMC123"`). |
430
+ | `publication` | String | Yes | Repository-local identifier emitted as a CURIE. For `repo: PMC` the value **must** start with `PMC` followed by digits (e.g., `"PMC11708054"`, `"PMC123"`) and is emitted under the `PMCID:` namespace as `PMCID:PMC...` (previously `PMC:PMC...`); for `repo: PMID` it is emitted as `PMID:<publication>` (e.g., `"11708054"` → `PMID:11708054`). The `repository` output column still records the raw `repo` value (`PMC`/`PMID`). |
424
431
  | `contributors` | List[Contributor] | Yes | Curation information |
425
432
 
426
433
  **Contributor fields:**
@@ -437,7 +444,7 @@ Required metadata about data source.
437
444
  ```yaml
438
445
  provenance:
439
446
  repo: PMC
440
- publication: 11708054
447
+ publication: "PMC11708054"
441
448
  contributors:
442
449
  - kind: curation
443
450
  name: Skye Lane Goetz
@@ -452,10 +459,10 @@ provenance:
452
459
 
453
460
  Optional edge attributes (statistical metadata, notes, etc.).
454
461
 
455
- | Field | Type | Description |
456
- |-------|------|-------------|
457
- | `annotation` | String | Attribute name (e.g., `"p value"`, `"sample size"`). Underscores are automatically replaced with spaces at parse time. |
458
- | (inherits Encoding) | | All Encoding fields available (method, encoding, regex, etc.) |
462
+ | Field | Type | Required | Description |
463
+ |-------|------|----------|-------------|
464
+ | `annotation` | String | Yes | Attribute name (e.g., `"p value"`, `"sample size"`). Underscores are automatically replaced with spaces and the result is trimmed of leading/trailing whitespace at parse time. |
465
+ | (inherits Encoding) | | | All Encoding fields available (method, encoding, regex, etc.) |
459
466
 
460
467
  **Example:**
461
468
  ```yaml
@@ -510,7 +517,7 @@ template:
510
517
 
511
518
  provenance:
512
519
  repo: PMID
513
- publication: 12345678
520
+ publication: "12345678"
514
521
  contributors:
515
522
  - kind: curation
516
523
  name: Example User
@@ -10,7 +10,7 @@ The image is based on `python:3.14-slim` with the Tablassert CLI as the entrypoi
10
10
  docker pull ghcr.io/skyeav/tablassert:latest
11
11
  ```
12
12
 
13
- Version-pinned tags match the git tag (e.g., `ghcr.io/skyeav/tablassert:v7.4.0`).
13
+ Version-pinned tags match the git tag (e.g., `ghcr.io/skyeav/tablassert:v7.5.0`).
14
14
 
15
15
  ## Quick Start
16
16
 
@@ -49,7 +49,7 @@ docker run --rm \
49
49
 
50
50
  All dependencies ship in the base install, so the Docker image includes:
51
51
 
52
- - **Quality control** — The QC pipeline in `src/tablassert/qc.py` runs a three-stage audit: exact match, then fuzzy matching via rapidfuzz (threshold >= 20), then BioBERT sentence embeddings with cosine similarity (threshold >= 0.2). The ONNX model is cached in `.onnxassert/` (line 26).
52
+ - **Quality control** — The QC pipeline in `src/tablassert/qc.py` runs a three-stage audit: exact match, then fuzzy matching via rapidfuzz (`fuzz.ratio` >= 20 or `partial_token_sort_ratio` >= 30), then BioBERT sentence embeddings with cosine similarity (threshold >= 0.2). The ONNX model is cached in `.onnxassert/` (line 24).
53
53
  - **Web downloads** — `src/tablassert/downloader.py` uses Playwright to download remote files with retry logic.
54
54
  - **Legacy Excel** — `modernize_xls()` in `src/tablassert/downloader.py` converts `.xls` files using pyexcel.
55
55
 
@@ -61,7 +61,8 @@ Mount these volumes to persist data across container runs:
61
61
  |---|---|---|
62
62
  | `.storassert/` | `src/tablassert/utils.py:17` — `STORE` | Intermediate Parquet storage for compiled subgraphs |
63
63
  | `.logassert/` | `src/tablassert/log.py` | Loguru log files with 100 MB rotation |
64
- | `.onnxassert/` | `src/tablassert/qc.py:26` — `MODEL` | Cached ONNX/BioBERT model |
64
+ | `.onnxassert/` | `src/tablassert/qc.py:24` — `MODEL` | Cached ONNX/BioBERT model |
65
+ | `.cachassert/` | `src/tablassert/models.py:76` — `CACHE` | diskcache store for URL-validation memoization |
65
66
 
66
67
  Example:
67
68
 
@@ -72,6 +73,7 @@ docker run --rm \
72
73
  -v ./.storassert:/app/.storassert \
73
74
  -v ./.logassert:/app/.logassert \
74
75
  -v ./.onnxassert:/app/.onnxassert \
76
+ -v ./.cachassert:/app/.cachassert \
75
77
  -w /app \
76
78
  ghcr.io/skyeav/tablassert:latest \
77
79
  build /data/graph-config.yaml
@@ -87,4 +89,4 @@ docker run --rm \
87
89
 
88
90
  ## CI/CD Integration
89
91
 
90
- Images are built by `.github/workflows/docker.yml`, which triggers on tag pushes (after autotag and PyPI publish complete). Tags match the repository version tag (e.g., `v7.2.2`).
92
+ Images are built by `.github/workflows/docker.yml`, which triggers when the `Auto Tag Versions` workflow completes on `main` (also runnable manually via `workflow_dispatch`). Tags match the repository version tag (e.g., `v7.5.0`).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tablassert"
3
- version = "7.4.14"
3
+ version = "7.5.0"
4
4
  description = "Extract knowledge assertions from tabular data into NCATS Translator-compliant KGX NDJSON — declaratively, with entity resolution and quality control built in."
5
5
  authors = [
6
6
  { name = "Skye Lane Goetz", email = "sgoetz@isbscience.org" }
@@ -14,7 +14,7 @@ import lazy_loader as Lazy
14
14
  from pydantic import Field, NonNegativeInt, PositiveInt
15
15
 
16
16
  from tablassert.downloader import from_url
17
- from tablassert.enums import Categories, EncodingMethods, Files, Tokens
17
+ from tablassert.enums import Categories, EncodingMethods, Files, Repositories, Tokens
18
18
  from tablassert.fullmap import SHARDS, resolve
19
19
  from tablassert.log import cat
20
20
  from tablassert.models import Encoding, NodeEncoding, Section
@@ -279,11 +279,14 @@ class Tcode(Section):
279
279
  log: bool = Field(False)
280
280
  qc: bool = Field(False)
281
281
 
282
- def encoding(self: Self, x: Encoding, col: str) -> list[Any]:
282
+ def encoding(self: Self, x: Encoding, col: str, table_literal: bool = False) -> list[Any]:
283
283
  # ? Collect Helper For Encoding Classes
284
284
  return [
285
285
  (value, (col, x.encoding)) if eq(x.method, EncodingMethods.VALUE) else None,
286
286
  (column, (col, idxname(x.encoding))) if eq(x.method, EncodingMethods.COLUMN) else None,
287
+ (column, (add(col, " table literal value"), col))
288
+ if (table_literal and eq(x.method, EncodingMethods.COLUMN))
289
+ else None,
287
290
  (fill, (col, x.fill)) if x.fill else None,
288
291
  (explode, (col, x.explode_by)) if x.explode_by else None,
289
292
  [(regex, (col, r.pattern, r.replacement)) for r in x.regex] if x.regex else None,
@@ -295,7 +298,7 @@ class Tcode(Section):
295
298
 
296
299
  def node(self: Self, x: NodeEncoding, col: str, conns: list[object]) -> list[Any]:
297
300
  # ? Collect Helper For NodeEncoding Classes
298
- encoding: list[Any] = self.encoding(x, col)
301
+ encoding: list[Any] = self.encoding(x, col, table_literal=True)
299
302
  node: list[Any] = [
300
303
  (column, (add("original ", col), col)),
301
304
  (level_one, (col,)),
@@ -355,10 +358,11 @@ class Tcode(Section):
355
358
  (value, ("section number", self.number)),
356
359
  (value, ("status", self.status)),
357
360
  (value, ("repository", self.provenance.repo)),
358
- (value, ("publication", (self.provenance.repo + ":" + self.provenance.publication))),
361
+ (value, ("publication", publication_curie(self.provenance.repo, self.provenance.publication))),
359
362
  (contributor_values, ("contributors", self.provenance.contributors)),
360
363
  (value, ("url", str(self.source.url))),
361
364
  (value, ("section hash", self.store.stem)),
365
+ (value, ("sheet name", self.source.sheet)) if eq(self.source.kind, Files.EXCEL) else None, # pyright: ignore
362
366
  (with_mesh, (pubmed_db, self.provenance.publication)) if pubmed_db else None,
363
367
  (with_captions, (pmc_db, self.provenance.publication, str(self.source.url))) if pmc_db else None,
364
368
  (sig, ()),
@@ -391,6 +395,13 @@ def normalize(
391
395
  return nodes, edges_out
392
396
 
393
397
 
398
+ def publication_curie(repo: str, publication: str) -> str:
399
+ # ? Builds The Publication CURIE; PMCID Namespace For PubMed Central
400
+ if eq(repo, Repositories.PUBMED_CENTRAL):
401
+ return add("PMCID:", publication)
402
+ return add(repo, add(":", publication))
403
+
404
+
394
405
  def publications(
395
406
  edges: pl.LazyFrame, names: list[str] = ["id", "name", "first author", "journal", "year published"]
396
407
  ) -> tuple[pl.LazyFrame, pl.LazyFrame]:
@@ -182,6 +182,59 @@ def test_tcode_collect_enables_qc_logging(fixtures_path: Path) -> None:
182
182
  assert qc_ops[1][1] == ("object", "sectionhash", "minimal_section.yaml", "passed", True)
183
183
 
184
184
 
185
+ # ? publication_curie Uses PMCID Namespace For PubMed Central
186
+ def test_publication_curie_pmc() -> None:
187
+ assert lib.publication_curie("PMC", "PMC1234567") == "PMCID:PMC1234567"
188
+
189
+
190
+ # ? publication_curie Uses Repo Namespace For Non PMC Repositories
191
+ def test_publication_curie_pubmed() -> None:
192
+ assert lib.publication_curie("PMID", "11708054") == "PMID:11708054"
193
+
194
+
195
+ # ? Tcode Captures Table Literal Value Before Regex For Column Encoded Nodes
196
+ def test_tcode_table_literal_value_before_regex_for_columns(fixtures_path: Path) -> None:
197
+ data: Any = from_yaml(fixtures_path / "minimal_section.yaml")
198
+ store: Path = Path("/tmp/sectionhash.parquet")
199
+ data["statement"]["subject"] = {
200
+ "method": "column",
201
+ "encoding": "A",
202
+ "regex": [{"pattern": "\\s+", "replacement": " "}],
203
+ }
204
+ data["statement"]["object"] = {"method": "column", "encoding": "B"}
205
+
206
+ tcode_model: Tcode = Tcode.model_validate( # pyright: ignore
207
+ {**data, "number": 7, "config": fixtures_path / "minimal_section.yaml", "store": store}
208
+ )
209
+
210
+ collected: list[tuple[Any, tuple[Any]]] = tcode_model.collect([], None, None) # pyright: ignore
211
+ targets: list[str] = [op[1][0] for op in collected if op[0].__name__ == "column" and len(op[1]) > 1]
212
+ assert "subject table literal value" in targets
213
+ assert "object table literal value" in targets
214
+
215
+ lit_idx: int = next(
216
+ i for i, op in enumerate(collected) if len(op[1]) > 0 and op[1][0] == "subject table literal value"
217
+ )
218
+ regex_idx: int = next(
219
+ i for i, op in enumerate(collected) if op[0].__name__ == "regex" and len(op[1]) > 0 and op[1][0] == "subject"
220
+ )
221
+ assert lit_idx < regex_idx
222
+
223
+
224
+ # ? Tcode Omits Table Literal Value For Value Encoded Nodes
225
+ def test_tcode_table_literal_value_absent_for_value_encoding(fixtures_path: Path) -> None:
226
+ data: Any = from_yaml(fixtures_path / "minimal_section.yaml")
227
+ store: Path = Path("/tmp/sectionhash.parquet")
228
+ tcode_model: Tcode = Tcode.model_validate( # pyright: ignore
229
+ {**data, "number": 7, "config": fixtures_path / "minimal_section.yaml", "store": store}
230
+ )
231
+
232
+ collected: list[tuple[Any, tuple[Any]]] = tcode_model.collect([], None, None) # pyright: ignore
233
+ targets: list[str] = [op[1][0] for op in collected if op[0].__name__ == "column" and len(op[1]) > 1]
234
+ assert "subject table literal value" not in targets
235
+ assert "object table literal value" not in targets
236
+
237
+
185
238
  # ? resolve_many Skips QC When Disabled
186
239
  def test_resolve_many_skips_qc(monkeypatch: Any, tmp_path: Path) -> None:
187
240
  calls: list[tuple[Any, ...]] = []
@@ -2340,7 +2340,7 @@ wheels = [
2340
2340
 
2341
2341
  [[package]]
2342
2342
  name = "tablassert"
2343
- version = "7.4.14"
2343
+ version = "7.5.0"
2344
2344
  source = { editable = "." }
2345
2345
  dependencies = [
2346
2346
  { name = "cyclopts" },
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