Skip to content

Commit f850247

Browse files
feat: consumer schema-location api and validate command
1 parent c4ea05b commit f850247

3 files changed

Lines changed: 82 additions & 0 deletions

File tree

src/lokf/cli.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
lokf new my-kb # scaffold a knowledge base
44
lokf convert path/to/concept.md --format ttl # markdown -> RDF
5+
lokf validate knowledge # assemble + validate a bundle
56
lokf query examples/acme-knowledge "SELECT ..." # SPARQL over a bundle
67
lokf serve examples/acme-knowledge # local SPARQL endpoint + viz
78
lokf propose examples/acme-knowledge --apply # typed relations from links
@@ -94,6 +95,62 @@ def convert(
9495
typer.echo(data, nl=False)
9596

9697

98+
# ---------------------------------------------------------------------------
99+
# validate
100+
# ---------------------------------------------------------------------------
101+
@app.command()
102+
def validate(
103+
bundle_dir: Path = typer.Argument(
104+
Path("knowledge"), exists=True, file_okay=False,
105+
help="A LOKF bundle directory to assemble and validate.",
106+
),
107+
schema: Optional[Path] = typer.Option(
108+
None, "--schema", "-s",
109+
help="Schema file (default: a local lokf.yaml checkout, else the copy "
110+
"packaged with lokf).",
111+
),
112+
) -> None:
113+
"""Assemble a bundle and validate it against the LOKF schema.
114+
115+
Works on any bundle directory. The schema is resolved from ``--schema`` if
116+
passed, otherwise from a ``lokf.yaml`` found in the current directory or an
117+
ancestor, otherwise from the copy shipped inside the installed ``lokf``
118+
package - so a bundle needs no schema file of its own to be validated.
119+
"""
120+
import os
121+
import subprocess
122+
import tempfile
123+
124+
from lokf.model import load_bundle
125+
from lokf.schema import schema_path
126+
127+
try:
128+
sch = schema_path(schema)
129+
except FileNotFoundError as exc:
130+
_err(str(exc))
131+
raise typer.Exit(1)
132+
133+
bundle = load_bundle(bundle_dir)
134+
doc = dict(bundle.meta)
135+
doc["concepts"] = bundle.docs()
136+
137+
fd, tmp = tempfile.mkstemp(suffix=".bundle.json")
138+
try:
139+
with os.fdopen(fd, "w", encoding="utf-8") as f:
140+
json.dump(doc, f)
141+
proc = subprocess.run(
142+
["linkml-validate", "-s", str(sch), "-C", "KnowledgeBundle", tmp]
143+
)
144+
finally:
145+
os.remove(tmp)
146+
if proc.returncode != 0:
147+
raise typer.Exit(proc.returncode)
148+
typer.echo(
149+
f"OK — {len(bundle.concepts)} concepts in {bundle_dir} validate "
150+
"against KnowledgeBundle."
151+
)
152+
153+
97154
# ---------------------------------------------------------------------------
98155
# tables
99156
# ---------------------------------------------------------------------------

src/lokf/schema.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ def _load_context(resolved: str) -> dict:
6767
return json.loads(pathlib.Path(resolved).read_text(encoding="utf-8"))["@context"]
6868

6969

70+
def schema_path(path: str | pathlib.Path | None = None) -> pathlib.Path:
71+
"""Resolve the LOKF schema file (``lokf.yaml``) to a filesystem path.
72+
73+
Resolution order (same as every other reader here): an explicit *path*, a
74+
checkout in an ancestor of the current directory, then the copy packaged
75+
with lokf. Downstream repos therefore validate against the packaged schema
76+
without needing their own ``lokf.yaml``.
77+
"""
78+
return _resolve(_SCHEMA_NAME, path)
79+
80+
7081
def load_schema(path: str | pathlib.Path | None = None) -> dict:
7182
"""Return the LOKF LinkML schema as a plain dict (cached per file)."""
7283
return _load_schema(str(_resolve(_SCHEMA_NAME, path)))

tests/test_cli.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ def test_convert_output_writes_file(tmp_path):
6565
assert "lokf:Metric" in text
6666

6767

68+
# -- validate ---------------------------------------------------------------
69+
def test_validate_reference_bundle_ok():
70+
"""validate <bundle> assembles and validates against KnowledgeBundle."""
71+
result = runner.invoke(app, ["validate", str(BUNDLE)])
72+
assert result.exit_code == 0
73+
assert "validate against KnowledgeBundle" in result.stdout
74+
75+
76+
def test_validate_missing_bundle_nonzero_exit():
77+
"""A non-existent bundle directory is rejected before validation."""
78+
result = runner.invoke(app, ["validate", "does-not-exist"])
79+
assert result.exit_code != 0
80+
81+
6882
# -- query ------------------------------------------------------------------
6983
def test_query_select_table_contains_wau():
7084
"""query <bundle> <SELECT> prints a table with the metric name."""

0 commit comments

Comments
 (0)