From f012cd3445fb28fcea0390fdeac9a3122ba7dd80 Mon Sep 17 00:00:00 2001 From: "marcin p. joachimiak" <4625870+realmarcin@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:02:12 -0700 Subject: [PATCH 1/3] ontologies: NCBITaxon .owl/.db version-alignment gate (#604 fix 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the remaining piece of #604. metatraits/lpsn/bacdive do label + lineage lookups against the OAK-fetched ncbitaxon.db (whose release is whatever OAK last downloaded), while the NCBITaxon transform emits nodes from ncbitaxon.owl — nothing pinned the two to the same release. - assert_ncbitaxon_version_alignment(db_path): compares ncbitaxon.owl's versionIRI release with the db's owl:versionInfo (via _ncbitaxon_db_release, a small SemSQL query) and fails loudly on mismatch; KG_NCBITAXON_VERSION_CHECK =warn downgrades to a warning. No-op when either stamp is unreadable. - Generalized _obo_release_from_head to also match NCBITaxon's versionIRI form (`ncbitaxon/DATE/`, no `releases/` segment). - Factored the strictness resolution shared with the GO gate into _version_check_strict. - Wired the gate into metatraits `_ensure_ncbitaxon_db_ready` (both the happy-path and cache-repair success branches). Verified on the current sources: ncbitaxon.owl and ncbitaxon.db are both 2026-05-13 → gate passes silently. 7 new tests (owl/db release parsing, aligned/mismatch/env-warn/missing-stamp); GO gate unchanged (11 tests). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../transform_utils/metatraits/metatraits.py | 4 + kg_microbe/utils/ontology_utils.py | 82 ++++++++++++++++- tests/test_ncbitaxon_version_alignment.py | 87 +++++++++++++++++++ 3 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 tests/test_ncbitaxon_version_alignment.py diff --git a/kg_microbe/transform_utils/metatraits/metatraits.py b/kg_microbe/transform_utils/metatraits/metatraits.py index 7e04fd4e..72061f4a 100644 --- a/kg_microbe/transform_utils/metatraits/metatraits.py +++ b/kg_microbe/transform_utils/metatraits/metatraits.py @@ -108,12 +108,15 @@ def _ensure_ncbitaxon_db_ready() -> None: Raises RuntimeError with remediation steps if no valid DB can be found. """ + from kg_microbe.utils.ontology_utils import assert_ncbitaxon_version_alignment + local_db, oak_cache = _ncbitaxon_db_paths() # Happy path: symlink resolves to a valid DB. ok, reason = _validate_ncbitaxon_db(local_db) if ok: print(f" NCBITaxon DB validated: {local_db} -> {local_db.resolve()}") + assert_ncbitaxon_version_alignment(str(local_db)) return print(f" NCBITaxon DB at {local_db} invalid ({reason}); attempting repair") @@ -126,6 +129,7 @@ def _ensure_ncbitaxon_db_ready() -> None: local_db.unlink() local_db.symlink_to(oak_cache) print(f" Repaired symlink: {local_db} -> {oak_cache}") + assert_ncbitaxon_version_alignment(str(local_db)) return print(f" OAK cache also invalid ({cache_reason})") diff --git a/kg_microbe/utils/ontology_utils.py b/kg_microbe/utils/ontology_utils.py index e9c000a6..46ebb7ee 100644 --- a/kg_microbe/utils/ontology_utils.py +++ b/kg_microbe/utils/ontology_utils.py @@ -53,6 +53,11 @@ def _obo_release_from_head(path: Path, nbytes: int = 2_000_000) -> Optional[str] except OSError: return None m = _RELEASE_RE.search(head) + if m: + return m.group(1) + # OWL versionIRI whose date isn't under a ``releases/`` path — e.g. NCBITaxon's + # ``versionIRI rdf:resource=".../ncbitaxon/2026-05-13/ncbitaxon.owl"``. + m = re.search(r"versionIRI[^>]{0,160}?(\d{4}-\d{2}-\d{2})", head) if m: return m.group(1) # OBO-JSON versionInfo, e.g. {"pred": ".../versionInfo", "val": "2026-05-19"} @@ -61,6 +66,18 @@ def _obo_release_from_head(path: Path, nbytes: int = 2_000_000) -> Optional[str] return m.group(1) if m else None +def _version_check_strict(env_var: str, strict: Optional[bool]) -> bool: + """ + Resolve a version-gate's strictness: explicit arg wins, else the env var. + + Defaults to fail-loud (raise). ``=warn`` downgrades to a warning — + an escape hatch when the release-stamp heuristic disagrees spuriously. + """ + if strict is not None: + return strict + return os.environ.get(env_var, "strict").strip().lower() != "warn" + + def assert_go_version_alignment(strict: Optional[bool] = None) -> None: """ Guard that GO's aspect-map source (go.owl→go.db) matches the transform's (go.json). @@ -79,8 +96,7 @@ def assert_go_version_alignment(strict: Optional[bool] = None) -> None: """ from kg_microbe.transform_utils.constants import GO_SOURCE - if strict is None: - strict = os.environ.get("KG_GO_VERSION_CHECK", "strict").strip().lower() != "warn" + strict = _version_check_strict("KG_GO_VERSION_CHECK", strict) if not GO_SOURCE: return owl_release = _obo_release_from_head(Path(GO_SOURCE)) @@ -99,6 +115,68 @@ def assert_go_version_alignment(strict: Optional[bool] = None) -> None: print(f"WARNING: {msg}") +def _ncbitaxon_db_release(db_path: str) -> Optional[str]: + """ + Return the NCBITaxon release (YYYY-MM-DD) recorded in a SemSQL ``.db``. + + Reads ``owl:versionInfo`` from the ``statements`` table (OAK/SemanticSQL + stores the ontology's version there). Returns None on any read error or a + missing/unparseable stamp. + """ + try: + conn = sqlite3.connect(db_path) + try: + row = conn.execute( + "SELECT value FROM statements WHERE predicate = 'owl:versionInfo' " + "AND value IS NOT NULL LIMIT 1" + ).fetchone() + finally: + conn.close() + except sqlite3.Error: + return None + if not row or not row[0]: + return None + m = re.search(r"(\d{4}-\d{2}-\d{2})", str(row[0])) + return m.group(1) if m else None + + +def assert_ncbitaxon_version_alignment(db_path: str, strict: Optional[bool] = None) -> None: + """ + Guard that the NCBITaxon lookup DB matches the transform's OWL release. + + metatraits/lpsn/bacdive do label + lineage lookups against ``ncbitaxon.db`` + (an OAK-fetched prebuilt SemSQL DB whose release is whatever OAK last + downloaded), while the NCBITaxon transform output is built from + ``ncbitaxon.owl``. If the two are different releases, lookups can resolve + against taxa that differ from those emitted by the transform. Compare the + ``owl:versionInfo`` in ``db_path`` with ``ncbitaxon.owl``'s versionIRI and, + on mismatch, raise (default) or warn. No-op when either stamp can't be read. + + ``KG_NCBITAXON_VERSION_CHECK=warn`` downgrades the default fail-loud to a + warning (release-stamp heuristic escape hatch). + """ + from kg_microbe.transform_utils.constants import NCBITAXON_SOURCE + + strict = _version_check_strict("KG_NCBITAXON_VERSION_CHECK", strict) + if not NCBITAXON_SOURCE: + return + owl_release = _obo_release_from_head(Path(NCBITAXON_SOURCE)) + db_release = _ncbitaxon_db_release(db_path) + if owl_release and db_release and owl_release != db_release: + msg = ( + f"NCBITaxon source version mismatch: ncbitaxon.owl={owl_release} vs " + f"ncbitaxon.db={db_release}. metatraits/lpsn/bacdive look taxa up in " + "ncbitaxon.db while the transform emits nodes from ncbitaxon.owl; the " + "two releases must match. Re-download NCBITaxon and refresh the OAK " + "SemSQL DB so both are the same release " + "(rm ~/.data/oaklib/ncbitaxon.db; poetry run python -c " + "'from oaklib import get_adapter; get_adapter(\"sqlite:obo:ncbitaxon\")')." + ) + if strict: + raise RuntimeError(msg) + print(f"WARNING: {msg}") + + def _ensure_go_db(go_db_path: str) -> bool: """ Build the GO SemSQL DB from ``go.owl`` if missing/empty; return True if usable. diff --git a/tests/test_ncbitaxon_version_alignment.py b/tests/test_ncbitaxon_version_alignment.py new file mode 100644 index 00000000..1f8be0ea --- /dev/null +++ b/tests/test_ncbitaxon_version_alignment.py @@ -0,0 +1,87 @@ +"""Tests for the NCBITaxon .owl/.db version-alignment gate (#604 fix 4).""" + +import sqlite3 +from pathlib import Path + +import pytest + +from kg_microbe.utils import ontology_utils as ou + +# NCBITaxon stamps its version as ``ncbitaxon/DATE/`` (no ``releases/`` segment). +_OWL = '' + + +def _write_owl(tmp_path: Path, release: str) -> Path: + p = tmp_path / "ncbitaxon.owl" + p.write_text(_OWL.format(d=release), encoding="utf-8") + return p + + +def _make_db(tmp_path: Path, release, name: str = "ncbitaxon.db") -> str: + """Write a minimal SemSQL-shaped sqlite with an owl:versionInfo row.""" + path = str(tmp_path / name) + conn = sqlite3.connect(path) + conn.execute("CREATE TABLE statements (subject TEXT, predicate TEXT, object TEXT, value TEXT)") + if release is not None: + conn.execute( + "INSERT INTO statements (subject, predicate, object, value) " + "VALUES ('obo:ncbitaxon.owl', 'owl:versionInfo', NULL, ?)", + (release,), + ) + conn.commit() + conn.close() + return path + + +def test_owl_release_parsed_from_ncbitaxon_versioniri(tmp_path): + """The date is read from NCBITaxon's ``ncbitaxon/DATE/`` versionIRI (no releases/).""" + assert ou._obo_release_from_head(_write_owl(tmp_path, "2026-05-13")) == "2026-05-13" + + +def test_db_release_read_from_versioninfo(tmp_path): + """_ncbitaxon_db_release reads owl:versionInfo from the SemSQL statements table.""" + assert ou._ncbitaxon_db_release(_make_db(tmp_path, "2026-05-13")) == "2026-05-13" + + +def test_db_release_none_when_absent(tmp_path): + """A db with no versionInfo row (or an unreadable path) yields None.""" + assert ou._ncbitaxon_db_release(_make_db(tmp_path, None)) is None + assert ou._ncbitaxon_db_release(str(tmp_path / "missing.db")) is None + + +def test_aligned_does_not_raise(tmp_path, monkeypatch): + """Matching ncbitaxon.owl and ncbitaxon.db releases pass silently.""" + monkeypatch.setattr( + "kg_microbe.transform_utils.constants.NCBITAXON_SOURCE", _write_owl(tmp_path, "2026-05-13") + ) + db = _make_db(tmp_path, "2026-05-13") + ou.assert_ncbitaxon_version_alignment(db, strict=True) # must not raise + + +def test_mismatch_raises_strict(tmp_path, monkeypatch): + """Divergent .owl/.db releases fail loudly under strict.""" + monkeypatch.setattr( + "kg_microbe.transform_utils.constants.NCBITAXON_SOURCE", _write_owl(tmp_path, "2026-05-13") + ) + db = _make_db(tmp_path, "2026-01-01") + with pytest.raises(RuntimeError, match="NCBITaxon source version mismatch"): + ou.assert_ncbitaxon_version_alignment(db, strict=True) + + +def test_env_var_downgrades_to_warn(tmp_path, monkeypatch, capsys): + """KG_NCBITAXON_VERSION_CHECK=warn downgrades the default gate to a warning.""" + monkeypatch.setattr( + "kg_microbe.transform_utils.constants.NCBITAXON_SOURCE", _write_owl(tmp_path, "2026-05-13") + ) + monkeypatch.setenv("KG_NCBITAXON_VERSION_CHECK", "warn") + db = _make_db(tmp_path, "2026-01-01") + ou.assert_ncbitaxon_version_alignment(db) # strict=None → env warn → no raise + assert "NCBITaxon source version mismatch" in capsys.readouterr().out + + +def test_missing_stamp_is_a_noop(tmp_path, monkeypatch): + """When the db has no version stamp, the gate can't judge → no-op.""" + monkeypatch.setattr( + "kg_microbe.transform_utils.constants.NCBITAXON_SOURCE", _write_owl(tmp_path, "2026-05-13") + ) + ou.assert_ncbitaxon_version_alignment(_make_db(tmp_path, None), strict=True) # must not raise From 47d12859a24b35a1a13a4bcade9e8869454373f7 Mon Sep 17 00:00:00 2001 From: "marcin p. joachimiak" <4625870+realmarcin@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:09:22 -0700 Subject: [PATCH 2/3] =?UTF-8?q?ncbitaxon=20gate:=20address=20review=20?= =?UTF-8?q?=E2=80=94=20run=20in=20sequential=20mode,=20warn=20by=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses PR #607 review: - #1 (MEDIUM): the gate only ran under `if use_mp:` — so sequential metatraits (METATRAITS_MULTIPROCESSING=false / single worker) skipped it entirely. Moved the alignment call to run unconditionally in run() (no-ops if the DB isn't materialized yet), keeping the MP-only cache-repair pre-flight as-is. - #2 (MEDIUM): default the NCBITaxon gate to WARN, not raise — the OAK cache and the pinned ncbitaxon.owl legitimately drift (OAK auto-refreshes) and NCBITaxon labels/lineage are stable, so a mismatch is worth surfacing loudly but not aborting a previously-green run. KG_NCBITAXON_VERSION_CHECK=strict (or strict=True) opts back into fail-loud. _version_check_strict grew a per-gate default_strict; GO stays strict. - #3 (LOW/MED): target the ontology node in the versionInfo query (`subject LIKE '%ncbitaxon%'`) instead of relying on a single row. - #4 (LOW): correct the docstring/message — only metatraits invokes the gate (not lpsn/bacdive). - Tests: default-warn, env-strict-upgrade, subject-targeting (decoy row), and corrupt-db→None (21 total; GO's 11 unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../transform_utils/metatraits/metatraits.py | 13 +++-- kg_microbe/utils/ontology_utils.py | 47 +++++++++++------- tests/test_ncbitaxon_version_alignment.py | 48 ++++++++++++++++--- 3 files changed, 79 insertions(+), 29 deletions(-) diff --git a/kg_microbe/transform_utils/metatraits/metatraits.py b/kg_microbe/transform_utils/metatraits/metatraits.py index 72061f4a..6b5b9bf2 100644 --- a/kg_microbe/transform_utils/metatraits/metatraits.py +++ b/kg_microbe/transform_utils/metatraits/metatraits.py @@ -108,15 +108,12 @@ def _ensure_ncbitaxon_db_ready() -> None: Raises RuntimeError with remediation steps if no valid DB can be found. """ - from kg_microbe.utils.ontology_utils import assert_ncbitaxon_version_alignment - local_db, oak_cache = _ncbitaxon_db_paths() # Happy path: symlink resolves to a valid DB. ok, reason = _validate_ncbitaxon_db(local_db) if ok: print(f" NCBITaxon DB validated: {local_db} -> {local_db.resolve()}") - assert_ncbitaxon_version_alignment(str(local_db)) return print(f" NCBITaxon DB at {local_db} invalid ({reason}); attempting repair") @@ -129,7 +126,6 @@ def _ensure_ncbitaxon_db_ready() -> None: local_db.unlink() local_db.symlink_to(oak_cache) print(f" Repaired symlink: {local_db} -> {oak_cache}") - assert_ncbitaxon_version_alignment(str(local_db)) return print(f" OAK cache also invalid ({cache_reason})") @@ -3603,6 +3599,15 @@ def run( if use_mp: _ensure_ncbitaxon_db_ready() + # Version-alignment guard runs regardless of MP mode (the mismatch it + # catches is independent of parallelism). No-ops when the DB isn't + # present yet — sequential runs fetch it lazily downstream. + local_db, _ = _ncbitaxon_db_paths() + if local_db.exists(): + from kg_microbe.utils.ontology_utils import assert_ncbitaxon_version_alignment + + assert_ncbitaxon_version_alignment(str(local_db)) + # Decide whether to use parallel or sequential processing try: if use_mp and len(input_files) > 1: diff --git a/kg_microbe/utils/ontology_utils.py b/kg_microbe/utils/ontology_utils.py index 46ebb7ee..2ec079bc 100644 --- a/kg_microbe/utils/ontology_utils.py +++ b/kg_microbe/utils/ontology_utils.py @@ -66,16 +66,23 @@ def _obo_release_from_head(path: Path, nbytes: int = 2_000_000) -> Optional[str] return m.group(1) if m else None -def _version_check_strict(env_var: str, strict: Optional[bool]) -> bool: +def _version_check_strict(env_var: str, strict: Optional[bool], default_strict: bool = True) -> bool: """ Resolve a version-gate's strictness: explicit arg wins, else the env var. - Defaults to fail-loud (raise). ``=warn`` downgrades to a warning — - an escape hatch when the release-stamp heuristic disagrees spuriously. + ```` may be ``strict`` (raise) or ``warn``; unset falls back to + ``default_strict`` (which differs per gate — GO defaults strict because a + mismatch silently corrupts categories, NCBITaxon defaults warn because + owl/db release drift is common and low-risk). """ if strict is not None: return strict - return os.environ.get(env_var, "strict").strip().lower() != "warn" + val = os.environ.get(env_var, "").strip().lower() + if val == "warn": + return False + if val == "strict": + return True + return default_strict def assert_go_version_alignment(strict: Optional[bool] = None) -> None: @@ -126,9 +133,11 @@ def _ncbitaxon_db_release(db_path: str) -> Optional[str]: try: conn = sqlite3.connect(db_path) try: + # Target the ontology node's versionInfo (subject carries "ncbitaxon") + # rather than any entity that happens to be annotated with one. row = conn.execute( "SELECT value FROM statements WHERE predicate = 'owl:versionInfo' " - "AND value IS NOT NULL LIMIT 1" + "AND subject LIKE '%ncbitaxon%' AND value IS NOT NULL LIMIT 1" ).fetchone() finally: conn.close() @@ -144,20 +153,22 @@ def assert_ncbitaxon_version_alignment(db_path: str, strict: Optional[bool] = No """ Guard that the NCBITaxon lookup DB matches the transform's OWL release. - metatraits/lpsn/bacdive do label + lineage lookups against ``ncbitaxon.db`` - (an OAK-fetched prebuilt SemSQL DB whose release is whatever OAK last - downloaded), while the NCBITaxon transform output is built from - ``ncbitaxon.owl``. If the two are different releases, lookups can resolve - against taxa that differ from those emitted by the transform. Compare the + The metatraits transform looks taxa up in ``ncbitaxon.db`` (an OAK-fetched + prebuilt SemSQL DB whose release is whatever OAK last downloaded) while its + nodes are emitted from ``ncbitaxon.owl``. If the two are different releases, + lookups can resolve against taxa that differ from those emitted. Compare the ``owl:versionInfo`` in ``db_path`` with ``ncbitaxon.owl``'s versionIRI and, - on mismatch, raise (default) or warn. No-op when either stamp can't be read. + on mismatch, warn (default) or raise. No-op when either stamp can't be read. - ``KG_NCBITAXON_VERSION_CHECK=warn`` downgrades the default fail-loud to a - warning (release-stamp heuristic escape hatch). + Unlike the GO gate this **defaults to warn** — the OAK cache and the pinned + ``ncbitaxon.owl`` legitimately drift (OAK auto-refreshes to the latest), + and NCBITaxon labels/lineage are stable, so a mismatch is worth surfacing + loudly but not aborting. Set ``KG_NCBITAXON_VERSION_CHECK=strict`` (or pass + ``strict=True``) to fail loud instead. """ from kg_microbe.transform_utils.constants import NCBITAXON_SOURCE - strict = _version_check_strict("KG_NCBITAXON_VERSION_CHECK", strict) + strict = _version_check_strict("KG_NCBITAXON_VERSION_CHECK", strict, default_strict=False) if not NCBITAXON_SOURCE: return owl_release = _obo_release_from_head(Path(NCBITAXON_SOURCE)) @@ -165,10 +176,10 @@ def assert_ncbitaxon_version_alignment(db_path: str, strict: Optional[bool] = No if owl_release and db_release and owl_release != db_release: msg = ( f"NCBITaxon source version mismatch: ncbitaxon.owl={owl_release} vs " - f"ncbitaxon.db={db_release}. metatraits/lpsn/bacdive look taxa up in " - "ncbitaxon.db while the transform emits nodes from ncbitaxon.owl; the " - "two releases must match. Re-download NCBITaxon and refresh the OAK " - "SemSQL DB so both are the same release " + f"ncbitaxon.db={db_release}. The metatraits transform emits nodes from " + "ncbitaxon.owl but looks taxa up in ncbitaxon.db; a release gap can " + "resolve lookups against taxa the transform didn't emit. To realign, " + "refresh the OAK SemSQL DB to the pinned release " "(rm ~/.data/oaklib/ncbitaxon.db; poetry run python -c " "'from oaklib import get_adapter; get_adapter(\"sqlite:obo:ncbitaxon\")')." ) diff --git a/tests/test_ncbitaxon_version_alignment.py b/tests/test_ncbitaxon_version_alignment.py index 1f8be0ea..2bfc29a3 100644 --- a/tests/test_ncbitaxon_version_alignment.py +++ b/tests/test_ncbitaxon_version_alignment.py @@ -58,8 +58,18 @@ def test_aligned_does_not_raise(tmp_path, monkeypatch): ou.assert_ncbitaxon_version_alignment(db, strict=True) # must not raise -def test_mismatch_raises_strict(tmp_path, monkeypatch): - """Divergent .owl/.db releases fail loudly under strict.""" +def test_default_is_warn_not_raise(tmp_path, monkeypatch, capsys): + """Unlike GO, the NCBITaxon gate defaults to warn (owl/db drift is expected).""" + monkeypatch.setattr( + "kg_microbe.transform_utils.constants.NCBITAXON_SOURCE", _write_owl(tmp_path, "2026-05-13") + ) + db = _make_db(tmp_path, "2026-01-01") + ou.assert_ncbitaxon_version_alignment(db) # strict=None → default warn → no raise + assert "NCBITaxon source version mismatch" in capsys.readouterr().out + + +def test_mismatch_raises_when_strict(tmp_path, monkeypatch): + """strict=True (or KG_NCBITAXON_VERSION_CHECK=strict) fails loudly.""" monkeypatch.setattr( "kg_microbe.transform_utils.constants.NCBITAXON_SOURCE", _write_owl(tmp_path, "2026-05-13") ) @@ -68,15 +78,39 @@ def test_mismatch_raises_strict(tmp_path, monkeypatch): ou.assert_ncbitaxon_version_alignment(db, strict=True) -def test_env_var_downgrades_to_warn(tmp_path, monkeypatch, capsys): - """KG_NCBITAXON_VERSION_CHECK=warn downgrades the default gate to a warning.""" +def test_env_var_strict_upgrades_to_raise(tmp_path, monkeypatch): + """KG_NCBITAXON_VERSION_CHECK=strict upgrades the warn default to a raise.""" monkeypatch.setattr( "kg_microbe.transform_utils.constants.NCBITAXON_SOURCE", _write_owl(tmp_path, "2026-05-13") ) - monkeypatch.setenv("KG_NCBITAXON_VERSION_CHECK", "warn") + monkeypatch.setenv("KG_NCBITAXON_VERSION_CHECK", "strict") db = _make_db(tmp_path, "2026-01-01") - ou.assert_ncbitaxon_version_alignment(db) # strict=None → env warn → no raise - assert "NCBITaxon source version mismatch" in capsys.readouterr().out + with pytest.raises(RuntimeError, match="NCBITaxon source version mismatch"): + ou.assert_ncbitaxon_version_alignment(db) # strict=None → env strict → raise + + +def test_versioninfo_query_targets_ontology_subject(tmp_path): + """A decoy owl:versionInfo on a non-ontology subject is ignored (subject targeting).""" + path = str(tmp_path / "ncbitaxon.db") + conn = sqlite3.connect(path) + conn.execute("CREATE TABLE statements (subject TEXT, predicate TEXT, object TEXT, value TEXT)") + # Decoy first (a random entity annotated with a version-shaped date), then the ontology row. + conn.execute( + "INSERT INTO statements VALUES ('SomeEntity:1', 'owl:versionInfo', NULL, '1999-12-31')" + ) + conn.execute( + "INSERT INTO statements VALUES ('obo:ncbitaxon.owl', 'owl:versionInfo', NULL, '2026-05-13')" + ) + conn.commit() + conn.close() + assert ou._ncbitaxon_db_release(path) == "2026-05-13" + + +def test_corrupt_db_yields_none(tmp_path): + """A non-sqlite / corrupt file is handled (sqlite3.Error → None), not raised.""" + corrupt = tmp_path / "ncbitaxon.db" + corrupt.write_text("this is not a sqlite database", encoding="utf-8") + assert ou._ncbitaxon_db_release(str(corrupt)) is None def test_missing_stamp_is_a_noop(tmp_path, monkeypatch): From 28fa56a5777ece9fdb89861846e1a73b3341de69 Mon Sep 17 00:00:00 2001 From: "marcin p. joachimiak" <4625870+realmarcin@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:11:46 -0700 Subject: [PATCH 3/3] fix codespell: unparseable -> unparsable (ontology_utils docstring) Co-Authored-By: Claude Opus 4.8 (1M context) --- kg_microbe/utils/ontology_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kg_microbe/utils/ontology_utils.py b/kg_microbe/utils/ontology_utils.py index 2ec079bc..5b17015b 100644 --- a/kg_microbe/utils/ontology_utils.py +++ b/kg_microbe/utils/ontology_utils.py @@ -128,7 +128,7 @@ def _ncbitaxon_db_release(db_path: str) -> Optional[str]: Reads ``owl:versionInfo`` from the ``statements`` table (OAK/SemanticSQL stores the ontology's version there). Returns None on any read error or a - missing/unparseable stamp. + missing/unparsable stamp. """ try: conn = sqlite3.connect(db_path)