cassis-cli 1.1.1__tar.gz → 1.3.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.1.1 → cassis_cli-1.3.0}/PKG-INFO +44 -6
- {cassis_cli-1.1.1 → cassis_cli-1.3.0}/README.md +43 -5
- {cassis_cli-1.1.1 → cassis_cli-1.3.0}/cassis_cli/__init__.py +1 -1
- {cassis_cli-1.1.1 → cassis_cli-1.3.0}/cassis_cli/api.py +218 -2
- {cassis_cli-1.1.1 → cassis_cli-1.3.0}/cassis_cli/common.py +11 -4
- {cassis_cli-1.1.1 → cassis_cli-1.3.0}/cassis_cli/eval.py +176 -7
- {cassis_cli-1.1.1 → cassis_cli-1.3.0}/cassis_cli/guide.py +1 -1
- {cassis_cli-1.1.1 → cassis_cli-1.3.0}/cassis_cli/main.py +8 -0
- {cassis_cli-1.1.1 → cassis_cli-1.3.0}/cassis_cli/ontology.py +43 -3
- {cassis_cli-1.1.1 → cassis_cli-1.3.0}/cassis_cli/ontology_design_guide.md +10 -0
- cassis_cli-1.3.0/cassis_cli/projects.py +69 -0
- cassis_cli-1.3.0/cassis_cli/schema.py +295 -0
- cassis_cli-1.3.0/cassis_cli/status.py +184 -0
- cassis_cli-1.3.0/cassis_cli/verify.py +123 -0
- {cassis_cli-1.1.1 → cassis_cli-1.3.0}/pyproject.toml +1 -1
- {cassis_cli-1.1.1 → cassis_cli-1.3.0}/LICENSE +0 -0
- {cassis_cli-1.1.1 → cassis_cli-1.3.0}/NOTICE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cassis-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.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,8 @@ 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.
|
|
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), never a failed check.
|
|
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.
|
|
28
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.
|
|
29
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.
|
|
30
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).
|
|
@@ -33,6 +34,11 @@ Run Cassis actions from your CI pipelines:
|
|
|
33
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.
|
|
34
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.
|
|
35
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
|
+
- `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.
|
|
36
42
|
|
|
37
43
|
## Install
|
|
38
44
|
|
|
@@ -54,7 +60,7 @@ The ontology tree under `<base-path>` (default `cassis/`) is:
|
|
|
54
60
|
|
|
55
61
|
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
56
62
|
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
57
|
-
3. For `pull`, `upload`, `eval run`, `ontology test`, and `eval add-case
|
|
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).
|
|
58
64
|
|
|
59
65
|
## Usage
|
|
60
66
|
|
|
@@ -91,6 +97,9 @@ cassis eval run --project ...
|
|
|
91
97
|
# Run against an existing Cassis ontology branch, or the unpublished ontology:
|
|
92
98
|
cassis eval run --project ... --branch feature-x
|
|
93
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
|
+
|
|
94
103
|
# Start the run and return immediately (poll in the webapp):
|
|
95
104
|
cassis eval run --project ... --no-wait
|
|
96
105
|
|
|
@@ -101,6 +110,34 @@ cassis ontology test --project ... -q "How much was refunded last month?" -q "Ne
|
|
|
101
110
|
# Add a gold case to the eval suite (rejected if the exact question already exists):
|
|
102
111
|
cassis eval add-case --project ... -q "How much was refunded last month?" \
|
|
103
112
|
--gold-sql "SELECT SUM(refunded_cents) / 100.0 FROM public.orders WHERE ..."
|
|
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
|
+
|
|
118
|
+
# List the suite's cases (id + question; --json adds the gold SQL), then prune one:
|
|
119
|
+
cassis eval list-cases --project ...
|
|
120
|
+
cassis eval delete-case 019f0000-0000-7000-8000-0000000000ca --project ...
|
|
121
|
+
|
|
122
|
+
# Pull the source schema into <base-path>/.schema.json (gitignored local snapshot):
|
|
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
|
|
104
141
|
```
|
|
105
142
|
|
|
106
143
|
Configuration (flags take precedence over env vars):
|
|
@@ -110,9 +147,10 @@ Configuration (flags take precedence over env vars):
|
|
|
110
147
|
| `--api-key` | `CASSIS_API_KEY` | — (required) |
|
|
111
148
|
| `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
|
|
112
149
|
| `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
|
|
113
|
-
| `--project` (pull, upload, eval run, eval add-case, test) | `CASSIS_PROJECT_ID` |
|
|
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) |
|
|
114
151
|
|
|
115
|
-
`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
|
|
116
154
|
to the branch name from the CI environment or the local git checkout; rejected
|
|
117
155
|
with `--branch`, whose runs are labelled with the branch name), `--wait/--no-wait`, `--poll-interval` (5 s),
|
|
118
156
|
`--timeout` (30 min — the run keeps going server-side if the CLI stops waiting),
|
|
@@ -139,7 +177,7 @@ cassis ontology fmt --check
|
|
|
139
177
|
| Code | Meaning |
|
|
140
178
|
| ---- | ------------------------------------------------------------------------------ |
|
|
141
179
|
| 0 | Ontology 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) |
|
|
142
|
-
| 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) |
|
|
180
|
+
| 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) |
|
|
143
181
|
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits) |
|
|
144
182
|
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, out of credits, or `--timeout` reached |
|
|
145
183
|
|
|
@@ -2,7 +2,8 @@
|
|
|
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.
|
|
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), never a failed check.
|
|
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.
|
|
6
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.
|
|
7
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.
|
|
8
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).
|
|
@@ -11,6 +12,11 @@ Run Cassis actions from your CI pipelines:
|
|
|
11
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.
|
|
12
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.
|
|
13
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
|
+
- `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.
|
|
14
20
|
|
|
15
21
|
## Install
|
|
16
22
|
|
|
@@ -32,7 +38,7 @@ The ontology tree under `<base-path>` (default `cassis/`) is:
|
|
|
32
38
|
|
|
33
39
|
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
34
40
|
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
35
|
-
3. For `pull`, `upload`, `eval run`, `ontology test`, and `eval add-case
|
|
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).
|
|
36
42
|
|
|
37
43
|
## Usage
|
|
38
44
|
|
|
@@ -69,6 +75,9 @@ cassis eval run --project ...
|
|
|
69
75
|
# Run against an existing Cassis ontology branch, or the unpublished ontology:
|
|
70
76
|
cassis eval run --project ... --branch feature-x
|
|
71
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
|
+
|
|
72
81
|
# Start the run and return immediately (poll in the webapp):
|
|
73
82
|
cassis eval run --project ... --no-wait
|
|
74
83
|
|
|
@@ -79,6 +88,34 @@ cassis ontology test --project ... -q "How much was refunded last month?" -q "Ne
|
|
|
79
88
|
# Add a gold case to the eval suite (rejected if the exact question already exists):
|
|
80
89
|
cassis eval add-case --project ... -q "How much was refunded last month?" \
|
|
81
90
|
--gold-sql "SELECT SUM(refunded_cents) / 100.0 FROM public.orders WHERE ..."
|
|
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
|
+
|
|
96
|
+
# List the suite's cases (id + question; --json adds the gold SQL), then prune one:
|
|
97
|
+
cassis eval list-cases --project ...
|
|
98
|
+
cassis eval delete-case 019f0000-0000-7000-8000-0000000000ca --project ...
|
|
99
|
+
|
|
100
|
+
# Pull the source schema into <base-path>/.schema.json (gitignored local snapshot):
|
|
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
|
|
82
119
|
```
|
|
83
120
|
|
|
84
121
|
Configuration (flags take precedence over env vars):
|
|
@@ -88,9 +125,10 @@ Configuration (flags take precedence over env vars):
|
|
|
88
125
|
| `--api-key` | `CASSIS_API_KEY` | — (required) |
|
|
89
126
|
| `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
|
|
90
127
|
| `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
|
|
91
|
-
| `--project` (pull, upload, eval run, eval add-case, test) | `CASSIS_PROJECT_ID` |
|
|
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) |
|
|
92
129
|
|
|
93
|
-
`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
|
|
94
132
|
to the branch name from the CI environment or the local git checkout; rejected
|
|
95
133
|
with `--branch`, whose runs are labelled with the branch name), `--wait/--no-wait`, `--poll-interval` (5 s),
|
|
96
134
|
`--timeout` (30 min — the run keeps going server-side if the CLI stops waiting),
|
|
@@ -117,7 +155,7 @@ cassis ontology fmt --check
|
|
|
117
155
|
| Code | Meaning |
|
|
118
156
|
| ---- | ------------------------------------------------------------------------------ |
|
|
119
157
|
| 0 | Ontology 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) |
|
|
120
|
-
| 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) |
|
|
158
|
+
| 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) |
|
|
121
159
|
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits) |
|
|
122
160
|
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, out of credits, or `--timeout` reached |
|
|
123
161
|
|
|
@@ -134,10 +134,19 @@ def post_ontology_check(
|
|
|
134
134
|
api_url: str,
|
|
135
135
|
api_key: str,
|
|
136
136
|
files: dict[str, str],
|
|
137
|
+
project_id: Optional[str] = None,
|
|
137
138
|
transport: Optional[httpx.BaseTransport] = None,
|
|
138
139
|
) -> dict[str, Any]:
|
|
139
|
-
"""POST the ontology tree to
|
|
140
|
-
|
|
140
|
+
"""POST the ontology tree to the check endpoint and return the response body.
|
|
141
|
+
|
|
142
|
+
With ``project_id``, calls the project-scoped route, which additionally
|
|
143
|
+
cross-checks the tree against the project's source schema and returns
|
|
144
|
+
advisory ``warnings``; without it, the pure tree check.
|
|
145
|
+
"""
|
|
146
|
+
if project_id:
|
|
147
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/ontology/check"
|
|
148
|
+
else:
|
|
149
|
+
url = api_url.rstrip("/") + "/api/ci/ontology-check"
|
|
141
150
|
try:
|
|
142
151
|
with _client(transport=transport) as client:
|
|
143
152
|
response = client.post(
|
|
@@ -150,6 +159,8 @@ def post_ontology_check(
|
|
|
150
159
|
|
|
151
160
|
if response.status_code == 401:
|
|
152
161
|
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
162
|
+
if project_id and response.status_code in (403, 404):
|
|
163
|
+
raise _project_scope_error(response)
|
|
153
164
|
if response.status_code >= 400:
|
|
154
165
|
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
155
166
|
result = _parse_json_response(response, url)
|
|
@@ -232,6 +243,160 @@ def get_ontology_export(
|
|
|
232
243
|
return result["files"]
|
|
233
244
|
|
|
234
245
|
|
|
246
|
+
def get_projects(
|
|
247
|
+
*,
|
|
248
|
+
api_url: str,
|
|
249
|
+
api_key: str,
|
|
250
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
251
|
+
) -> list[dict[str, Any]]:
|
|
252
|
+
"""GET /api/ci/projects and return the project list."""
|
|
253
|
+
url = api_url.rstrip("/") + "/api/ci/projects"
|
|
254
|
+
try:
|
|
255
|
+
with _client(transport=transport) as client:
|
|
256
|
+
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
257
|
+
except httpx.HTTPError as exc:
|
|
258
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
259
|
+
|
|
260
|
+
if response.status_code == 401:
|
|
261
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
262
|
+
if response.status_code >= 400:
|
|
263
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
264
|
+
result = _parse_json_response(response, url)
|
|
265
|
+
if not isinstance(result, list) or not all(isinstance(p, dict) and "id" in p and "name" in p for p in result):
|
|
266
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
267
|
+
return result
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def get_project_status(
|
|
271
|
+
*,
|
|
272
|
+
api_url: str,
|
|
273
|
+
api_key: str,
|
|
274
|
+
project_id: str,
|
|
275
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
276
|
+
) -> dict[str, Any]:
|
|
277
|
+
"""GET /api/ci/projects/{project_id}/status and return the status record."""
|
|
278
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/status"
|
|
279
|
+
try:
|
|
280
|
+
with _client(transport=transport) as client:
|
|
281
|
+
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
282
|
+
except httpx.HTTPError as exc:
|
|
283
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
284
|
+
|
|
285
|
+
if response.status_code == 401:
|
|
286
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
287
|
+
if response.status_code in (403, 404):
|
|
288
|
+
raise _project_scope_error(response)
|
|
289
|
+
if response.status_code >= 400:
|
|
290
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
291
|
+
result = _parse_json_response(response, url)
|
|
292
|
+
if not isinstance(result, dict) or "has_unpublished_changes" not in result:
|
|
293
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
294
|
+
return result
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class NoSourceSchemaError(ApiError):
|
|
298
|
+
"""The project's data source has no introspected schema to pull."""
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def get_schema_export(
|
|
302
|
+
*,
|
|
303
|
+
api_url: str,
|
|
304
|
+
api_key: str,
|
|
305
|
+
project_id: str,
|
|
306
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
307
|
+
) -> dict[str, Any]:
|
|
308
|
+
"""GET /api/ci/projects/{project_id}/schema and return the response body."""
|
|
309
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/schema"
|
|
310
|
+
try:
|
|
311
|
+
with _client(transport=transport) as client:
|
|
312
|
+
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
313
|
+
except httpx.HTTPError as exc:
|
|
314
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
315
|
+
|
|
316
|
+
if response.status_code == 401:
|
|
317
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
318
|
+
# "No source schema" is the marker the server's export endpoint puts in its
|
|
319
|
+
# 404 detail (backend endpoints/ci.py::export_source_schema — reworded only
|
|
320
|
+
# with a paired CLI release). Without this routing, a schema-less project
|
|
321
|
+
# would surface as the misleading "check --project / key access" hint below.
|
|
322
|
+
if response.status_code == 404 and "No source schema" in response.text:
|
|
323
|
+
raise NoSourceSchemaError(str(_detail_or_text(response)))
|
|
324
|
+
if response.status_code in (400, 403, 404):
|
|
325
|
+
raise _project_scope_error(response)
|
|
326
|
+
if response.status_code >= 400:
|
|
327
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
328
|
+
result = _parse_json_response(response, url)
|
|
329
|
+
if (
|
|
330
|
+
not isinstance(result, dict)
|
|
331
|
+
or not isinstance(result.get("tables"), list)
|
|
332
|
+
or not isinstance(result.get("schema_version"), dict)
|
|
333
|
+
):
|
|
334
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
335
|
+
return result
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class SourceChangeConflictError(ApiError):
|
|
339
|
+
"""A concurrent detection run is already active, or the project has a connected data source."""
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def post_detect_from_ddl(
|
|
343
|
+
*,
|
|
344
|
+
api_url: str,
|
|
345
|
+
api_key: str,
|
|
346
|
+
project_id: str,
|
|
347
|
+
ddl: str,
|
|
348
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
349
|
+
) -> dict[str, Any]:
|
|
350
|
+
"""POST /api/ci/projects/{project_id}/source-changes/detect-from-ddl and return the run record."""
|
|
351
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/source-changes/detect-from-ddl"
|
|
352
|
+
try:
|
|
353
|
+
with _client(transport=transport) as client:
|
|
354
|
+
response = client.post(url, json={"ddl": ddl}, headers={"Authorization": f"Bearer {api_key}"})
|
|
355
|
+
except httpx.HTTPError as exc:
|
|
356
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
357
|
+
|
|
358
|
+
if response.status_code == 401:
|
|
359
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
360
|
+
if response.status_code == 409:
|
|
361
|
+
raise SourceChangeConflictError(str(_detail_or_text(response)))
|
|
362
|
+
if response.status_code in (403, 404):
|
|
363
|
+
raise _project_scope_error(response)
|
|
364
|
+
if response.status_code >= 400:
|
|
365
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
366
|
+
result = _parse_json_response(response, url)
|
|
367
|
+
if not isinstance(result, dict) or "run_id" not in result:
|
|
368
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
369
|
+
return result
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def get_source_change_run(
|
|
373
|
+
*,
|
|
374
|
+
api_url: str,
|
|
375
|
+
api_key: str,
|
|
376
|
+
project_id: str,
|
|
377
|
+
run_id: str,
|
|
378
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
379
|
+
) -> dict[str, Any]:
|
|
380
|
+
"""GET /api/ci/projects/{project_id}/source-changes/runs/{run_id} and return the run record."""
|
|
381
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/source-changes/runs/{run_id}"
|
|
382
|
+
try:
|
|
383
|
+
with _client(transport=transport) as client:
|
|
384
|
+
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
385
|
+
except httpx.HTTPError as exc:
|
|
386
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
387
|
+
|
|
388
|
+
if response.status_code == 401:
|
|
389
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
390
|
+
if response.status_code in (403, 404):
|
|
391
|
+
raise _project_scope_error(response)
|
|
392
|
+
if response.status_code >= 400:
|
|
393
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
394
|
+
result = _parse_json_response(response, url)
|
|
395
|
+
if not isinstance(result, dict) or "run_id" not in result:
|
|
396
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
397
|
+
return result
|
|
398
|
+
|
|
399
|
+
|
|
235
400
|
def post_eval_run_start(
|
|
236
401
|
*,
|
|
237
402
|
api_url: str,
|
|
@@ -240,6 +405,7 @@ def post_eval_run_start(
|
|
|
240
405
|
files: Optional[dict[str, str]] = None,
|
|
241
406
|
branch: Optional[str] = None,
|
|
242
407
|
label: Optional[str] = None,
|
|
408
|
+
case_ids: Optional[list[str]] = None,
|
|
243
409
|
transport: Optional[httpx.BaseTransport] = None,
|
|
244
410
|
) -> dict[str, Any]:
|
|
245
411
|
"""POST to /api/ci/projects/{project_id}/eval/runs and return the run record."""
|
|
@@ -251,6 +417,8 @@ def post_eval_run_start(
|
|
|
251
417
|
body["branch"] = branch
|
|
252
418
|
if label is not None:
|
|
253
419
|
body["label"] = label
|
|
420
|
+
if case_ids is not None:
|
|
421
|
+
body["test_case_ids"] = case_ids
|
|
254
422
|
try:
|
|
255
423
|
with _client(transport=transport) as client:
|
|
256
424
|
response = client.post(url, json=body, headers={"Authorization": f"Bearer {api_key}"})
|
|
@@ -321,6 +489,54 @@ def post_eval_case_create(
|
|
|
321
489
|
return result
|
|
322
490
|
|
|
323
491
|
|
|
492
|
+
class EvalCaseNotFoundError(ApiError):
|
|
493
|
+
"""The project has no current eval case with this id."""
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def get_eval_cases(
|
|
497
|
+
*,
|
|
498
|
+
api_url: str,
|
|
499
|
+
api_key: str,
|
|
500
|
+
project_id: str,
|
|
501
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
502
|
+
) -> list[dict[str, Any]]:
|
|
503
|
+
"""GET /api/ci/projects/{project_id}/eval/cases and return the case list."""
|
|
504
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/eval/cases"
|
|
505
|
+
result = _get_eval_json(url, api_key, transport)
|
|
506
|
+
if not isinstance(result, list):
|
|
507
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
508
|
+
return result
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def delete_eval_case(
|
|
512
|
+
*,
|
|
513
|
+
api_url: str,
|
|
514
|
+
api_key: str,
|
|
515
|
+
project_id: str,
|
|
516
|
+
case_id: str,
|
|
517
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
518
|
+
) -> None:
|
|
519
|
+
"""DELETE /api/ci/projects/{project_id}/eval/cases/{case_id}."""
|
|
520
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/eval/cases/{case_id}"
|
|
521
|
+
try:
|
|
522
|
+
with _client(transport=transport) as client:
|
|
523
|
+
response = client.delete(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
524
|
+
except httpx.HTTPError as exc:
|
|
525
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
526
|
+
|
|
527
|
+
if response.status_code == 401:
|
|
528
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
529
|
+
# Exact-match wire contract with the DELETE endpoint's 404 detail (see
|
|
530
|
+
# `delete_eval_case` in backend/app/endpoints/ci.py): it distinguishes a
|
|
531
|
+
# missing case (exit 1) from a project-scope 404 (exit 3).
|
|
532
|
+
if response.status_code == 404 and _detail_or_text(response) == "Eval case not found":
|
|
533
|
+
raise EvalCaseNotFoundError(f"No current eval case {case_id} in this project (already deleted, or wrong id?).")
|
|
534
|
+
if response.status_code in (403, 404):
|
|
535
|
+
raise _project_scope_error(response)
|
|
536
|
+
if response.status_code >= 400:
|
|
537
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
538
|
+
|
|
539
|
+
|
|
324
540
|
def _get_eval_json(url: str, api_key: str, transport: Optional[httpx.BaseTransport]) -> Any:
|
|
325
541
|
"""GET an eval-run URL with the shared error mapping."""
|
|
326
542
|
try:
|
|
@@ -93,20 +93,27 @@ def read_project_id_from_dir(ontology_dir: Path) -> Optional[str]:
|
|
|
93
93
|
return None
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
def resolve_project_id(
|
|
96
|
+
def resolve_project_id(
|
|
97
|
+
project_id: Optional[str], ontology_dir: Path, *, optional: bool = False, quiet: bool = False
|
|
98
|
+
) -> Optional[str]:
|
|
97
99
|
"""Resolve the target project id, defaulting to the checkout's ``project.yml``.
|
|
98
100
|
|
|
99
101
|
Precedence: an explicit ``--project`` / ``CASSIS_PROJECT_ID`` wins; otherwise
|
|
100
102
|
the ``project_id`` recorded in ``<base-path>/project.yml`` (written by
|
|
101
103
|
``pull`` / publish) is used, and where it came from is noted on stderr so a
|
|
102
|
-
stale value in a copied repo is visible
|
|
103
|
-
|
|
104
|
+
stale value in a copied repo is visible (``quiet`` suppresses the note for
|
|
105
|
+
machine-readable output). Exits 2 (usage) when the value isn't a UUID, or —
|
|
106
|
+
unless ``optional`` — when no value is available at all; with ``optional``,
|
|
107
|
+
an unbound checkout returns None (``check`` falls back to the project-less
|
|
108
|
+
validation).
|
|
104
109
|
"""
|
|
105
110
|
from_file = False
|
|
106
111
|
if not project_id:
|
|
107
112
|
project_id = read_project_id_from_dir(ontology_dir)
|
|
108
113
|
from_file = project_id is not None
|
|
109
114
|
if not project_id:
|
|
115
|
+
if optional:
|
|
116
|
+
return None
|
|
110
117
|
typer.secho(
|
|
111
118
|
f"No project. Pass --project (or set CASSIS_PROJECT_ID), or run in a checkout whose "
|
|
112
119
|
f"{ontology_dir.name}/project.yml records it (written by `cassis ontology pull` or a publish).",
|
|
@@ -119,7 +126,7 @@ def resolve_project_id(project_id: Optional[str], ontology_dir: Path) -> str:
|
|
|
119
126
|
except ValueError:
|
|
120
127
|
typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
|
|
121
128
|
raise typer.Exit(EXIT_USAGE)
|
|
122
|
-
if from_file:
|
|
129
|
+
if from_file and not quiet:
|
|
123
130
|
typer.secho(f"Using project {project_id} from {ontology_dir.name}/project.yml.", fg=typer.colors.CYAN, err=True)
|
|
124
131
|
return project_id
|
|
125
132
|
|