cassis-cli 1.5.0__tar.gz → 1.5.1__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.
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cassis-cli
3
- Version: 1.5.0
4
- Summary: Cassis CLI — run Cassis actions (ontology validation, upload and publish) from your CI pipelines
3
+ Version: 1.5.1
4
+ Summary: Validate, test and evaluate your Cassis ontology from your terminal, then publish it
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
7
7
  License-File: NOTICE
8
- Keywords: cassis,ontology,ci,text-to-sql
8
+ Keywords: cassis,ontology,cli,ci,text-to-sql
9
9
  Author: Cassis
10
10
  Author-email: tech.admin@getcassis.com
11
11
  Requires-Python: >=3.10,<4.0
@@ -22,13 +22,13 @@ Description-Content-Type: text/markdown
22
22
 
23
23
  # Cassis CLI
24
24
 
25
- Run Cassis actions from your CI pipelines:
25
+ Validate, test and evaluate your ontology from your terminal, then publish it. The same commands gate your pull requests in CI:
26
26
 
27
27
  - `cassis ontology check` validates the ontology files in your repository with the exact same checks as the Cassis GitHub PR check (YAML parsing, round-trip, import validation) — so you can gate merges in any CI system, not just GitHub. It then prints advisory **ontology quality warnings** for a tree that parsed — tables not assigned to any domain, joins/metrics pointing at unknown tables or columns, missing table/column descriptions (the same findings `ontology test` reports, without the agent run). In a checkout bound to a project (`project.yml`, `--project`, or `CASSIS_PROJECT_ID`), it also cross-checks the tree against the project's source schema: references to tables or columns the warehouse doesn't have print as **warnings** too — advisory only (the object may simply not be built or synced yet). Warnings never fail the check.
28
28
  - `cassis schema pull` downloads the data source's full source schema (as Cassis last introspected it) into `<base-path>/.schema.json` — a **gitignored** local snapshot (the command maintains the ignore entry) with a `pulled_at` stamp. The warehouse stays authoritative; the snapshot is a cache for offline/bulk work — e.g. a coding agent grepping table and column names during a modeling pass instead of paging through the MCP `get_source_schema` tool. Re-run to refresh.
29
29
  - `cassis ontology fmt` rewrites the ontology files in canonical form (think `black`/`gofmt` for the ontology), so hand or agent edits pass the round-trip check.
30
30
  - `cassis ontology upload` uploads the ontology files to a Cassis project (full replace) and, by default, publishes them immediately as a new version — so a merge to your main branch can go live in one CI step.
31
- - `cassis ontology pull` downloads the project's unpublished ontology into your repository checkout (full sync — stale local ontology files are pruned), so you can start editing from the current state, or bootstrap a repo that isn't git-synced (e.g. Bitbucket).
31
+ - `cassis ontology pull` downloads the project's unpublished ontology into your repository checkout (full sync — stale local ontology files are pruned), so you can start editing from the current state, or bootstrap a repo that isn't git-synced (e.g. Bitbucket). Pruning only deletes files that are tracked and unmodified in git (i.e. restorable with `git checkout`); untracked or locally modified files are kept and listed, and every deleted path is printed.
32
32
  - `cassis ontology pull` and `cassis ontology fmt` also write `<base-path>/AGENTS.md`, the Cassis ontology modeling guide, into the checkout (default `cassis/AGENTS.md`) — a managed file (generated banner; the CLI overwrites local edits) so a repo-aware coding agent loads current Cassis modeling doctrine by convention. It sits inside the ontology directory but is not part of the ontology tree (which is the YAML files plus the domain Markdown files `domains/**/README.md`), so it is never uploaded, validated, or pruned. Commit it alongside your ontology changes. The guide text ships inside the CLI package, so its version tracks the **installed cassis-cli version** — upgrade the CLI (`pip install -U cassis-cli`) and re-run `fmt` to pick up doctrine updates; an unpinned `pip install cassis-cli` in CI gets them automatically. The banner stamps a doctrine version, and the CLI never *downgrades* the file: if the checkout's `AGENTS.md` was written by a newer doctrine (a newer CLI, or Cassis itself on a publish), `fmt`/`pull` leave it in place, print an upgrade notice, and `fmt --check` still passes.
