cassis-cli 3.2.0__tar.gz → 3.4.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.
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/PKG-INFO +32 -11
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/README.md +31 -10
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/cassis_cli/api.py +189 -2
- cassis_cli-3.4.0/cassis_cli/dbt.py +700 -0
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/cassis_cli/eval.py +458 -25
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/cassis_cli/guide.py +1 -1
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/cassis_cli/issues.py +73 -10
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/cassis_cli/main.py +2 -0
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/cassis_cli/ontology.py +20 -6
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/cassis_cli/ontology_design_guide.md +15 -3
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/cassis_cli/schema.py +1 -1
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/cassis_cli/status.py +1 -1
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/cassis_cli/verify.py +1 -0
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/pyproject.toml +1 -1
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/LICENSE +0 -0
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/NOTICE +0 -0
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/cassis_cli/__init__.py +0 -0
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/cassis_cli/common.py +0 -0
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/cassis_cli/projects.py +0 -0
- {cassis_cli-3.2.0 → cassis_cli-3.4.0}/cassis_cli/schema_plan.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cassis-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: Validate, test and evaluate your Cassis context from your terminal, then publish it
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -33,14 +33,17 @@ Validate, test and evaluate your context from your terminal, then publish it. Th
|
|
|
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 context 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.
|
|
35
35
|
- `cassis context test` runs individual questions through the text-to-SQL agent using your local context files, so you can check that a change actually works (e.g. a new column gets picked), where `eval run` only checks for regressions on existing eval cases.
|
|
36
|
-
- `cassis eval add-case` adds a gold question/SQL case to the project's eval suite: after fixing a context issue, add the question users were failing on so `eval run` guards it from regressing.
|
|
37
|
-
- `cassis eval
|
|
36
|
+
- `cassis eval add-case` adds a gold question/SQL case to the project's eval suite: after fixing a context issue, add the question users were failing on so `eval run` guards it from regressing. Cassis then runs the new case three times in the background, and `eval list-cases` flags it if the attempts fail or disagree.
|
|
37
|
+
- `cassis eval run --repeat N` runs every case N times (2 to 5) on the same context and model and compares the attempts per case: which cases flipped, and whether a flip came from the agent writing different SQL or the judge ruling differently on the same SQL. `cassis eval compare <run-id>` prints that comparison again. A repeat run measures noise: it opens no issue, is not the run shown before publishing, and exits 1 only when a case failed every attempt.
|
|
38
|
+
- `cassis eval list-cases`, `cassis eval history` and `cassis eval delete-case` maintain the suite: list the current cases with their ids (flagging those whose gold SQL reads tables the context lacks), see how one case has behaved across runs (pass rate, whether repeated runs under one configuration disagree, divergences and timings), and prune one that is stale or wrong (e.g. its gold SQL encodes a definition the context has since changed).
|
|
38
39
|
- `cassis schema plan <ddl>` (or `--warehouse` on a project connected to a warehouse) previews what a schema update would change before anything is applied: the source schema diff, the context changes Cassis will make (every change on a table placed in the context, with everything a drop takes with it) and warnings, terraform-style. `cassis schema apply <ddl>` (or `--plan <id>`) writes the resulting context files into the local checkout, app untouched, for review with `git diff`. `cassis schema push <ddl> [--publish]` pushes the new schema and the local context to the app (`--yes` in CI). The file speaks only for the schemas it contains: pass `--complete` when it is the project's complete source schema so schemas absent from it are treated as dropped. With `--warehouse` the server introspects the connected warehouse instead of parsing a file; the plan is always whole-source. `cassis schema plan <ddl> --dry-run` is the prepare-ahead variant: the plan is computed synchronously and nothing is kept in Cassis (no plan to apply or resume, the current plan untouched), so the schema snapshot for a change still in a PR can be planned against safely; `--write-checkout` writes the context files it would produce into the checkout, to commit alongside the schema change.
|
|
39
40
|
- `cassis projects list` lists the projects your API key can reach: id (what `--project` and `CASSIS_PROJECT_ID` take), name, published context version, and data-source dialect. A pipeline or agent can discover the project id from the terminal instead of fishing it out of a webapp URL.
|
|
40
41
|
- DDL imports describe one schema snapshot/export file, including ordinary and materialized views; they do not replay incremental migrations. Extraction diagnostics include object names and statement locations. If extraction is incomplete, `schema plan` and `--dry-run` show the extracted inventory and exit 1; `apply`, `push`, and `--write-checkout` cannot save that result. Unknown column types are warnings when all output names are known. Plans also show object-kind, view-definition, comment, and constraint changes. `--json` preserves structured diagnostics and the server-capped inventory.
|
|
41
42
|
|
|
42
43
|
- `cassis status` shows the project's published version (number, label, git commit), whether unpublished changes await publication, the git-sync binding, a schema plan waiting to be applied, and how your local git HEAD relates to the published commit (in sync / N commits ahead / diverged). `cassis status --watch` polls until the published commit matches your local HEAD (e.g. right after merging a PR whose CI publishes the context) instead of watching the GitHub Actions tab.
|
|
43
|
-
- `cassis issues` triages the issues Cassis raised on the project (what it found wrong while answering questions: a context gap, missing data) without leaving the checkout: `issues list` (filterable by status, impact, cause and context domain, and showing each issue's domain so you can work through one domain at a time), `issues show <id>` for the diagnosis, suggested action
|
|
44
|
+
- `cassis issues` triages the issues Cassis raised on the project (what it found wrong while answering questions: a context gap, missing data) without leaving the checkout: `issues list` (filterable by status, impact, cause and context domain, and showing each issue's domain so you can work through one domain at a time; and each issue's reach, the chat answers that used its context tables in the last 30 days), `issues show <id>` for the diagnosis, suggested action, the occurrences behind it, its reach and evidence (removed from dbt, an AI review suggestion, user feedback, a failed eval, or the chat analysis), `issues evidence <id> <occurrence-id>` for what the agent actually saw, and `issues resolve` / `dismiss` / `reopen` once you've acted on it. When the fix ships through a pull request, write the `PR mention:` line `issues show` prints (`Resolves <id>`) in the PR description instead: Cassis resolves the issue when the PR merges, and `issues show` then reports how it was closed and through which PR.
|
|
45
|
+
- `cassis dbt push` sends your dbt project's artifacts (`target/manifest.json`, and `catalog.json` when present) to Cassis from the dbt project's CI, with the commit they were built from. Cassis checks the files, matches each dbt model, seed, snapshot and source to a table of the project's data source, and keeps the upload as the project's dbt baseline unless a newer one is already there. The command prints how many relations matched and how many tests they declare. Cassis compares the upload with the previous baseline and files each part of the context the merged change puts in doubt as an issue (impact "at risk"); the command lists them with links. Nothing the agent answers with changes.
|
|
46
|
+
- `cassis dbt check` runs the same comparison on a pull request, before the change merges: it uploads the PR's artifacts (after `dbt compile` on the production target), compares them with the baseline, and prints the context tables, columns, joins and metrics the change puts in doubt, with why it matters and the context change to make. Each finding also says its **reach**, how many chat answers used its context tables in the last 30 days, and within each section the most-reached objects print first, the order the Review page will show them in. Findings are advisory: nothing is filed, the baseline doesn't move, and the check exits 0 with or without findings.
|
|
44
47
|
- `cassis verify` runs the full local gate in one verb (`context fmt --check`, `context check`, `eval run`), stopping at the first failure. One command in a checkout ("is this change safe to merge?"), one job in CI. `--no-eval` skips the eval suite.
|
|
45
48
|
|
|
46
49
|
## Install
|
|
@@ -63,7 +66,7 @@ The context tree under `<base-path>` (default `cassis/`) is:
|
|
|
63
66
|
|
|
64
67
|
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
65
68
|
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
66
|
-
3. For `pull`, `upload`, `schema pull`, `eval run`, `context test`, the `eval` case commands (`add-case`, `list-cases`, `delete-case`), and the `issues` commands: the project ID (UUID) is taken from `<base-path>/project.yml` in the checkout (written by `pull` and by publishing), so once a repo is pulled you don't need to pass it. To override, or before the first pull, set `CASSIS_PROJECT_ID` or pass `--project` (find the UUID with `cassis projects list`, or in the project's URL). `context check` uses the same resolution but treats it as optional: unbound checkouts get the project-less validation (no schema reference warnings).
|
|
69
|
+
3. For `pull`, `upload`, `schema pull`, `eval run`, `context test`, the `eval` case commands (`add-case`, `list-cases`, `history`, `delete-case`), and the `issues` commands: the project ID (UUID) is taken from `<base-path>/project.yml` in the checkout (written by `pull` and by publishing), so once a repo is pulled you don't need to pass it. To override, or before the first pull, set `CASSIS_PROJECT_ID` or pass `--project` (find the UUID with `cassis projects list`, or in the project's URL). `context check` uses the same resolution but treats it as optional: unbound checkouts get the project-less validation (no schema reference warnings).
|
|
67
70
|
|
|
68
71
|
## Usage
|
|
69
72
|
|
|
@@ -131,10 +134,18 @@ cassis eval add-case --project ... -q "How much was refunded last month?" \
|
|
|
131
134
|
cassis eval add-case --project ... -q "How much was refunded last month?" \
|
|
132
135
|
--gold-sql-file refunds.sql
|
|
133
136
|
|
|
134
|
-
# List the suite's cases (id + question
|
|
137
|
+
# List the suite's cases (id + question, a warning under any case whose gold SQL
|
|
138
|
+
# reads tables the context lacks; --json adds the gold SQL and the footprint;
|
|
139
|
+
# --branch <name> resolves against a context branch), see how one has behaved
|
|
140
|
+
# across runs, then prune one:
|
|
135
141
|
cassis eval list-cases --project ...
|
|
142
|
+
cassis eval history 019f0000-0000-7000-8000-0000000000ca --project ...
|
|
136
143
|
cassis eval delete-case 019f0000-0000-7000-8000-0000000000ca --project ...
|
|
137
144
|
|
|
145
|
+
# Run the suite 3 times to see which cases flip, and why; print it again later:
|
|
146
|
+
cassis eval run --repeat 3
|
|
147
|
+
cassis eval compare 019f0000-0000-7000-8000-00000000aaaa --project ...
|
|
148
|
+
|
|
138
149
|
# Pull the source schema into <base-path>/.schema.json (gitignored local snapshot):
|
|
139
150
|
cassis schema pull
|
|
140
151
|
|
|
@@ -175,6 +186,16 @@ cassis issues reopen 019f0000-0000-7000-8000-0000000000e1
|
|
|
175
186
|
cassis status
|
|
176
187
|
cassis status --watch --timeout 600
|
|
177
188
|
|
|
189
|
+
# In the dbt project's CI, after a merge to the production branch: write the manifest and the
|
|
190
|
+
# catalog, then send them (findings are filed as issues). On GitHub Actions, expose GITHUB_TOKEN to
|
|
191
|
+
# the step so the push names the pull request that merged the commit:
|
|
192
|
+
dbt docs generate --target prod
|
|
193
|
+
cassis dbt push target --project ...
|
|
194
|
+
|
|
195
|
+
# On a dbt pull request: compile on the production target, then list what the change puts in doubt:
|
|
196
|
+
dbt compile --target prod
|
|
197
|
+
cassis dbt check target --project ...
|
|
198
|
+
|
|
178
199
|
# The full local gate in one verb (fmt --check, check, eval run; stops at the first failure):
|
|
179
200
|
cassis verify
|
|
180
201
|
cassis verify --no-eval
|
|
@@ -187,7 +208,7 @@ Configuration (flags take precedence over env vars):
|
|
|
187
208
|
| `--api-key` | `CASSIS_API_KEY` | none (required) |
|
|
188
209
|
| `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
|
|
189
210
|
| `--base-path` | `CASSIS_BASE_PATH` | `cassis`, must match the project's git-sync "Path" setting |
|
|
190
|
-
| `--project` (check, pull, upload, schema pull, eval run, eval add-case, eval list-cases, eval delete-case, test, issues) | `CASSIS_PROJECT_ID` | the id in `<base-path>/project.yml` (required before the first pull; `check` alone falls back to the project-less validation when unbound) |
|
|
211
|
+
| `--project` (check, pull, upload, schema pull, eval run, eval add-case, eval list-cases, eval history, eval compare, eval delete-case, test, issues, dbt push, dbt check) | `CASSIS_PROJECT_ID` | the id in `<base-path>/project.yml` (required before the first pull; `check` alone falls back to the project-less validation when unbound) |
|
|
191
212
|
|
|
192
213
|
`cassis eval run` also accepts `--case <id>` (repeatable; run only the named
|
|
193
214
|
cases, ids from `eval list-cases` or `add-case`), `--label` (run label in the Evals page; defaults
|
|
@@ -216,10 +237,10 @@ cassis context fmt --check
|
|
|
216
237
|
|
|
217
238
|
| Code | Meaning |
|
|
218
239
|
| ---- | ------------------------------------------------------------------------------ |
|
|
219
|
-
| 0 | Context is valid (check) / pulled (pull) / uploaded (upload) / eval run completed all-passed (eval run) / every probe completed (test, whatever its outcome; probes are informational, don't gate CI on them) |
|
|
220
|
-
| 1 | Validation failed (check: findings printed; upload: nothing imported; eval run: invalid tree, failed cases, or failed/cancelled run; test: invalid tree or a probe failed; add-case: duplicate question or gold SQL that does not run; delete-case: no such case in the project; issues: no such issue or occurrence in the project; issues analyze: failed or cancelled analysis run; schema plan/apply: extraction is incomplete, the plan failed (unparseable or truncated DDL), is stale or expired, the apply failed, or the project won't accept it (a plan is being applied, a DDL was given for a warehouse-connected project, or --warehouse for a DDL-only one)) |
|
|
221
|
-
| 2 | Usage error (missing API key or project, no context directory, unreadable file, tree over the size limits, `eval run --branch` naming a context branch the project does not have, `upload` or `schema push` outside a git checkout or with uncommitted context files) |
|
|
222
|
-
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another eval run or issue analysis already active, out of credits,
|
|
240
|
+
| 0 | Context is valid (check) / pulled (pull) / uploaded (upload) / eval run completed all-passed (eval run) / every probe completed (test, whatever its outcome; probes are informational, don't gate CI on them) / dbt upload accepted, or kept behind a newer one (dbt push) / dbt upload checked, with or without findings (dbt check) |
|
|
241
|
+
| 1 | Validation failed (check: findings printed; upload: nothing imported; eval run: invalid tree, failed cases, or failed/cancelled run; test: invalid tree or a probe failed; add-case: duplicate question or gold SQL that does not run; delete-case, history: no such case in the project; issues: no such issue or occurrence in the project; issues analyze: failed or cancelled analysis run; schema plan/apply: extraction is incomplete, the plan failed (unparseable or truncated DDL), is stale or expired, the apply failed, or the project won't accept it (a plan is being applied, a DDL was given for a warehouse-connected project, or --warehouse for a DDL-only one); dbt push: the upload was refused or rejected (a file is invalid, too large or from an unsupported dbt version, or no relation matches the project's data source); dbt check: the upload was refused or rejected (an invalid file, or no baseline yet: run `cassis dbt push` first)) |
|
|
242
|
+
| 2 | Usage error (missing API key or project, no context directory, unreadable file, tree over the size limits, `eval run --branch` naming a context branch the project does not have, `upload` or `schema push` outside a git checkout or with uncommitted context files, `dbt push` or `dbt check` without a manifest.json, outside a git checkout without `--commit`, or with an artifact over the size limits) |
|
|
243
|
+
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another eval run or issue analysis already active, out of credits, `--timeout` reached, or Cassis failed on its side while processing a dbt upload (`dbt push` / `dbt check`: nothing to fix in the upload, push again) |
|
|
223
244
|
|
|
224
245
|
Commands that send the local tree (`check`, `fmt`, `upload`, `eval run`, `test`) accept up to
|
|
225
246
|
20,000 context files / 100 MB total (path + content bytes), sized for a context of
|
|
@@ -11,14 +11,17 @@ Validate, test and evaluate your context from your terminal, then publish it. Th
|
|
|
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 context 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.
|
|
13
13
|
- `cassis context test` runs individual questions through the text-to-SQL agent using your local context files, so you can check that a change actually works (e.g. a new column gets picked), where `eval run` only checks for regressions on existing eval cases.
|
|
14
|
-
- `cassis eval add-case` adds a gold question/SQL case to the project's eval suite: after fixing a context issue, add the question users were failing on so `eval run` guards it from regressing.
|
|
15
|
-
- `cassis eval
|
|
14
|
+
- `cassis eval add-case` adds a gold question/SQL case to the project's eval suite: after fixing a context issue, add the question users were failing on so `eval run` guards it from regressing. Cassis then runs the new case three times in the background, and `eval list-cases` flags it if the attempts fail or disagree.
|
|
15
|
+
- `cassis eval run --repeat N` runs every case N times (2 to 5) on the same context and model and compares the attempts per case: which cases flipped, and whether a flip came from the agent writing different SQL or the judge ruling differently on the same SQL. `cassis eval compare <run-id>` prints that comparison again. A repeat run measures noise: it opens no issue, is not the run shown before publishing, and exits 1 only when a case failed every attempt.
|
|
16
|
+
- `cassis eval list-cases`, `cassis eval history` and `cassis eval delete-case` maintain the suite: list the current cases with their ids (flagging those whose gold SQL reads tables the context lacks), see how one case has behaved across runs (pass rate, whether repeated runs under one configuration disagree, divergences and timings), and prune one that is stale or wrong (e.g. its gold SQL encodes a definition the context has since changed).
|
|
16
17
|
- `cassis schema plan <ddl>` (or `--warehouse` on a project connected to a warehouse) previews what a schema update would change before anything is applied: the source schema diff, the context changes Cassis will make (every change on a table placed in the context, with everything a drop takes with it) and warnings, terraform-style. `cassis schema apply <ddl>` (or `--plan <id>`) writes the resulting context files into the local checkout, app untouched, for review with `git diff`. `cassis schema push <ddl> [--publish]` pushes the new schema and the local context to the app (`--yes` in CI). The file speaks only for the schemas it contains: pass `--complete` when it is the project's complete source schema so schemas absent from it are treated as dropped. With `--warehouse` the server introspects the connected warehouse instead of parsing a file; the plan is always whole-source. `cassis schema plan <ddl> --dry-run` is the prepare-ahead variant: the plan is computed synchronously and nothing is kept in Cassis (no plan to apply or resume, the current plan untouched), so the schema snapshot for a change still in a PR can be planned against safely; `--write-checkout` writes the context files it would produce into the checkout, to commit alongside the schema change.
|
|
17
18
|
- `cassis projects list` lists the projects your API key can reach: id (what `--project` and `CASSIS_PROJECT_ID` take), name, published context version, and data-source dialect. A pipeline or agent can discover the project id from the terminal instead of fishing it out of a webapp URL.
|
|
18
19
|
- DDL imports describe one schema snapshot/export file, including ordinary and materialized views; they do not replay incremental migrations. Extraction diagnostics include object names and statement locations. If extraction is incomplete, `schema plan` and `--dry-run` show the extracted inventory and exit 1; `apply`, `push`, and `--write-checkout` cannot save that result. Unknown column types are warnings when all output names are known. Plans also show object-kind, view-definition, comment, and constraint changes. `--json` preserves structured diagnostics and the server-capped inventory.
|
|
19
20
|
|
|
20
21
|
- `cassis status` shows the project's published version (number, label, git commit), whether unpublished changes await publication, the git-sync binding, a schema plan waiting to be applied, and how your local git HEAD relates to the published commit (in sync / N commits ahead / diverged). `cassis status --watch` polls until the published commit matches your local HEAD (e.g. right after merging a PR whose CI publishes the context) instead of watching the GitHub Actions tab.
|
|
21
|
-
- `cassis issues` triages the issues Cassis raised on the project (what it found wrong while answering questions: a context gap, missing data) without leaving the checkout: `issues list` (filterable by status, impact, cause and context domain, and showing each issue's domain so you can work through one domain at a time), `issues show <id>` for the diagnosis, suggested action
|
|
22
|
+
- `cassis issues` triages the issues Cassis raised on the project (what it found wrong while answering questions: a context gap, missing data) without leaving the checkout: `issues list` (filterable by status, impact, cause and context domain, and showing each issue's domain so you can work through one domain at a time; and each issue's reach, the chat answers that used its context tables in the last 30 days), `issues show <id>` for the diagnosis, suggested action, the occurrences behind it, its reach and evidence (removed from dbt, an AI review suggestion, user feedback, a failed eval, or the chat analysis), `issues evidence <id> <occurrence-id>` for what the agent actually saw, and `issues resolve` / `dismiss` / `reopen` once you've acted on it. When the fix ships through a pull request, write the `PR mention:` line `issues show` prints (`Resolves <id>`) in the PR description instead: Cassis resolves the issue when the PR merges, and `issues show` then reports how it was closed and through which PR.
|
|
23
|
+
- `cassis dbt push` sends your dbt project's artifacts (`target/manifest.json`, and `catalog.json` when present) to Cassis from the dbt project's CI, with the commit they were built from. Cassis checks the files, matches each dbt model, seed, snapshot and source to a table of the project's data source, and keeps the upload as the project's dbt baseline unless a newer one is already there. The command prints how many relations matched and how many tests they declare. Cassis compares the upload with the previous baseline and files each part of the context the merged change puts in doubt as an issue (impact "at risk"); the command lists them with links. Nothing the agent answers with changes.
|
|
24
|
+
- `cassis dbt check` runs the same comparison on a pull request, before the change merges: it uploads the PR's artifacts (after `dbt compile` on the production target), compares them with the baseline, and prints the context tables, columns, joins and metrics the change puts in doubt, with why it matters and the context change to make. Each finding also says its **reach**, how many chat answers used its context tables in the last 30 days, and within each section the most-reached objects print first, the order the Review page will show them in. Findings are advisory: nothing is filed, the baseline doesn't move, and the check exits 0 with or without findings.
|
|
22
25
|
- `cassis verify` runs the full local gate in one verb (`context fmt --check`, `context check`, `eval run`), stopping at the first failure. One command in a checkout ("is this change safe to merge?"), one job in CI. `--no-eval` skips the eval suite.
|
|
23
26
|
|
|
24
27
|
## Install
|
|
@@ -41,7 +44,7 @@ The context tree under `<base-path>` (default `cassis/`) is:
|
|
|
41
44
|
|
|
42
45
|
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
43
46
|
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
44
|
-
3. For `pull`, `upload`, `schema pull`, `eval run`, `context test`, the `eval` case commands (`add-case`, `list-cases`, `delete-case`), and the `issues` commands: the project ID (UUID) is taken from `<base-path>/project.yml` in the checkout (written by `pull` and by publishing), so once a repo is pulled you don't need to pass it. To override, or before the first pull, set `CASSIS_PROJECT_ID` or pass `--project` (find the UUID with `cassis projects list`, or in the project's URL). `context check` uses the same resolution but treats it as optional: unbound checkouts get the project-less validation (no schema reference warnings).
|
|
47
|
+
3. For `pull`, `upload`, `schema pull`, `eval run`, `context test`, the `eval` case commands (`add-case`, `list-cases`, `history`, `delete-case`), and the `issues` commands: the project ID (UUID) is taken from `<base-path>/project.yml` in the checkout (written by `pull` and by publishing), so once a repo is pulled you don't need to pass it. To override, or before the first pull, set `CASSIS_PROJECT_ID` or pass `--project` (find the UUID with `cassis projects list`, or in the project's URL). `context check` uses the same resolution but treats it as optional: unbound checkouts get the project-less validation (no schema reference warnings).
|
|
45
48
|
|
|
46
49
|
## Usage
|
|
47
50
|
|
|
@@ -109,10 +112,18 @@ cassis eval add-case --project ... -q "How much was refunded last month?" \
|
|
|
109
112
|
cassis eval add-case --project ... -q "How much was refunded last month?" \
|
|
110
113
|
--gold-sql-file refunds.sql
|
|
111
114
|
|
|
112
|
-
# List the suite's cases (id + question
|
|
115
|
+
# List the suite's cases (id + question, a warning under any case whose gold SQL
|
|
116
|
+
# reads tables the context lacks; --json adds the gold SQL and the footprint;
|
|
117
|
+
# --branch <name> resolves against a context branch), see how one has behaved
|
|
118
|
+
# across runs, then prune one:
|
|
113
119
|
cassis eval list-cases --project ...
|
|
120
|
+
cassis eval history 019f0000-0000-7000-8000-0000000000ca --project ...
|
|
114
121
|
cassis eval delete-case 019f0000-0000-7000-8000-0000000000ca --project ...
|
|
115
122
|
|
|
123
|
+
# Run the suite 3 times to see which cases flip, and why; print it again later:
|
|
124
|
+
cassis eval run --repeat 3
|
|
125
|
+
cassis eval compare 019f0000-0000-7000-8000-00000000aaaa --project ...
|
|
126
|
+
|
|
116
127
|
# Pull the source schema into <base-path>/.schema.json (gitignored local snapshot):
|
|
117
128
|
cassis schema pull
|
|
118
129
|
|
|
@@ -153,6 +164,16 @@ cassis issues reopen 019f0000-0000-7000-8000-0000000000e1
|
|
|
153
164
|
cassis status
|
|
154
165
|
cassis status --watch --timeout 600
|
|
155
166
|
|
|
167
|
+
# In the dbt project's CI, after a merge to the production branch: write the manifest and the
|
|
168
|
+
# catalog, then send them (findings are filed as issues). On GitHub Actions, expose GITHUB_TOKEN to
|
|
169
|
+
# the step so the push names the pull request that merged the commit:
|
|
170
|
+
dbt docs generate --target prod
|
|
171
|
+
cassis dbt push target --project ...
|
|
172
|
+
|
|
173
|
+
# On a dbt pull request: compile on the production target, then list what the change puts in doubt:
|
|
174
|
+
dbt compile --target prod
|
|
175
|
+
cassis dbt check target --project ...
|
|
176
|
+
|
|
156
177
|
# The full local gate in one verb (fmt --check, check, eval run; stops at the first failure):
|
|
157
178
|
cassis verify
|
|
158
179
|
cassis verify --no-eval
|
|
@@ -165,7 +186,7 @@ Configuration (flags take precedence over env vars):
|
|
|
165
186
|
| `--api-key` | `CASSIS_API_KEY` | none (required) |
|
|
166
187
|
| `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
|
|
167
188
|
| `--base-path` | `CASSIS_BASE_PATH` | `cassis`, must match the project's git-sync "Path" setting |
|
|
168
|
-
| `--project` (check, pull, upload, schema pull, eval run, eval add-case, eval list-cases, eval delete-case, test, issues) | `CASSIS_PROJECT_ID` | the id in `<base-path>/project.yml` (required before the first pull; `check` alone falls back to the project-less validation when unbound) |
|
|
189
|
+
| `--project` (check, pull, upload, schema pull, eval run, eval add-case, eval list-cases, eval history, eval compare, eval delete-case, test, issues, dbt push, dbt check) | `CASSIS_PROJECT_ID` | the id in `<base-path>/project.yml` (required before the first pull; `check` alone falls back to the project-less validation when unbound) |
|
|
169
190
|
|
|
170
191
|
`cassis eval run` also accepts `--case <id>` (repeatable; run only the named
|
|
171
192
|
cases, ids from `eval list-cases` or `add-case`), `--label` (run label in the Evals page; defaults
|
|
@@ -194,10 +215,10 @@ cassis context fmt --check
|
|
|
194
215
|
|
|
195
216
|
| Code | Meaning |
|
|
196
217
|
| ---- | ------------------------------------------------------------------------------ |
|
|
197
|
-
| 0 | Context is valid (check) / pulled (pull) / uploaded (upload) / eval run completed all-passed (eval run) / every probe completed (test, whatever its outcome; probes are informational, don't gate CI on them) |
|
|
198
|
-
| 1 | Validation failed (check: findings printed; upload: nothing imported; eval run: invalid tree, failed cases, or failed/cancelled run; test: invalid tree or a probe failed; add-case: duplicate question or gold SQL that does not run; delete-case: no such case in the project; issues: no such issue or occurrence in the project; issues analyze: failed or cancelled analysis run; schema plan/apply: extraction is incomplete, the plan failed (unparseable or truncated DDL), is stale or expired, the apply failed, or the project won't accept it (a plan is being applied, a DDL was given for a warehouse-connected project, or --warehouse for a DDL-only one)) |
|
|
199
|
-
| 2 | Usage error (missing API key or project, no context directory, unreadable file, tree over the size limits, `eval run --branch` naming a context branch the project does not have, `upload` or `schema push` outside a git checkout or with uncommitted context files) |
|
|
200
|
-
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another eval run or issue analysis already active, out of credits,
|
|
218
|
+
| 0 | Context is valid (check) / pulled (pull) / uploaded (upload) / eval run completed all-passed (eval run) / every probe completed (test, whatever its outcome; probes are informational, don't gate CI on them) / dbt upload accepted, or kept behind a newer one (dbt push) / dbt upload checked, with or without findings (dbt check) |
|
|
219
|
+
| 1 | Validation failed (check: findings printed; upload: nothing imported; eval run: invalid tree, failed cases, or failed/cancelled run; test: invalid tree or a probe failed; add-case: duplicate question or gold SQL that does not run; delete-case, history: no such case in the project; issues: no such issue or occurrence in the project; issues analyze: failed or cancelled analysis run; schema plan/apply: extraction is incomplete, the plan failed (unparseable or truncated DDL), is stale or expired, the apply failed, or the project won't accept it (a plan is being applied, a DDL was given for a warehouse-connected project, or --warehouse for a DDL-only one); dbt push: the upload was refused or rejected (a file is invalid, too large or from an unsupported dbt version, or no relation matches the project's data source); dbt check: the upload was refused or rejected (an invalid file, or no baseline yet: run `cassis dbt push` first)) |
|
|
220
|
+
| 2 | Usage error (missing API key or project, no context directory, unreadable file, tree over the size limits, `eval run --branch` naming a context branch the project does not have, `upload` or `schema push` outside a git checkout or with uncommitted context files, `dbt push` or `dbt check` without a manifest.json, outside a git checkout without `--commit`, or with an artifact over the size limits) |
|
|
221
|
+
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another eval run or issue analysis already active, out of credits, `--timeout` reached, or Cassis failed on its side while processing a dbt upload (`dbt push` / `dbt check`: nothing to fix in the upload, push again) |
|
|
201
222
|
|
|
202
223
|
Commands that send the local tree (`check`, `fmt`, `upload`, `eval run`, `test`) accept up to
|
|
203
224
|
20,000 context files / 100 MB total (path + content bytes), sized for a context of
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import sys
|
|
6
6
|
from typing import Any, Optional
|
|
7
|
+
from urllib.parse import urlencode
|
|
7
8
|
|
|
8
9
|
import httpx
|
|
9
10
|
from cassis_cli import __version__
|
|
@@ -593,6 +594,7 @@ def post_eval_run_start(
|
|
|
593
594
|
branch: Optional[str] = None,
|
|
594
595
|
label: Optional[str] = None,
|
|
595
596
|
case_ids: Optional[list[str]] = None,
|
|
597
|
+
repeat_count: Optional[int] = None,
|
|
596
598
|
transport: Optional[httpx.BaseTransport] = None,
|
|
597
599
|
) -> dict[str, Any]:
|
|
598
600
|
"""POST to /api/ci/projects/{project_id}/eval/runs and return the run record."""
|
|
@@ -606,6 +608,8 @@ def post_eval_run_start(
|
|
|
606
608
|
body["label"] = label
|
|
607
609
|
if case_ids is not None:
|
|
608
610
|
body["test_case_ids"] = case_ids
|
|
611
|
+
if repeat_count is not None:
|
|
612
|
+
body["repeat_count"] = repeat_count
|
|
609
613
|
try:
|
|
610
614
|
# May carry the whole ontology tree in `files` — same budget as the
|
|
611
615
|
# other whole-tree endpoints.
|
|
@@ -692,16 +696,68 @@ def get_eval_cases(
|
|
|
692
696
|
api_url: str,
|
|
693
697
|
api_key: str,
|
|
694
698
|
project_id: str,
|
|
699
|
+
branch: Optional[str] = None,
|
|
695
700
|
transport: Optional[httpx.BaseTransport] = None,
|
|
696
701
|
) -> list[dict[str, Any]]:
|
|
697
|
-
"""GET /api/ci/projects/{project_id}/eval/cases and return the case list.
|
|
702
|
+
"""GET /api/ci/projects/{project_id}/eval/cases and return the case list.
|
|
703
|
+
|
|
704
|
+
With ``branch``, each case's footprint is resolved against that context
|
|
705
|
+
branch instead of main; a branch the project lacks raises
|
|
706
|
+
`EvalBranchNotFoundError` off the server's exact 404 detail.
|
|
707
|
+
"""
|
|
698
708
|
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/eval/cases"
|
|
699
|
-
|
|
709
|
+
if branch is not None:
|
|
710
|
+
url += "?" + urlencode({"branch": branch})
|
|
711
|
+
try:
|
|
712
|
+
with _client(transport=transport) as client:
|
|
713
|
+
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
714
|
+
except httpx.HTTPError as exc:
|
|
715
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
716
|
+
if response.status_code == 401:
|
|
717
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
718
|
+
if response.status_code == 404 and _is_branch_not_found(_detail_or_text(response)):
|
|
719
|
+
raise EvalBranchNotFoundError(_detail_or_text(response))
|
|
720
|
+
if response.status_code in (403, 404):
|
|
721
|
+
raise _project_scope_error(response)
|
|
722
|
+
if response.status_code >= 400:
|
|
723
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
724
|
+
result = _parse_json_response(response, url)
|
|
700
725
|
if not isinstance(result, list):
|
|
701
726
|
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
702
727
|
return result
|
|
703
728
|
|
|
704
729
|
|
|
730
|
+
def get_eval_case_history(
|
|
731
|
+
*,
|
|
732
|
+
api_url: str,
|
|
733
|
+
api_key: str,
|
|
734
|
+
project_id: str,
|
|
735
|
+
case_id: str,
|
|
736
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
737
|
+
) -> dict[str, Any]:
|
|
738
|
+
"""GET /api/ci/projects/{project_id}/eval/cases/{case_id}/history: the case's health and results."""
|
|
739
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/eval/cases/{case_id}/history"
|
|
740
|
+
try:
|
|
741
|
+
with _client(transport=transport) as client:
|
|
742
|
+
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
743
|
+
except httpx.HTTPError as exc:
|
|
744
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
745
|
+
if response.status_code == 401:
|
|
746
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
747
|
+
# Same exact-match wire contract as `delete_eval_case`: a missing case
|
|
748
|
+
# (exit 1) is not a project-scope 404 (exit 3).
|
|
749
|
+
if response.status_code == 404 and _detail_or_text(response) == "Eval case not found":
|
|
750
|
+
raise EvalCaseNotFoundError(f"No current eval case {case_id} in this project (already deleted, or wrong id?).")
|
|
751
|
+
if response.status_code in (403, 404):
|
|
752
|
+
raise _project_scope_error(response)
|
|
753
|
+
if response.status_code >= 400:
|
|
754
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
755
|
+
result = _parse_json_response(response, url)
|
|
756
|
+
if not isinstance(result, dict) or not all(key in result for key in ("case_id", "health", "results")):
|
|
757
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
758
|
+
return result
|
|
759
|
+
|
|
760
|
+
|
|
705
761
|
def delete_eval_case(
|
|
706
762
|
*,
|
|
707
763
|
api_url: str,
|
|
@@ -780,6 +836,41 @@ def get_eval_run_results(
|
|
|
780
836
|
return result
|
|
781
837
|
|
|
782
838
|
|
|
839
|
+
class EvalCompareUnsupportedError(ApiError):
|
|
840
|
+
"""The server predates run comparisons."""
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def get_eval_run_comparison(
|
|
844
|
+
*,
|
|
845
|
+
api_url: str,
|
|
846
|
+
api_key: str,
|
|
847
|
+
project_id: str,
|
|
848
|
+
run_id: str,
|
|
849
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
850
|
+
) -> dict[str, Any]:
|
|
851
|
+
"""GET /api/ci/projects/{project_id}/eval/runs/{run_id}/comparison: the run's attempts compared per case."""
|
|
852
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/eval/runs/{run_id}/comparison"
|
|
853
|
+
try:
|
|
854
|
+
with _client(transport=transport) as client:
|
|
855
|
+
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
856
|
+
except httpx.HTTPError as exc:
|
|
857
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
858
|
+
if response.status_code == 401:
|
|
859
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
860
|
+
# A server without the route answers the framework's own "Not Found",
|
|
861
|
+
# distinct from the "Eval run not found" a real lookup returns.
|
|
862
|
+
if response.status_code == 404 and _detail_or_text(response) == "Not Found":
|
|
863
|
+
raise EvalCompareUnsupportedError("This Cassis server does not compare runs yet; it needs a newer server.")
|
|
864
|
+
if response.status_code in (403, 404):
|
|
865
|
+
raise _project_scope_error(response)
|
|
866
|
+
if response.status_code >= 400:
|
|
867
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
868
|
+
result = _parse_json_response(response, url)
|
|
869
|
+
if not isinstance(result, dict) or not all(key in result for key in ("run_id", "summary", "cases")):
|
|
870
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
871
|
+
return result
|
|
872
|
+
|
|
873
|
+
|
|
783
874
|
def post_eval_run_cancel(
|
|
784
875
|
*,
|
|
785
876
|
api_url: str,
|
|
@@ -1141,3 +1232,99 @@ def post_issue_analysis_cancel(
|
|
|
1141
1232
|
raise _project_scope_error(response)
|
|
1142
1233
|
if response.status_code >= 400:
|
|
1143
1234
|
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
class DbtUploadRejectedError(ApiError):
|
|
1238
|
+
"""The server refused the dbt upload request itself (400, 413, 422).
|
|
1239
|
+
|
|
1240
|
+
A file misnamed, too large or not gzip, an invalid field, or a project the
|
|
1241
|
+
upload can't go to (no data source): the message is the server's detail.
|
|
1242
|
+
"""
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
_DBT_UPLOAD_NOT_FOUND = "dbt upload not found"
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
def _raise_for_dbt_status(response: httpx.Response) -> None:
|
|
1249
|
+
"""Map the dbt routes' error statuses to the CLI's exceptions; return on 2xx."""
|
|
1250
|
+
if response.status_code == 401:
|
|
1251
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
1252
|
+
if response.status_code in (400, 413, 422):
|
|
1253
|
+
raise DbtUploadRejectedError(_readable_detail(_detail_or_text(response)))
|
|
1254
|
+
if response.status_code == 404:
|
|
1255
|
+
detail = _detail_or_text(response)
|
|
1256
|
+
if detail == _DBT_UPLOAD_NOT_FOUND:
|
|
1257
|
+
raise ApiError("The dbt upload was not found in this project.")
|
|
1258
|
+
if detail == "Not Found":
|
|
1259
|
+
raise ServerTooOldError("This Cassis server does not accept dbt uploads yet: upgrade the server first.")
|
|
1260
|
+
raise _project_scope_error(response)
|
|
1261
|
+
if response.status_code == 403:
|
|
1262
|
+
raise _project_scope_error(response)
|
|
1263
|
+
if response.status_code >= 400:
|
|
1264
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
def _readable_detail(detail: Any) -> str:
|
|
1268
|
+
"""A FastAPI validation error list as `field: message` pairs; any other detail as text."""
|
|
1269
|
+
if not isinstance(detail, list):
|
|
1270
|
+
return str(detail)
|
|
1271
|
+
parts = []
|
|
1272
|
+
for error in detail:
|
|
1273
|
+
if not isinstance(error, dict):
|
|
1274
|
+
parts.append(str(error))
|
|
1275
|
+
continue
|
|
1276
|
+
location = [str(part) for part in error.get("loc") or [] if part not in ("body", "query", "path")]
|
|
1277
|
+
message = str(error.get("msg") or "invalid")
|
|
1278
|
+
parts.append(f"{'.'.join(location)}: {message}" if location else message)
|
|
1279
|
+
return "; ".join(parts)
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
def _dbt_observation_response(response: httpx.Response, url: str) -> dict[str, Any]:
|
|
1283
|
+
_raise_for_dbt_status(response)
|
|
1284
|
+
result = _parse_json_response(response, url)
|
|
1285
|
+
if not isinstance(result, dict) or "id" not in result or "status" not in result:
|
|
1286
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
1287
|
+
return result
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
def post_dbt_observation(
|
|
1291
|
+
*,
|
|
1292
|
+
api_url: str,
|
|
1293
|
+
api_key: str,
|
|
1294
|
+
project_id: str,
|
|
1295
|
+
files: "dict[str, bytes]",
|
|
1296
|
+
fields: "dict[str, str]",
|
|
1297
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
1298
|
+
) -> dict[str, Any]:
|
|
1299
|
+
"""POST gzip-compressed dbt artifacts to /api/ci/projects/{project_id}/dbt/observations.
|
|
1300
|
+
|
|
1301
|
+
``files`` maps each artifact's file name (``manifest.json``, ...) to its
|
|
1302
|
+
gzip bytes; ``fields`` carries the git context. Returns the PROCESSING
|
|
1303
|
+
upload record.
|
|
1304
|
+
"""
|
|
1305
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/dbt/observations"
|
|
1306
|
+
multipart = {name[: -len(".json")]: (f"{name}.gz", content, "application/gzip") for name, content in files.items()}
|
|
1307
|
+
try:
|
|
1308
|
+
with _client(timeout=ONTOLOGY_TREE_TIMEOUT_SECONDS, transport=transport) as client:
|
|
1309
|
+
response = client.post(url, files=multipart, data=fields, headers={"Authorization": f"Bearer {api_key}"})
|
|
1310
|
+
except httpx.HTTPError as exc:
|
|
1311
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
1312
|
+
return _dbt_observation_response(response, url)
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
def get_dbt_observation(
|
|
1316
|
+
*,
|
|
1317
|
+
api_url: str,
|
|
1318
|
+
api_key: str,
|
|
1319
|
+
project_id: str,
|
|
1320
|
+
observation_id: str,
|
|
1321
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
1322
|
+
) -> dict[str, Any]:
|
|
1323
|
+
"""GET /api/ci/projects/{project_id}/dbt/observations/{observation_id} and return the upload record."""
|
|
1324
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/dbt/observations/{observation_id}"
|
|
1325
|
+
try:
|
|
1326
|
+
with _client(transport=transport) as client:
|
|
1327
|
+
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
1328
|
+
except httpx.HTTPError as exc:
|
|
1329
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
1330
|
+
return _dbt_observation_response(response, url)
|