cassis-cli 1.2.0__tar.gz → 1.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-1.2.0 → cassis_cli-1.4.0}/PKG-INFO +33 -4
- {cassis_cli-1.2.0 → cassis_cli-1.4.0}/README.md +32 -3
- {cassis_cli-1.2.0 → cassis_cli-1.4.0}/cassis_cli/__init__.py +1 -1
- {cassis_cli-1.2.0 → cassis_cli-1.4.0}/cassis_cli/api.py +119 -2
- {cassis_cli-1.2.0 → cassis_cli-1.4.0}/cassis_cli/eval.py +44 -7
- {cassis_cli-1.2.0 → cassis_cli-1.4.0}/cassis_cli/main.py +6 -0
- {cassis_cli-1.2.0 → cassis_cli-1.4.0}/cassis_cli/ontology.py +23 -7
- cassis_cli-1.4.0/cassis_cli/projects.py +69 -0
- cassis_cli-1.4.0/cassis_cli/schema.py +295 -0
- cassis_cli-1.4.0/cassis_cli/status.py +184 -0
- cassis_cli-1.4.0/cassis_cli/verify.py +123 -0
- {cassis_cli-1.2.0 → cassis_cli-1.4.0}/pyproject.toml +1 -1
- cassis_cli-1.2.0/cassis_cli/schema.py +0 -129
- {cassis_cli-1.2.0 → cassis_cli-1.4.0}/LICENSE +0 -0
- {cassis_cli-1.2.0 → cassis_cli-1.4.0}/NOTICE +0 -0
- {cassis_cli-1.2.0 → cassis_cli-1.4.0}/cassis_cli/common.py +0 -0
- {cassis_cli-1.2.0 → cassis_cli-1.4.0}/cassis_cli/guide.py +0 -0
- {cassis_cli-1.2.0 → cassis_cli-1.4.0}/cassis_cli/ontology_design_guide.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cassis-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Cassis CLI — run Cassis actions (ontology validation, upload and publish) from your CI pipelines
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -24,7 +24,7 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
|
|
25
25
|
Run Cassis actions from your CI pipelines:
|
|
26
26
|
|
|
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. 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** — advisory only (the object may simply not be built or synced yet)
|
|
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.
|
|
@@ -35,6 +35,10 @@ Run Cassis actions from your CI pipelines:
|
|
|
35
35
|
- `cassis ontology test` runs individual questions through the text-to-SQL agent using your local ontology 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
36
|
- `cassis eval add-case` adds a gold question/SQL case to the project's eval suite — after fixing an ontology issue, add the question users were failing on so `eval run` guards it from regressing.
|
|
37
37
|
- `cassis eval list-cases` and `cassis eval delete-case` maintain the suite: list the current cases with their ids, and prune one that is stale or wrong (e.g. its gold SQL encodes a definition the ontology has since changed).
|
|
38
|
+
- `cassis schema push` uploads a DDL file to detect source-schema changes on a DDL-only project (same as the webapp's "Update from DDL" button): Cassis diffs the DDL against the ontology and surfaces added, dropped, and changed objects in Ontology > Review > Data source for approval. Waits for completion by default; `--no-wait` returns immediately.
|
|
39
|
+
- `cassis projects list` lists the projects your API key can reach — id (what `--project` and `CASSIS_PROJECT_ID` take), name, published ontology version, and data-source dialect — so a pipeline or agent can discover the project id from the terminal instead of fishing it out of a webapp URL.
|
|
40
|
+
- `cassis status` shows the project's published version (number, label, git commit), whether unpublished changes await publication, the git-sync binding, 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 ontology — instead of watching the GitHub Actions tab.
|
|
41
|
+
- `cassis verify` runs the full local gate in one verb — `ontology fmt --check`, `ontology 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.
|
|
38
42
|
|
|
39
43
|
## Install
|
|
40
44
|
|
|
@@ -56,7 +60,7 @@ The ontology tree under `<base-path>` (default `cassis/`) is:
|
|
|
56
60
|
|
|
57
61
|
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
58
62
|
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
59
|
-
3. For `pull`, `upload`, `schema pull`, `eval run`, `ontology test`, and the `eval` case commands (`add-case`, `list-cases`, `delete-case`): 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 in the project's URL). `ontology check` uses the same resolution but treats it as optional: unbound checkouts get the project-less validation (no schema reference warnings).
|
|
63
|
+
3. For `pull`, `upload`, `schema pull`, `eval run`, `ontology test`, and the `eval` case commands (`add-case`, `list-cases`, `delete-case`): 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). `ontology check` uses the same resolution but treats it as optional: unbound checkouts get the project-less validation (no schema reference warnings).
|
|
60
64
|
|
|
61
65
|
## Usage
|
|
62
66
|
|
|
@@ -93,6 +97,9 @@ cassis eval run --project ...
|
|
|
93
97
|
# Run against an existing Cassis ontology branch, or the unpublished ontology:
|
|
94
98
|
cassis eval run --project ... --branch feature-x
|
|
95
99
|
|
|
100
|
+
# Run only specific cases (repeatable) — e.g. prove a fresh add-case in seconds:
|
|
101
|
+
cassis eval run --project ... --case 019f0000-0000-7000-8000-0000000000ca
|
|
102
|
+
|
|
96
103
|
# Start the run and return immediately (poll in the webapp):
|
|
97
104
|
cassis eval run --project ... --no-wait
|
|
98
105
|
|
|
@@ -104,12 +111,33 @@ cassis ontology test --project ... -q "How much was refunded last month?" -q "Ne
|
|
|
104
111
|
cassis eval add-case --project ... -q "How much was refunded last month?" \
|
|
105
112
|
--gold-sql "SELECT SUM(refunded_cents) / 100.0 FROM public.orders WHERE ..."
|
|
106
113
|
|
|
114
|
+
# Multi-line gold SQL: read it from a file instead (no shell quoting pitfalls):
|
|
115
|
+
cassis eval add-case --project ... -q "How much was refunded last month?" \
|
|
116
|
+
--gold-sql-file refunds.sql
|
|
117
|
+
|
|
107
118
|
# List the suite's cases (id + question; --json adds the gold SQL), then prune one:
|
|
108
119
|
cassis eval list-cases --project ...
|
|
109
120
|
cassis eval delete-case 019f0000-0000-7000-8000-0000000000ca --project ...
|
|
110
121
|
|
|
111
122
|
# Pull the source schema into <base-path>/.schema.json (gitignored local snapshot):
|
|
112
123
|
cassis schema pull
|
|
124
|
+
|
|
125
|
+
# Push a DDL file to detect source-schema changes (DDL-only projects):
|
|
126
|
+
cassis schema push schema.sql
|
|
127
|
+
|
|
128
|
+
# Push and return immediately (poll in the webapp):
|
|
129
|
+
cassis schema push schema.sql --no-wait
|
|
130
|
+
|
|
131
|
+
# List the projects the API key can reach (id, name, published version, dialect):
|
|
132
|
+
cassis projects list
|
|
133
|
+
|
|
134
|
+
# Published version vs local checkout (add --watch to poll until your merge is published):
|
|
135
|
+
cassis status
|
|
136
|
+
cassis status --watch --timeout 600
|
|
137
|
+
|
|
138
|
+
# The full local gate in one verb (fmt --check, check, eval run; stops at the first failure):
|
|
139
|
+
cassis verify
|
|
140
|
+
cassis verify --no-eval
|
|
113
141
|
```
|
|
114
142
|
|
|
115
143
|
Configuration (flags take precedence over env vars):
|
|
@@ -121,7 +149,8 @@ Configuration (flags take precedence over env vars):
|
|
|
121
149
|
| `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
|
|
122
150
|
| `--project` (check, pull, upload, schema pull, eval run, eval add-case, eval list-cases, eval delete-case, test) | `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) |
|
|
123
151
|
|
|
124
|
-
`cassis eval run` also accepts `--
|
|
152
|
+
`cassis eval run` also accepts `--case <id>` (repeatable; run only the named
|
|
153
|
+
cases, ids from `eval list-cases` or `add-case`), `--label` (run label in the Evals page; defaults
|
|
125
154
|
to the branch name from the CI environment or the local git checkout; rejected
|
|
126
155
|
with `--branch`, whose runs are labelled with the branch name), `--wait/--no-wait`, `--poll-interval` (5 s),
|
|
127
156
|
`--timeout` (30 min — the run keeps going server-side if the CLI stops waiting),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Run Cassis actions from your CI pipelines:
|
|
4
4
|
|
|
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. 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** — advisory only (the object may simply not be built or synced yet)
|
|
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.
|
|
@@ -13,6 +13,10 @@ Run Cassis actions from your CI pipelines:
|
|
|
13
13
|
- `cassis ontology test` runs individual questions through the text-to-SQL agent using your local ontology 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
14
|
- `cassis eval add-case` adds a gold question/SQL case to the project's eval suite — after fixing an ontology issue, add the question users were failing on so `eval run` guards it from regressing.
|
|
15
15
|
- `cassis eval list-cases` and `cassis eval delete-case` maintain the suite: list the current cases with their ids, and prune one that is stale or wrong (e.g. its gold SQL encodes a definition the ontology has since changed).
|
|
16
|
+
- `cassis schema push` uploads a DDL file to detect source-schema changes on a DDL-only project (same as the webapp's "Update from DDL" button): Cassis diffs the DDL against the ontology and surfaces added, dropped, and changed objects in Ontology > Review > Data source for approval. Waits for completion by default; `--no-wait` returns immediately.
|
|
17
|
+
- `cassis projects list` lists the projects your API key can reach — id (what `--project` and `CASSIS_PROJECT_ID` take), name, published ontology version, and data-source dialect — so a pipeline or agent can discover the project id from the terminal instead of fishing it out of a webapp URL.
|
|
18
|
+
- `cassis status` shows the project's published version (number, label, git commit), whether unpublished changes await publication, the git-sync binding, 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 ontology — instead of watching the GitHub Actions tab.
|
|
19
|
+
- `cassis verify` runs the full local gate in one verb — `ontology fmt --check`, `ontology 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.
|
|
16
20
|
|
|
17
21
|
## Install
|
|
18
22
|
|
|
@@ -34,7 +38,7 @@ The ontology tree under `<base-path>` (default `cassis/`) is:
|
|
|
34
38
|
|
|
35
39
|
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
36
40
|
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
37
|
-
3. For `pull`, `upload`, `schema pull`, `eval run`, `ontology test`, and the `eval` case commands (`add-case`, `list-cases`, `delete-case`): 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 in the project's URL). `ontology check` uses the same resolution but treats it as optional: unbound checkouts get the project-less validation (no schema reference warnings).
|
|
41
|
+
3. For `pull`, `upload`, `schema pull`, `eval run`, `ontology test`, and the `eval` case commands (`add-case`, `list-cases`, `delete-case`): 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). `ontology check` uses the same resolution but treats it as optional: unbound checkouts get the project-less validation (no schema reference warnings).
|
|
38
42
|
|
|
39
43
|
## Usage
|
|
40
44
|
|
|
@@ -71,6 +75,9 @@ cassis eval run --project ...
|
|
|
71
75
|
# Run against an existing Cassis ontology branch, or the unpublished ontology:
|
|
72
76
|
cassis eval run --project ... --branch feature-x
|
|
73
77
|
|
|
78
|
+
# Run only specific cases (repeatable) — e.g. prove a fresh add-case in seconds:
|
|
79
|
+
cassis eval run --project ... --case 019f0000-0000-7000-8000-0000000000ca
|
|
80
|
+
|
|
74
81
|
# Start the run and return immediately (poll in the webapp):
|
|
75
82
|
cassis eval run --project ... --no-wait
|
|
76
83
|
|
|
@@ -82,12 +89,33 @@ cassis ontology test --project ... -q "How much was refunded last month?" -q "Ne
|
|
|
82
89
|
cassis eval add-case --project ... -q "How much was refunded last month?" \
|
|
83
90
|
--gold-sql "SELECT SUM(refunded_cents) / 100.0 FROM public.orders WHERE ..."
|
|
84
91
|
|
|
92
|
+
# Multi-line gold SQL: read it from a file instead (no shell quoting pitfalls):
|
|
93
|
+
cassis eval add-case --project ... -q "How much was refunded last month?" \
|
|
94
|
+
--gold-sql-file refunds.sql
|
|
95
|
+
|
|
85
96
|
# List the suite's cases (id + question; --json adds the gold SQL), then prune one:
|
|
86
97
|
cassis eval list-cases --project ...
|
|
87
98
|
cassis eval delete-case 019f0000-0000-7000-8000-0000000000ca --project ...
|
|
88
99
|
|
|
89
100
|
# Pull the source schema into <base-path>/.schema.json (gitignored local snapshot):
|
|
90
101
|
cassis schema pull
|
|
102
|
+
|
|
103
|
+
# Push a DDL file to detect source-schema changes (DDL-only projects):
|
|
104
|
+
cassis schema push schema.sql
|
|
105
|
+
|
|
106
|
+
# Push and return immediately (poll in the webapp):
|
|
107
|
+
cassis schema push schema.sql --no-wait
|
|
108
|
+
|
|
109
|
+
# List the projects the API key can reach (id, name, published version, dialect):
|
|
110
|
+
cassis projects list
|
|
111
|
+
|
|
112
|
+
# Published version vs local checkout (add --watch to poll until your merge is published):
|
|
113
|
+
cassis status
|
|
114
|
+
cassis status --watch --timeout 600
|
|
115
|
+
|
|
116
|
+
# The full local gate in one verb (fmt --check, check, eval run; stops at the first failure):
|
|
117
|
+
cassis verify
|
|
118
|
+
cassis verify --no-eval
|
|
91
119
|
```
|
|
92
120
|
|
|
93
121
|
Configuration (flags take precedence over env vars):
|
|
@@ -99,7 +127,8 @@ Configuration (flags take precedence over env vars):
|
|
|
99
127
|
| `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
|
|
100
128
|
| `--project` (check, pull, upload, schema pull, eval run, eval add-case, eval list-cases, eval delete-case, test) | `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) |
|
|
101
129
|
|
|
102
|
-
`cassis eval run` also accepts `--
|
|
130
|
+
`cassis eval run` also accepts `--case <id>` (repeatable; run only the named
|
|
131
|
+
cases, ids from `eval list-cases` or `add-case`), `--label` (run label in the Evals page; defaults
|
|
103
132
|
to the branch name from the CI environment or the local git checkout; rejected
|
|
104
133
|
with `--branch`, whose runs are labelled with the branch name), `--wait/--no-wait`, `--poll-interval` (5 s),
|
|
105
134
|
`--timeout` (30 min — the run keeps going server-side if the CLI stops waiting),
|
|
@@ -139,9 +139,10 @@ def post_ontology_check(
|
|
|
139
139
|
) -> dict[str, Any]:
|
|
140
140
|
"""POST the ontology tree to the check endpoint and return the response body.
|
|
141
141
|
|
|
142
|
+
Both routes return advisory ``audit`` quality findings in ``warnings``.
|
|
142
143
|
With ``project_id``, calls the project-scoped route, which additionally
|
|
143
|
-
cross-checks the tree against the project's source schema and
|
|
144
|
-
|
|
144
|
+
cross-checks the tree against the project's source schema and adds
|
|
145
|
+
``references`` warnings; without it, the pure tree check.
|
|
145
146
|
"""
|
|
146
147
|
if project_id:
|
|
147
148
|
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/ontology/check"
|
|
@@ -243,6 +244,57 @@ def get_ontology_export(
|
|
|
243
244
|
return result["files"]
|
|
244
245
|
|
|
245
246
|
|
|
247
|
+
def get_projects(
|
|
248
|
+
*,
|
|
249
|
+
api_url: str,
|
|
250
|
+
api_key: str,
|
|
251
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
252
|
+
) -> list[dict[str, Any]]:
|
|
253
|
+
"""GET /api/ci/projects and return the project list."""
|
|
254
|
+
url = api_url.rstrip("/") + "/api/ci/projects"
|
|
255
|
+
try:
|
|
256
|
+
with _client(transport=transport) as client:
|
|
257
|
+
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
258
|
+
except httpx.HTTPError as exc:
|
|
259
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
260
|
+
|
|
261
|
+
if response.status_code == 401:
|
|
262
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
263
|
+
if response.status_code >= 400:
|
|
264
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
265
|
+
result = _parse_json_response(response, url)
|
|
266
|
+
if not isinstance(result, list) or not all(isinstance(p, dict) and "id" in p and "name" in p for p in result):
|
|
267
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
268
|
+
return result
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def get_project_status(
|
|
272
|
+
*,
|
|
273
|
+
api_url: str,
|
|
274
|
+
api_key: str,
|
|
275
|
+
project_id: str,
|
|
276
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
277
|
+
) -> dict[str, Any]:
|
|
278
|
+
"""GET /api/ci/projects/{project_id}/status and return the status record."""
|
|
279
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/status"
|
|
280
|
+
try:
|
|
281
|
+
with _client(transport=transport) as client:
|
|
282
|
+
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
283
|
+
except httpx.HTTPError as exc:
|
|
284
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
285
|
+
|
|
286
|
+
if response.status_code == 401:
|
|
287
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
288
|
+
if response.status_code in (403, 404):
|
|
289
|
+
raise _project_scope_error(response)
|
|
290
|
+
if response.status_code >= 400:
|
|
291
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
292
|
+
result = _parse_json_response(response, url)
|
|
293
|
+
if not isinstance(result, dict) or "has_unpublished_changes" not in result:
|
|
294
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
295
|
+
return result
|
|
296
|
+
|
|
297
|
+
|
|
246
298
|
class NoSourceSchemaError(ApiError):
|
|
247
299
|
"""The project's data source has no introspected schema to pull."""
|
|
248
300
|
|
|
@@ -284,6 +336,68 @@ def get_schema_export(
|
|
|
284
336
|
return result
|
|
285
337
|
|
|
286
338
|
|
|
339
|
+
class SourceChangeConflictError(ApiError):
|
|
340
|
+
"""A concurrent detection run is already active, or the project has a connected data source."""
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def post_detect_from_ddl(
|
|
344
|
+
*,
|
|
345
|
+
api_url: str,
|
|
346
|
+
api_key: str,
|
|
347
|
+
project_id: str,
|
|
348
|
+
ddl: str,
|
|
349
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
350
|
+
) -> dict[str, Any]:
|
|
351
|
+
"""POST /api/ci/projects/{project_id}/source-changes/detect-from-ddl and return the run record."""
|
|
352
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/source-changes/detect-from-ddl"
|
|
353
|
+
try:
|
|
354
|
+
with _client(transport=transport) as client:
|
|
355
|
+
response = client.post(url, json={"ddl": ddl}, headers={"Authorization": f"Bearer {api_key}"})
|
|
356
|
+
except httpx.HTTPError as exc:
|
|
357
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
358
|
+
|
|
359
|
+
if response.status_code == 401:
|
|
360
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
361
|
+
if response.status_code == 409:
|
|
362
|
+
raise SourceChangeConflictError(str(_detail_or_text(response)))
|
|
363
|
+
if response.status_code in (403, 404):
|
|
364
|
+
raise _project_scope_error(response)
|
|
365
|
+
if response.status_code >= 400:
|
|
366
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
367
|
+
result = _parse_json_response(response, url)
|
|
368
|
+
if not isinstance(result, dict) or "run_id" not in result:
|
|
369
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
370
|
+
return result
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def get_source_change_run(
|
|
374
|
+
*,
|
|
375
|
+
api_url: str,
|
|
376
|
+
api_key: str,
|
|
377
|
+
project_id: str,
|
|
378
|
+
run_id: str,
|
|
379
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
380
|
+
) -> dict[str, Any]:
|
|
381
|
+
"""GET /api/ci/projects/{project_id}/source-changes/runs/{run_id} and return the run record."""
|
|
382
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/source-changes/runs/{run_id}"
|
|
383
|
+
try:
|
|
384
|
+
with _client(transport=transport) as client:
|
|
385
|
+
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
386
|
+
except httpx.HTTPError as exc:
|
|
387
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
388
|
+
|
|
389
|
+
if response.status_code == 401:
|
|
390
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
391
|
+
if response.status_code in (403, 404):
|
|
392
|
+
raise _project_scope_error(response)
|
|
393
|
+
if response.status_code >= 400:
|
|
394
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
395
|
+
result = _parse_json_response(response, url)
|
|
396
|
+
if not isinstance(result, dict) or "run_id" not in result:
|
|
397
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
398
|
+
return result
|
|
399
|
+
|
|
400
|
+
|
|
287
401
|
def post_eval_run_start(
|
|
288
402
|
*,
|
|
289
403
|
api_url: str,
|
|
@@ -292,6 +406,7 @@ def post_eval_run_start(
|
|
|
292
406
|
files: Optional[dict[str, str]] = None,
|
|
293
407
|
branch: Optional[str] = None,
|
|
294
408
|
label: Optional[str] = None,
|
|
409
|
+
case_ids: Optional[list[str]] = None,
|
|
295
410
|
transport: Optional[httpx.BaseTransport] = None,
|
|
296
411
|
) -> dict[str, Any]:
|
|
297
412
|
"""POST to /api/ci/projects/{project_id}/eval/runs and return the run record."""
|
|
@@ -303,6 +418,8 @@ def post_eval_run_start(
|
|
|
303
418
|
body["branch"] = branch
|
|
304
419
|
if label is not None:
|
|
305
420
|
body["label"] = label
|
|
421
|
+
if case_ids is not None:
|
|
422
|
+
body["test_case_ids"] = case_ids
|
|
306
423
|
try:
|
|
307
424
|
with _client(transport=transport) as client:
|
|
308
425
|
response = client.post(url, json=body, headers={"Authorization": f"Bearer {api_key}"})
|
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
import subprocess
|
|
8
8
|
import time
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any, Optional
|
|
10
|
+
from typing import Any, List, Optional
|
|
11
11
|
from uuid import UUID
|
|
12
12
|
|
|
13
13
|
import typer
|
|
@@ -164,11 +164,16 @@ def add_case(
|
|
|
164
164
|
"--question",
|
|
165
165
|
help="The natural-language question the case guards.",
|
|
166
166
|
),
|
|
167
|
-
gold_sql: str = typer.Option(
|
|
168
|
-
|
|
167
|
+
gold_sql: Optional[str] = typer.Option(
|
|
168
|
+
None,
|
|
169
169
|
"--gold-sql",
|
|
170
170
|
help="The correct SQL for the question; executed at run time to produce the expected output.",
|
|
171
171
|
),
|
|
172
|
+
gold_sql_file: Optional[Path] = typer.Option(
|
|
173
|
+
None,
|
|
174
|
+
"--gold-sql-file",
|
|
175
|
+
help="Read the gold SQL from this file instead of --gold-sql (no shell quoting of multi-line SQL).",
|
|
176
|
+
),
|
|
172
177
|
api_key: Optional[str] = typer.Option(
|
|
173
178
|
None,
|
|
174
179
|
"--api-key",
|
|
@@ -195,13 +200,31 @@ def add_case(
|
|
|
195
200
|
failing on becomes a gold case, so `cassis eval run` guards it from
|
|
196
201
|
regressing. On an executable data source the gold SQL is run before the
|
|
197
202
|
case is stored, so a case that cannot execute never enters the suite.
|
|
203
|
+
The SQL comes from --gold-sql (inline) or --gold-sql-file (a file path;
|
|
204
|
+
prefer it for multi-line SQL, which shell quoting mangles inline).
|
|
198
205
|
Exits 0 on creation, 1 on a duplicate question or gold SQL that does not
|
|
199
206
|
run, 2 on usage errors, 3 on transport/API errors.
|
|
200
207
|
"""
|
|
201
208
|
api_key = require_api_key(api_key)
|
|
202
209
|
project_id = resolve_project_id(project_id, path / Path(base_path))
|
|
210
|
+
if (gold_sql is None) == (gold_sql_file is None):
|
|
211
|
+
typer.secho("Pass exactly one of --gold-sql or --gold-sql-file.", fg=typer.colors.RED, err=True)
|
|
212
|
+
raise typer.Exit(EXIT_USAGE)
|
|
213
|
+
if gold_sql_file is not None:
|
|
214
|
+
try:
|
|
215
|
+
gold_sql = gold_sql_file.read_text(encoding="utf-8")
|
|
216
|
+
# Two separate handlers: the repo-wide black targets py314 and would
|
|
217
|
+
# strip the parens off a tuple form, which is a SyntaxError on the
|
|
218
|
+
# CLI's supported Python (>=3.10).
|
|
219
|
+
except OSError as exc:
|
|
220
|
+
typer.secho(f"Cannot read {gold_sql_file}: {exc}", fg=typer.colors.RED, err=True)
|
|
221
|
+
raise typer.Exit(EXIT_USAGE) from exc
|
|
222
|
+
except UnicodeDecodeError as exc:
|
|
223
|
+
typer.secho(f"Cannot read {gold_sql_file}: {exc}", fg=typer.colors.RED, err=True)
|
|
224
|
+
raise typer.Exit(EXIT_USAGE) from exc
|
|
225
|
+
assert gold_sql is not None
|
|
203
226
|
if not question.strip() or not gold_sql.strip():
|
|
204
|
-
typer.secho("--question and
|
|
227
|
+
typer.secho("--question and the gold SQL must not be empty.", fg=typer.colors.RED, err=True)
|
|
205
228
|
raise typer.Exit(EXIT_USAGE)
|
|
206
229
|
|
|
207
230
|
try:
|
|
@@ -388,6 +411,11 @@ def run(
|
|
|
388
411
|
"--branch",
|
|
389
412
|
help="Run against an existing Cassis ontology branch by name instead of local files.",
|
|
390
413
|
),
|
|
414
|
+
case: Optional[List[str]] = typer.Option(
|
|
415
|
+
None,
|
|
416
|
+
"--case",
|
|
417
|
+
help="Run only this eval case id (repeatable; ids from `eval list-cases` or `add-case`).",
|
|
418
|
+
),
|
|
391
419
|
label: Optional[str] = typer.Option(
|
|
392
420
|
None,
|
|
393
421
|
"--label",
|
|
@@ -412,11 +440,19 @@ def run(
|
|
|
412
440
|
|
|
413
441
|
Uploads the local ontology file tree and scores it in-memory — nothing is pushed or
|
|
414
442
|
persisted in Cassis besides the eval run itself. With --branch, runs against
|
|
415
|
-
an existing Cassis branch instead (no files are sent).
|
|
416
|
-
|
|
417
|
-
|
|
443
|
+
an existing Cassis branch instead (no files are sent). With --case, only the
|
|
444
|
+
named case(s) run — e.g. proving one fresh `add-case` in seconds instead of
|
|
445
|
+
rerunning the whole suite. Exits 0 when the run completes with every case
|
|
446
|
+
passed, 1 on any failed case / failed run / invalid tree, 2 on usage
|
|
447
|
+
errors, 3 on transport errors or --timeout.
|
|
418
448
|
"""
|
|
419
449
|
api_key = require_api_key(api_key)
|
|
450
|
+
for case_id in case or []:
|
|
451
|
+
try:
|
|
452
|
+
UUID(case_id)
|
|
453
|
+
except ValueError:
|
|
454
|
+
typer.secho(f"--case must be an eval case ID (UUID), got {case_id!r}.", fg=typer.colors.RED, err=True)
|
|
455
|
+
raise typer.Exit(EXIT_USAGE)
|
|
420
456
|
if branch is not None and label is not None:
|
|
421
457
|
typer.secho(
|
|
422
458
|
"--label cannot be used with --branch: branch runs are labelled with the branch name.",
|
|
@@ -440,6 +476,7 @@ def run(
|
|
|
440
476
|
files=files,
|
|
441
477
|
branch=branch,
|
|
442
478
|
label=label,
|
|
479
|
+
case_ids=case,
|
|
443
480
|
)
|
|
444
481
|
except EvalStartValidationError as exc:
|
|
445
482
|
_print_validation_failure(exc.detail, base_path)
|
|
@@ -6,7 +6,10 @@ import typer
|
|
|
6
6
|
from cassis_cli import __version__
|
|
7
7
|
from cassis_cli.eval import app as eval_app
|
|
8
8
|
from cassis_cli.ontology import app as ontology_app
|
|
9
|
+
from cassis_cli.projects import app as projects_app
|
|
9
10
|
from cassis_cli.schema import app as schema_app
|
|
11
|
+
from cassis_cli.status import status
|
|
12
|
+
from cassis_cli.verify import verify
|
|
10
13
|
|
|
11
14
|
app = typer.Typer(
|
|
12
15
|
no_args_is_help=True,
|
|
@@ -15,6 +18,9 @@ app = typer.Typer(
|
|
|
15
18
|
app.add_typer(ontology_app, name="ontology")
|
|
16
19
|
app.add_typer(eval_app, name="eval")
|
|
17
20
|
app.add_typer(schema_app, name="schema")
|
|
21
|
+
app.add_typer(projects_app, name="projects")
|
|
22
|
+
app.command()(status)
|
|
23
|
+
app.command()(verify)
|
|
18
24
|
|
|
19
25
|
|
|
20
26
|
@app.command()
|
|
@@ -85,11 +85,14 @@ def check(
|
|
|
85
85
|
"""Validate the ontology files in a repository checkout.
|
|
86
86
|
|
|
87
87
|
Runs the same checks as the Cassis GitHub PR check (YAML parsing,
|
|
88
|
-
round-trip, import validation)
|
|
88
|
+
round-trip, import validation), then prints advisory quality warnings
|
|
89
|
+
(unassigned tables, broken joins/metrics, missing descriptions) for a
|
|
90
|
+
tree that parsed. When the checkout is bound to a project
|
|
89
91
|
(``project.yml``, ``--project``, or ``CASSIS_PROJECT_ID``), the tree is
|
|
90
92
|
additionally cross-checked against the project's source schema; unmatched
|
|
91
|
-
references print as warnings
|
|
92
|
-
1 when validation fails, 2 on usage
|
|
93
|
+
references print as warnings too. Warnings never fail the check or change
|
|
94
|
+
the exit code. Exits 0 when valid, 1 when validation fails, 2 on usage
|
|
95
|
+
errors, 3 on transport/API errors.
|
|
93
96
|
"""
|
|
94
97
|
api_key = _require_api_key(api_key)
|
|
95
98
|
files, base_path = _collect_tree(path, base_path)
|
|
@@ -114,13 +117,17 @@ def check(
|
|
|
114
117
|
raise typer.Exit(EXIT_TRANSPORT) from exc
|
|
115
118
|
|
|
116
119
|
warnings = result.get("warnings") or []
|
|
120
|
+
ref_warnings = [w for w in warnings if w.get("stage") == "references"]
|
|
121
|
+
# Everything else is the quality audit; bucketing by "not references" keeps
|
|
122
|
+
# a warning stage this CLI predates visible instead of silently dropped.
|
|
123
|
+
audit_warnings = [w for w in warnings if w.get("stage") != "references"]
|
|
117
124
|
if json_output:
|
|
118
125
|
typer.echo(json.dumps(result, indent=2))
|
|
119
126
|
elif result["passed"]:
|
|
120
127
|
typer.secho(f"✓ {result['summary']}", fg=typer.colors.GREEN)
|
|
121
128
|
# Disambiguate silence: "no warnings" must never read as "references
|
|
122
129
|
# verified" when the stage didn't run.
|
|
123
|
-
if project_id and result.get("references_checked") and not
|
|
130
|
+
if project_id and result.get("references_checked") and not ref_warnings:
|
|
124
131
|
typer.secho("✓ Schema references resolve against the source schema.", fg=typer.colors.GREEN)
|
|
125
132
|
elif project_id and not result.get("references_checked"):
|
|
126
133
|
typer.secho(
|
|
@@ -135,14 +142,23 @@ def check(
|
|
|
135
142
|
location = f"{base_path}/{finding.get('path')}: " if finding.get("path") else ""
|
|
136
143
|
typer.echo(f" {location}{finding.get('message', '')} ({finding.get('stage', '?')})")
|
|
137
144
|
|
|
138
|
-
if
|
|
145
|
+
if ref_warnings and not json_output:
|
|
139
146
|
typer.secho(
|
|
140
|
-
f"{len(
|
|
147
|
+
f"{len(ref_warnings)} schema reference warning(s) — advisory, expected if the objects "
|
|
141
148
|
"haven't been built or synced yet:",
|
|
142
149
|
fg=typer.colors.YELLOW,
|
|
143
150
|
bold=True,
|
|
144
151
|
)
|
|
145
|
-
for warning in
|
|
152
|
+
for warning in ref_warnings:
|
|
153
|
+
typer.secho(f" {warning.get('message', '')}", fg=typer.colors.YELLOW)
|
|
154
|
+
if audit_warnings and not json_output:
|
|
155
|
+
# Same wording as `ontology test` so the two commands read alike.
|
|
156
|
+
typer.secho(
|
|
157
|
+
f"{len(audit_warnings)} ontology quality warning(s) — advisory, never fail the check:",
|
|
158
|
+
fg=typer.colors.YELLOW,
|
|
159
|
+
bold=True,
|
|
160
|
+
)
|
|
161
|
+
for warning in audit_warnings:
|
|
146
162
|
typer.secho(f" {warning.get('message', '')}", fg=typer.colors.YELLOW)
|
|
147
163
|
|
|
148
164
|
raise typer.Exit(EXIT_OK if result["passed"] else EXIT_VALIDATION_FAILED)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""`cassis projects` — discover the projects an API key can reach.
|
|
2
|
+
|
|
3
|
+
Every other CI route is project-scoped, so the very first thing a pipeline or
|
|
4
|
+
checkout agent needs is a project id. `projects list` answers that from the
|
|
5
|
+
terminal instead of fishing the UUID out of a webapp URL.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
from cassis_cli.api import DEFAULT_API_URL, ApiError, AuthError, get_projects
|
|
15
|
+
from cassis_cli.common import EXIT_OK, EXIT_TRANSPORT, require_api_key
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(help="Discover the projects this API key can reach.")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command(name="list")
|
|
21
|
+
def list_projects(
|
|
22
|
+
api_key: Optional[str] = typer.Option(
|
|
23
|
+
None,
|
|
24
|
+
"--api-key",
|
|
25
|
+
envvar="CASSIS_API_KEY",
|
|
26
|
+
help="Cassis API key (sk-k6-...). Create one in Organization settings -> API keys.",
|
|
27
|
+
),
|
|
28
|
+
api_url: str = typer.Option(
|
|
29
|
+
DEFAULT_API_URL,
|
|
30
|
+
"--api-url",
|
|
31
|
+
envvar="CASSIS_API_URL",
|
|
32
|
+
help="Cassis API base URL.",
|
|
33
|
+
),
|
|
34
|
+
json_output: bool = typer.Option(False, "--json", help="Print the raw JSON response."),
|
|
35
|
+
) -> None:
|
|
36
|
+
"""List the projects available to the API key.
|
|
37
|
+
|
|
38
|
+
Prints each project's id (what --project and CASSIS_PROJECT_ID take), name,
|
|
39
|
+
published ontology version, and data-source dialect. A schema-only source
|
|
40
|
+
(no connection) is marked "not executable": SQL is generated but never run.
|
|
41
|
+
Exits 0 on success, 2 on usage errors, 3 on transport/API errors.
|
|
42
|
+
"""
|
|
43
|
+
api_key = require_api_key(api_key)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
projects = get_projects(api_url=api_url, api_key=api_key)
|
|
47
|
+
except (AuthError, ApiError) as exc:
|
|
48
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
49
|
+
raise typer.Exit(EXIT_TRANSPORT) from exc
|
|
50
|
+
|
|
51
|
+
if json_output:
|
|
52
|
+
typer.echo(json.dumps(projects, indent=2))
|
|
53
|
+
raise typer.Exit(EXIT_OK)
|
|
54
|
+
|
|
55
|
+
if not projects:
|
|
56
|
+
typer.echo("No projects visible to this API key.")
|
|
57
|
+
raise typer.Exit(EXIT_OK)
|
|
58
|
+
|
|
59
|
+
for project in projects:
|
|
60
|
+
version = project.get("published_version")
|
|
61
|
+
version_text = f"v{version}" if version is not None else "unpublished"
|
|
62
|
+
source = project.get("data_source")
|
|
63
|
+
if source:
|
|
64
|
+
dialect = source.get("sql_dialect") or "?"
|
|
65
|
+
source_text = dialect if source.get("is_executable") else f"{dialect}, not executable"
|
|
66
|
+
else:
|
|
67
|
+
source_text = "no data source"
|
|
68
|
+
typer.echo(f"{project['id']} {project['name']} ({version_text}; {source_text})")
|
|
69
|
+
raise typer.Exit(EXIT_OK)
|