33
33
  - The CLI identifies itself to the API (`User-Agent: cassis-cli/<version>`), and successful API responses advertise the newest published version — when you are behind, commands print a one-line upgrade notice on stderr (purely informational; output and exit codes are unchanged).
34
34
  - `cassis eval run` runs the project's eval suite against your local ontology files (scored in-memory — nothing is pushed to Cassis) and prints per-question results, so you can test the changes on your git branch before merging.
@@ -73,7 +73,8 @@ cassis ontology check
73
73
  cassis ontology check /path/to/checkout
74
74
 
75
75
  # Download the project's unpublished ontology into the checkout (full sync;
76
- # review with git diff — pass --no-prune to keep local files it would delete):
76
+ # review with git diff — untracked/modified files are never deleted, and
77
+ # --no-prune keeps even the tracked stale files it would otherwise delete):
77
78
  cassis ontology pull --project 019f0000-0000-7000-8000-000000000000
78
79
 
79
80
  # Upload the ontology to a project and publish it immediately:
@@ -193,9 +194,9 @@ cassis ontology fmt --check
193
194
  | 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, out of credits, or `--timeout` reached |
194
195
 
195
196
  Commands that send the local tree (`check`, `fmt`, `upload`, `eval run`, `test`) accept up to
196
- 2000 ontology files / 5 MB total — far above real ontologies (a few hundred small
197
- files). Beyond that the CLI fails fast with exit 2 before uploading anything;
198
- double-check `--base-path` if you hit it.
197
+ 20,000 ontology files / 100 MB total (path + content bytes) — sized for ontologies of
198
+ roughly 10,000 modeled tables. Beyond that the CLI fails fast with exit 2 before
199
+ uploading anything; double-check `--base-path` if you hit it.
199
200
 
200
201
  `upload` replaces the project's entire ontology with the uploaded tree. A
201
202
  never-published project always goes live immediately on first upload (even
@@ -1,12 +1,12 @@
1
1
  # Cassis CLI
2
2
 
3
- Run Cassis actions from your CI pipelines:
3
+ Validate, test and evaluate your ontology from your terminal, then publish it. The same commands gate your pull requests in CI:
4
4
 
5
5
  - `cassis ontology check` validates the ontology files in your repository with the exact same checks as the Cassis GitHub PR check (YAML parsing, round-trip, import validation) — so you can gate merges in any CI system, not just GitHub. It then prints advisory **ontology quality warnings** for a tree that parsed — tables not assigned to any domain, joins/metrics pointing at unknown tables or columns, missing table/column descriptions (the same findings `ontology test` reports, without the agent run). In a checkout bound to a project (`project.yml`, `--project`, or `CASSIS_PROJECT_ID`), it also cross-checks the tree against the project's source schema: references to tables or columns the warehouse doesn't have print as **warnings** too — advisory only (the object may simply not be built or synced yet). Warnings never fail the check.
6
6
  - `cassis schema pull` downloads the data source's full source schema (as Cassis last introspected it) into `<base-path>/.schema.json` — a **gitignored** local snapshot (the command maintains the ignore entry) with a `pulled_at` stamp. The warehouse stays authoritative; the snapshot is a cache for offline/bulk work — e.g. a coding agent grepping table and column names during a modeling pass instead of paging through the MCP `get_source_schema` tool. Re-run to refresh.
7
7
  - `cassis ontology fmt` rewrites the ontology files in canonical form (think `black`/`gofmt` for the ontology), so hand or agent edits pass the round-trip check.
8
8
  - `cassis ontology upload` uploads the ontology files to a Cassis project (full replace) and, by default, publishes them immediately as a new version — so a merge to your main branch can go live in one CI step.
9
- - `cassis ontology pull` downloads the project's unpublished ontology into your repository checkout (full sync — stale local ontology files are pruned), so you can start editing from the current state, or bootstrap a repo that isn't git-synced (e.g. Bitbucket).
9
+ - `cassis ontology pull` downloads the project's unpublished ontology into your repository checkout (full sync — stale local ontology files are pruned), so you can start editing from the current state, or bootstrap a repo that isn't git-synced (e.g. Bitbucket). Pruning only deletes files that are tracked and unmodified in git (i.e. restorable with `git checkout`); untracked or locally modified files are kept and listed, and every deleted path is printed.
10
10
  - `cassis ontology pull` and `cassis ontology fmt` also write `<base-path>/AGENTS.md`, the Cassis ontology modeling guide, into the checkout (default `cassis/AGENTS.md`) — a managed file (generated banner; the CLI overwrites local edits) so a repo-aware coding agent loads current Cassis modeling doctrine by convention. It sits inside the ontology directory but is not part of the ontology tree (which is the YAML files plus the domain Markdown files `domains/**/README.md`), so it is never uploaded, validated, or pruned. Commit it alongside your ontology changes. The guide text ships inside the CLI package, so its version tracks the **installed cassis-cli version** — upgrade the CLI (`pip install -U cassis-cli`) and re-run `fmt` to pick up doctrine updates; an unpinned `pip install cassis-cli` in CI gets them automatically. The banner stamps a doctrine version, and the CLI never *downgrades* the file: if the checkout's `AGENTS.md` was written by a newer doctrine (a newer CLI, or Cassis itself on a publish), `fmt`/`pull` leave it in place, print an upgrade notice, and `fmt --check` still passes.
11
11
  - The CLI identifies itself to the API (`User-Agent: cassis-cli/<version>`), and successful API responses advertise the newest published version — when you are behind, commands print a one-line upgrade notice on stderr (purely informational; output and exit codes are unchanged).
12
12
  - `cassis eval run` runs the project's eval suite against your local ontology files (scored in-memory — nothing is pushed to Cassis) and prints per-question results, so you can test the changes on your git branch before merging.
@@ -51,7 +51,8 @@ cassis ontology check
51
51
  cassis ontology check /path/to/checkout
52
52
 
53
53
  # Download the project's unpublished ontology into the checkout (full sync;
54
- # review with git diff — pass --no-prune to keep local files it would delete):
54
+ # review with git diff — untracked/modified files are never deleted, and
55
+ # --no-prune keeps even the tracked stale files it would otherwise delete):
55
56
  cassis ontology pull --project 019f0000-0000-7000-8000-000000000000
56
57
 
57
58
  # Upload the ontology to a project and publish it immediately:
@@ -171,9 +172,9 @@ cassis ontology fmt --check
171
172
  | 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, out of credits, or `--timeout` reached |
172
173
 
173
174
  Commands that send the local tree (`check`, `fmt`, `upload`, `eval run`, `test`) accept up to
174
- 2000 ontology files / 5 MB total — far above real ontologies (a few hundred small
175
- files). Beyond that the CLI fails fast with exit 2 before uploading anything;
176
- double-check `--base-path` if you hit it.
175
+ 20,000 ontology files / 100 MB total (path + content bytes) — sized for ontologies of
176
+ roughly 10,000 modeled tables. Beyond that the CLI fails fast with exit 2 before
177
+ uploading anything; double-check `--base-path` if you hit it.
177
178
 
178
179
  `upload` replaces the project's entire ontology with the uploaded tree. A
179
180
  never-published project always goes live immediately on first upload (even
@@ -1,4 +1,7 @@
1
- """Cassis CLI — run Cassis actions from your CI pipelines."""
1
+ """Validate, test and evaluate your ontology from your terminal, then publish it.
2
+
3
+ The same commands gate your pull requests in CI.
4
+ """
2
5
 
3
6
  from importlib.metadata import PackageNotFoundError, version
4
7
 
@@ -2,7 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
5
6
  import re
7
+ import subprocess
6
8
  from pathlib import Path
7
9
  from typing import Optional
8
10
  from uuid import UUID
@@ -20,10 +22,12 @@ EXIT_USAGE = 2
20
22
  EXIT_TRANSPORT = 3
21
23
 
22
24
  # Request ceilings of the /api/ci file-tree endpoints, mirrored so oversized
23
- # trees fail fast with a clear message before any upload. Source of truth:
25
+ # trees fail fast with a clear message before any upload. Sized for ~10,000
26
+ # modeled tables (a 6,000-table tree is ~8,600 files / ~60 MB). Source of
27
+ # truth: backend/app/services/ontology_check.py, enforced by
24
28
  # backend/app/schemas/ci.py (the server's 422 remains the backstop).
25
- MAX_FILES = 2000
26
- MAX_TOTAL_BYTES = 5 * 1024 * 1024
29
+ MAX_FILES = 20_000
30
+ MAX_TOTAL_BYTES = 100 * 1024 * 1024
27
31
 
28
32
 
29
33
  def is_ontology_file(rel_path: str) -> bool:
@@ -41,6 +45,42 @@ def is_ontology_file(rel_path: str) -> bool:
41
45
  return rel_path.startswith("domains/") and rel_path.endswith("/README.md")
42
46
 
43
47
 
48
+ def git_file_states(directory: Path) -> Optional[tuple[set[str], set[str]]]:
49
+ """(tracked, dirty) path sets for files under ``directory``, relative to it.
50
+
51
+ ``tracked`` is every git-tracked file below the directory; ``dirty`` the
52
+ subset whose working-tree content differs from the index (modified or
53
+ missing). Returns ``None`` when the directory is not inside a git work tree
54
+ or git is unavailable — callers must then treat every file as
55
+ unrecoverable and refuse to delete it.
56
+ """
57
+ # GIT_OPTIONAL_LOCKS=0: read-only queries must not take the index lock
58
+ # (and fail) when another git process is running.
59
+ env = {**os.environ, "GIT_OPTIONAL_LOCKS": "0"}
60
+ try:
61
+ tracked_proc = subprocess.run(
62
+ ["git", "ls-files", "-z"],
63
+ cwd=directory,
64
+ env=env,
65
+ capture_output=True,
66
+ check=True,
67
+ text=True,
68
+ )
69
+ dirty_proc = subprocess.run(
70
+ ["git", "ls-files", "-z", "--modified"],
71
+ cwd=directory,
72
+ env=env,
73
+ capture_output=True,
74
+ check=True,
75
+ text=True,
76
+ )
77
+ except (OSError, subprocess.CalledProcessError, UnicodeDecodeError):
78
+ return None
79
+ tracked = {p for p in tracked_proc.stdout.split("\0") if p}
80
+ dirty = {p for p in dirty_proc.stdout.split("\0") if p}
81
+ return tracked, dirty
82
+
83
+
44
84
  def is_legacy_domain_file(rel_path: str) -> bool:
45
85
  """Whether a base-relative path is a legacy (pre-Markdown) domain file.
46
86
 
@@ -84,7 +124,7 @@ def read_project_id_from_dir(ontology_dir: Path) -> Optional[str]:
84
124
  """Return the ``project_id`` recorded in ``<ontology_dir>/project.yml``, or None."""
85
125
  try:
86
126
  text = (ontology_dir / "project.yml").read_text(encoding="utf-8")
87
- except (OSError, UnicodeDecodeError) as _exc: # `as` keeps black from stripping the parens (3.14-only syntax)
127
+ except (OSError, UnicodeDecodeError):
88
128
  return None
89
129
  for line in text.splitlines():
90
130
  match = _PROJECT_ID_LINE.match(line.strip())
@@ -171,7 +211,9 @@ def collect_tree(path: Path, base_path: str) -> "tuple[dict[str, str], str]":
171
211
  typer.secho(f"No ontology files found under {ontology_dir}.", fg=typer.colors.RED, err=True)
172
212
  raise typer.Exit(EXIT_USAGE)
173
213
 
174
- total_bytes = sum(len(content.encode()) for content in files.values())
214
+ # Count path bytes too, exactly like the server's _validate_tree_files —
215
+ # a tree accepted here must never come back as a server-side 422.
216
+ total_bytes = sum(len(rel.encode()) + len(content.encode()) for rel, content in files.items())
175
217
  if len(files) > MAX_FILES or total_bytes > MAX_TOTAL_BYTES:
176
218
  typer.secho(
177
219
  f"Ontology tree too large: {len(files)} files / {total_bytes / (1024 * 1024):.1f} MB "
@@ -14,7 +14,10 @@ from cassis_cli.verify import verify
14
14
 
15
15
  app = typer.Typer(
16
16
  no_args_is_help=True,
17
- help="Cassis CLI — run Cassis actions from your CI pipelines.",
17
+ help=(
18
+ "Cassis CLI: validate, test and evaluate your ontology from your terminal, "
19
+ "then publish it. The same commands gate your pull requests in CI."
20
+ ),
18
21
  )
19
22
  app.add_typer(ontology_app, name="ontology")
20
23
  app.add_typer(eval_app, name="eval")
@@ -28,6 +28,7 @@ from cassis_cli.common import (
28
28
  )
29
29
  from cassis_cli.common import collect_files as _collect_files
30
30
  from cassis_cli.common import collect_tree as _collect_tree
31
+ from cassis_cli.common import git_file_states as _git_file_states
31
32
  from cassis_cli.common import is_legacy_domain_file as _is_legacy_domain_file
32
33
  from cassis_cli.common import require_api_key as _require_api_key
33
34
  from cassis_cli.common import resolve_project_id as _resolve_project_id
@@ -197,7 +198,11 @@ def pull(
197
198
  prune: bool = typer.Option(
198
199
  True,
199
200
  "--prune/--no-prune",
200
- help="Delete local ontology files that no longer exist in the project's ontology (default: prune).",
201
+ help=(
202
+ "Delete local ontology files that no longer exist in the project's ontology "
203
+ "(default: prune). Only files that are tracked and unmodified in git are "
204
+ "deleted; untracked or locally modified files are always kept and reported."
205
+ ),
201
206
  ),
202
207
  json_output: bool = typer.Option(False, "--json", help="Print a JSON summary of written/deleted files."),
203
208
  ) -> None:
@@ -205,7 +210,9 @@ def pull(
205
210
 
206
211
  Writes the ontology tree under the export path (full sync: files are
207
212
  overwritten and, unless --no-prune, stale local ontology files are deleted,
208
- so the checkout ends up matching the project exactly). Review the changes with
213
+ so the checkout ends up matching the project exactly). Pruning never touches
214
+ files git could not restore: untracked or locally modified files are kept
215
+ and listed, and every deleted path is named. Review the changes with
209
216
  git diff before committing. Exits 0 on success, 2 on usage errors, 3 on
210
217
  transport/API errors.
211
218
  """
@@ -243,15 +250,38 @@ def pull(
243
250
  written.append(rel)
244
251
 
245
252
  deleted: list[str] = []
253
+ kept: list[dict[str, str]] = []
246
254
  if prune and ontology_dir.is_dir():
247
255
  local = _collect_files(ontology_dir)
248
- for rel in sorted(set(local) - set(files)):
249
- try:
250
- (ontology_dir / rel).unlink()
251
- except OSError as exc:
252
- typer.secho(f"Cannot delete {ontology_dir / rel}: {exc}", fg=typer.colors.RED, err=True)
253
- raise typer.Exit(EXIT_USAGE) from exc
254
- deleted.append(rel)
256
+ stale = sorted(set(local) - set(files))
257
+ if stale:
258
+ # Only delete what git can restore. An untracked or locally
259
+ # modified file is user work Cassis has never seen — pruning it
260
+ # would be unrecoverable data loss (#27).
261
+ states = _git_file_states(ontology_dir)
262
+ to_delete: list[str] = []
263
+ if states is None:
264
+ kept = [{"path": rel, "reason": "not in a git repository"} for rel in stale]
265
+ else:
266
+ tracked, dirty = states
267
+ for rel in stale:
268
+ if rel not in tracked:
269
+ kept.append({"path": rel, "reason": "untracked in git"})
270
+ elif rel in dirty:
271
+ kept.append({"path": rel, "reason": "locally modified"})
272
+ else:
273
+ to_delete.append(rel)
274
+ if to_delete and not json_output:
275
+ typer.echo(f"Deleting {len(to_delete)} stale ontology file(s):")
276
+ for rel in to_delete:
277
+ typer.echo(f" {base_path}/{rel}")
278
+ for rel in to_delete:
279
+ try:
280
+ (ontology_dir / rel).unlink()
281
+ except OSError as exc:
282
+ typer.secho(f"Cannot delete {ontology_dir / rel}: {exc}", fg=typer.colors.RED, err=True)
283
+ raise typer.Exit(EXIT_USAGE) from exc
284
+ deleted.append(rel)
255
285
 
256
286
  # Managed modeling guide: refresh AGENTS.md so a repo-aware agent loads
257
287
  # current Cassis doctrine. Not part of the ontology tree (YAML-only), so it
@@ -266,14 +296,31 @@ def pull(
266
296
  _warn_newer_guide(base_path)
267
297
 
268
298
  if json_output:
269
- typer.echo(json.dumps({"written": written, "deleted": deleted, "guide_written": guide_written}, indent=2))
299
+ typer.echo(
300
+ json.dumps(
301
+ {"written": written, "deleted": deleted, "kept": kept, "guide_written": guide_written},
302
+ indent=2,
303
+ )
304
+ )
270
305
  else:
271
306
  summary = f"✓ Pulled {len(written)} files into {ontology_dir}"
272
307
  if deleted:
273
- summary += f" ({len(deleted)} stale files deleted)"
308
+ summary += f" ({len(deleted)} stale files deleted, listed above)"
274
309
  if guide_written:
275
310
  summary += f"; wrote {base_path}/{GUIDE_FILENAME}"
276
311
  typer.secho(f"{summary}.", fg=typer.colors.GREEN)
312
+ if kept:
313
+ typer.secho(
314
+ f"Kept {len(kept)} local file(s) not in the project ontology "
315
+ "(only files tracked and unmodified in git are pruned):",
316
+ fg=typer.colors.YELLOW,
317
+ )
318
+ for entry in kept:
319
+ typer.secho(f" {base_path}/{entry['path']} ({entry['reason']})", fg=typer.colors.YELLOW)
320
+ typer.secho(
321
+ " Commit them if they are intentional, or delete them manually.",
322
+ fg=typer.colors.YELLOW,
323
+ )
277
324
  migrated = sum(1 for rel in deleted if _is_legacy_domain_file(rel))
278
325
  if migrated:
279
326
  typer.secho(
@@ -134,7 +134,7 @@ def ensure_gitignored(ontology_dir: Path) -> None:
134
134
  gitignore = ontology_dir / ".gitignore"
135
135
  try:
136
136
  existing = gitignore.read_text(encoding="utf-8")
137
- except (OSError, UnicodeDecodeError) as _exc: # `as` keeps black from stripping the parens (3.14-only syntax)
137
+ except (OSError, UnicodeDecodeError):
138
138
  existing = ""
139
139
  if SNAPSHOT_FILENAME in existing.splitlines():
140
140
  return
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "cassis-cli"
3
- version = "1.5.0"
4
- description = "Cassis CLI — run Cassis actions (ontology validation, upload and publish) from your CI pipelines"
3
+ version = "1.5.1"
4
+ description = "Validate, test and evaluate your Cassis ontology from your terminal, then publish it"
5
5
  readme = "README.md"
6
6
  license = { text = "Apache-2.0" }
7
7
  authors = [{ name = "Cassis", email = "tech.admin@getcassis.com" }]
8
- keywords = ["cassis", "ontology", "ci", "text-to-sql"]
8
+ keywords = ["cassis", "ontology", "cli", "ci", "text-to-sql"]
9
9
  classifiers = [
10
10
  "License :: OSI Approved :: Apache Software License",
11
11
  "Environment :: Console",
@@ -43,3 +43,10 @@ build-backend = "poetry.core.masonry.api"
43
43
 
44
44
  [tool.pytest.ini_options]
45
45
  testpaths = ["tests"]
46
+
47
+ [tool.black]
48
+ # The package ships for Python >=3.10 (see requires-python above): formatting
49
+ # must target the floor, or black under the monorepo's 3.14 rewrites syntax
50
+ # into 3.14-only forms (e.g. stripping parens from multi-exception `except`).
51
+ line-length = 120
52
+ target-version = ["py310"]
File without changes
File without changes
File without changes