cassis-cli 1.6.0__tar.gz → 2.0.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.6.0 → cassis_cli-2.0.0}/PKG-INFO +15 -9
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/README.md +14 -8
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/cassis_cli/api.py +227 -107
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/cassis_cli/common.py +160 -1
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/cassis_cli/eval.py +57 -51
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/cassis_cli/guide.py +1 -1
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/cassis_cli/issues.py +183 -2
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/cassis_cli/main.py +0 -2
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/cassis_cli/ontology.py +10 -51
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/cassis_cli/ontology_design_guide.md +9 -0
- cassis_cli-2.0.0/cassis_cli/schema.py +792 -0
- cassis_cli-2.0.0/cassis_cli/schema_plan.py +130 -0
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/cassis_cli/status.py +49 -32
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/pyproject.toml +1 -1
- cassis_cli-1.6.0/cassis_cli/schema.py +0 -315
- cassis_cli-1.6.0/cassis_cli/source_changes.py +0 -191
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/LICENSE +0 -0
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/NOTICE +0 -0
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/cassis_cli/__init__.py +0 -0
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/cassis_cli/projects.py +0 -0
- {cassis_cli-1.6.0 → cassis_cli-2.0.0}/cassis_cli/verify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cassis-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Validate, test and evaluate your Cassis ontology from your terminal, then publish it
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -35,11 +35,10 @@ Validate, test and evaluate your ontology from your terminal, then publish it. T
|
|
|
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
|
|
38
|
+
- `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 ontology changes Cassis will make (every change on a table placed in the ontology, with everything a drop takes with it) and warnings, terraform-style. `cassis schema apply <ddl>` (or `--plan <id>`) writes the resulting ontology files into the local checkout, app untouched, for review with `git diff`. `cassis schema push <ddl> [--publish]` pushes the new schema and the local ontology 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.
|
|
39
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,
|
|
40
|
+
- `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 ontology — instead of watching the GitHub Actions tab.
|
|
41
41
|
- `cassis issues` triages the issues Cassis raised on the project — what it found wrong while answering questions (an ontology gap, missing data) — without leaving the checkout: `issues list` (filterable by status, impact and cause), `issues show <id>` for the diagnosis, suggested action and the occurrences behind it, `issues evidence <id> <occurrence-id>` for what the agent actually saw, and `issues resolve` / `dismiss` / `reopen` once you've acted on it.
|
|
42
|
-
- `cassis source-changes` reads the project's Data source review queue — the schema drift Cassis detected between the source and what the ontology tracks: `source-changes list` (paginated, pending by default, breaking severity flagged) and `source-changes show <id>` for one change's impact references and suggested edit. Read-only: approving or dismissing stays in the webapp; fix headlessly by editing the ontology files and opening a pull request.
|
|
43
42
|
- `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.
|
|
44
43
|
|
|
45
44
|
## Install
|
|
@@ -125,13 +124,20 @@ cassis eval delete-case 019f0000-0000-7000-8000-0000000000ca --project ...
|
|
|
125
124
|
# Pull the source schema into <base-path>/.schema.json (gitignored local snapshot):
|
|
126
125
|
cassis schema pull
|
|
127
126
|
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
cassis schema
|
|
127
|
+
# Preview, apply locally and push a schema update from a DDL file (DDL-only projects):
|
|
128
|
+
cassis schema plan schema.sql --complete
|
|
129
|
+
cassis schema apply schema.sql --complete # writes cassis/ locally
|
|
130
|
+
cassis schema push schema.sql --complete --yes # schema + ontology to the app
|
|
131
|
+
cassis schema plan --warehouse # warehouse-connected projects: introspect instead
|
|
131
132
|
|
|
132
133
|
# List the projects the API key can reach (id, name, published version, dialect):
|
|
133
134
|
cassis projects list
|
|
134
135
|
|
|
136
|
+
# Refresh the issues from the conversations nobody has analyzed yet (the same pass as the
|
|
137
|
+
# webapp's "Analyze conversations" button; waits for the result, --no-wait returns at once,
|
|
138
|
+
# and Ctrl-C cancels the run server-side and exits 130):
|
|
139
|
+
cassis issues analyze
|
|
140
|
+
|
|
135
141
|
# Triage the issues Cassis raised (filter by --status/--impact/--cause; --json for raw output):
|
|
136
142
|
cassis issues list --status open
|
|
137
143
|
cassis issues show 019f0000-0000-7000-8000-0000000000e1
|
|
@@ -190,9 +196,9 @@ cassis ontology fmt --check
|
|
|
190
196
|
| Code | Meaning |
|
|
191
197
|
| ---- | ------------------------------------------------------------------------------ |
|
|
192
198
|
| 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) |
|
|
193
|
-
| 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;
|
|
199
|
+
| 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: 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)) |
|
|
194
200
|
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits) |
|
|
195
|
-
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another eval run already active, out of credits, or `--timeout` reached |
|
|
201
|
+
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another eval run or issue analysis already active, out of credits, or `--timeout` reached |
|
|
196
202
|
|
|
197
203
|
Commands that send the local tree (`check`, `fmt`, `upload`, `eval run`, `test`) accept up to
|
|
198
204
|
20,000 ontology files / 100 MB total (path + content bytes) — sized for ontologies of
|
|
@@ -13,11 +13,10 @@ Validate, test and evaluate your ontology from your terminal, then publish it. T
|
|
|
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
|
|
16
|
+
- `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 ontology changes Cassis will make (every change on a table placed in the ontology, with everything a drop takes with it) and warnings, terraform-style. `cassis schema apply <ddl>` (or `--plan <id>`) writes the resulting ontology files into the local checkout, app untouched, for review with `git diff`. `cassis schema push <ddl> [--publish]` pushes the new schema and the local ontology 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.
|
|
17
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,
|
|
18
|
+
- `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 ontology — instead of watching the GitHub Actions tab.
|
|
19
19
|
- `cassis issues` triages the issues Cassis raised on the project — what it found wrong while answering questions (an ontology gap, missing data) — without leaving the checkout: `issues list` (filterable by status, impact and cause), `issues show <id>` for the diagnosis, suggested action and the occurrences behind it, `issues evidence <id> <occurrence-id>` for what the agent actually saw, and `issues resolve` / `dismiss` / `reopen` once you've acted on it.
|
|
20
|
-
- `cassis source-changes` reads the project's Data source review queue — the schema drift Cassis detected between the source and what the ontology tracks: `source-changes list` (paginated, pending by default, breaking severity flagged) and `source-changes show <id>` for one change's impact references and suggested edit. Read-only: approving or dismissing stays in the webapp; fix headlessly by editing the ontology files and opening a pull request.
|
|
21
20
|
- `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.
|
|
22
21
|
|
|
23
22
|
## Install
|
|
@@ -103,13 +102,20 @@ cassis eval delete-case 019f0000-0000-7000-8000-0000000000ca --project ...
|
|
|
103
102
|
# Pull the source schema into <base-path>/.schema.json (gitignored local snapshot):
|
|
104
103
|
cassis schema pull
|
|
105
104
|
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
cassis schema
|
|
105
|
+
# Preview, apply locally and push a schema update from a DDL file (DDL-only projects):
|
|
106
|
+
cassis schema plan schema.sql --complete
|
|
107
|
+
cassis schema apply schema.sql --complete # writes cassis/ locally
|
|
108
|
+
cassis schema push schema.sql --complete --yes # schema + ontology to the app
|
|
109
|
+
cassis schema plan --warehouse # warehouse-connected projects: introspect instead
|
|
109
110
|
|
|
110
111
|
# List the projects the API key can reach (id, name, published version, dialect):
|
|
111
112
|
cassis projects list
|
|
112
113
|
|
|
114
|
+
# Refresh the issues from the conversations nobody has analyzed yet (the same pass as the
|
|
115
|
+
# webapp's "Analyze conversations" button; waits for the result, --no-wait returns at once,
|
|
116
|
+
# and Ctrl-C cancels the run server-side and exits 130):
|
|
117
|
+
cassis issues analyze
|
|
118
|
+
|
|
113
119
|
# Triage the issues Cassis raised (filter by --status/--impact/--cause; --json for raw output):
|
|
114
120
|
cassis issues list --status open
|
|
115
121
|
cassis issues show 019f0000-0000-7000-8000-0000000000e1
|
|
@@ -168,9 +174,9 @@ cassis ontology fmt --check
|
|
|
168
174
|
| Code | Meaning |
|
|
169
175
|
| ---- | ------------------------------------------------------------------------------ |
|
|
170
176
|
| 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) |
|
|
171
|
-
| 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;
|
|
177
|
+
| 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: 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)) |
|
|
172
178
|
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits) |
|
|
173
|
-
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another eval run already active, out of credits, or `--timeout` reached |
|
|
179
|
+
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another eval run or issue analysis already active, out of credits, or `--timeout` reached |
|
|
174
180
|
|
|
175
181
|
Commands that send the local tree (`check`, `fmt`, `upload`, `eval run`, `test`) accept up to
|
|
176
182
|
20,000 ontology files / 100 MB total (path + content bytes) — sized for ontologies of
|
|
@@ -353,11 +353,57 @@ def get_schema_export(
|
|
|
353
353
|
return result
|
|
354
354
|
|
|
355
355
|
|
|
356
|
-
class
|
|
357
|
-
"""
|
|
356
|
+
class SchemaPlanConflictError(ApiError):
|
|
357
|
+
"""409 from the schema-plan routes: a plan is already active, the project is connected, the plan is stale / expired / not ready."""
|
|
358
358
|
|
|
359
359
|
|
|
360
|
-
|
|
360
|
+
class SchemaPlanNotFoundError(ApiError):
|
|
361
|
+
"""404 with the server's exact detail `"Schema plan not found"` (a project-scope 404 is a different error)."""
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class ServerTooOldError(ApiError):
|
|
365
|
+
"""The server has no schema-plan routes (bare FastAPI 404): upgrade the server first."""
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
_SCHEMA_PLAN_NOT_FOUND = "Schema plan not found"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _raise_for_schema_plan_status(response: httpx.Response) -> None:
|
|
372
|
+
"""Map the schema-plan routes' error statuses to the CLI's exceptions; return on 2xx.
|
|
373
|
+
|
|
374
|
+
One ladder for every plan route (start, read, apply, checkout): the 409
|
|
375
|
+
conflict and the exact-detail 404 are wire contracts cassis-cli matches.
|
|
376
|
+
A bare FastAPI 404 means the server predates schema plans altogether.
|
|
377
|
+
"""
|
|
378
|
+
if response.status_code == 401:
|
|
379
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
380
|
+
if response.status_code == 409:
|
|
381
|
+
raise SchemaPlanConflictError(str(_detail_or_text(response)))
|
|
382
|
+
if response.status_code == 404:
|
|
383
|
+
detail = _detail_or_text(response)
|
|
384
|
+
if detail == _SCHEMA_PLAN_NOT_FOUND:
|
|
385
|
+
raise SchemaPlanNotFoundError(_SCHEMA_PLAN_NOT_FOUND)
|
|
386
|
+
if detail == "Not Found":
|
|
387
|
+
raise ServerTooOldError(
|
|
388
|
+
"This Cassis server has no schema plans yet: upgrade the server first, "
|
|
389
|
+
"or pin cassis-cli<2.0 to keep the previous `schema push`."
|
|
390
|
+
)
|
|
391
|
+
raise _project_scope_error(response)
|
|
392
|
+
if response.status_code == 403:
|
|
393
|
+
raise _project_scope_error(response)
|
|
394
|
+
if response.status_code >= 400:
|
|
395
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _schema_plan_response(response: httpx.Response, url: str) -> dict[str, Any]:
|
|
399
|
+
_raise_for_schema_plan_status(response)
|
|
400
|
+
result = _parse_json_response(response, url)
|
|
401
|
+
if not isinstance(result, dict) or "id" not in result or "status" not in result:
|
|
402
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
403
|
+
return result
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def post_schema_plan(
|
|
361
407
|
*,
|
|
362
408
|
api_url: str,
|
|
363
409
|
api_key: str,
|
|
@@ -366,10 +412,10 @@ def post_detect_from_ddl(
|
|
|
366
412
|
complete_source: bool = False,
|
|
367
413
|
transport: Optional[httpx.BaseTransport] = None,
|
|
368
414
|
) -> dict[str, Any]:
|
|
369
|
-
"""POST /api/ci/projects/{project_id}/
|
|
370
|
-
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/
|
|
415
|
+
"""POST /api/ci/projects/{project_id}/schema/plans and return the PLANNING plan record."""
|
|
416
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/schema/plans"
|
|
371
417
|
try:
|
|
372
|
-
with _client(transport=transport) as client:
|
|
418
|
+
with _client(timeout=ONTOLOGY_TREE_TIMEOUT_SECONDS, transport=transport) as client:
|
|
373
419
|
response = client.post(
|
|
374
420
|
url,
|
|
375
421
|
json={"ddl": ddl, "complete_source": complete_source},
|
|
@@ -377,49 +423,88 @@ def post_detect_from_ddl(
|
|
|
377
423
|
)
|
|
378
424
|
except httpx.HTTPError as exc:
|
|
379
425
|
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
426
|
+
return _schema_plan_response(response, url)
|
|
380
427
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
428
|
+
|
|
429
|
+
def post_schema_plan_warehouse(
|
|
430
|
+
*,
|
|
431
|
+
api_url: str,
|
|
432
|
+
api_key: str,
|
|
433
|
+
project_id: str,
|
|
434
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
435
|
+
) -> dict[str, Any]:
|
|
436
|
+
"""POST /api/ci/projects/{project_id}/schema/plans/warehouse and return the PLANNING plan record.
|
|
437
|
+
|
|
438
|
+
The server introspects the project's connected warehouse instead of parsing
|
|
439
|
+
a DDL; the plan is always whole-source.
|
|
440
|
+
"""
|
|
441
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/schema/plans/warehouse"
|
|
442
|
+
try:
|
|
443
|
+
with _client(transport=transport) as client:
|
|
444
|
+
response = client.post(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
445
|
+
except httpx.HTTPError as exc:
|
|
446
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
447
|
+
return _schema_plan_response(response, url)
|
|
393
448
|
|
|
394
449
|
|
|
395
|
-
def
|
|
450
|
+
def get_schema_plan(
|
|
396
451
|
*,
|
|
397
452
|
api_url: str,
|
|
398
453
|
api_key: str,
|
|
399
454
|
project_id: str,
|
|
400
|
-
|
|
455
|
+
plan_id: str,
|
|
401
456
|
transport: Optional[httpx.BaseTransport] = None,
|
|
402
457
|
) -> dict[str, Any]:
|
|
403
|
-
"""GET /api/ci/projects/{project_id}/
|
|
404
|
-
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/
|
|
458
|
+
"""GET /api/ci/projects/{project_id}/schema/plans/{plan_id} and return the plan record."""
|
|
459
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/schema/plans/{plan_id}"
|
|
405
460
|
try:
|
|
406
461
|
with _client(transport=transport) as client:
|
|
407
462
|
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
408
463
|
except httpx.HTTPError as exc:
|
|
409
464
|
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
465
|
+
return _schema_plan_response(response, url)
|
|
410
466
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
467
|
+
|
|
468
|
+
def get_schema_plan_checkout(
|
|
469
|
+
*,
|
|
470
|
+
api_url: str,
|
|
471
|
+
api_key: str,
|
|
472
|
+
project_id: str,
|
|
473
|
+
plan_id: str,
|
|
474
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
475
|
+
) -> dict[str, Any]:
|
|
476
|
+
"""GET /api/ci/projects/{project_id}/schema/plans/{plan_id}/checkout: the post-apply ontology tree."""
|
|
477
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/schema/plans/{plan_id}/checkout"
|
|
478
|
+
try:
|
|
479
|
+
with _client(timeout=ONTOLOGY_TREE_TIMEOUT_SECONDS, transport=transport) as client:
|
|
480
|
+
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
481
|
+
except httpx.HTTPError as exc:
|
|
482
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
483
|
+
_raise_for_schema_plan_status(response)
|
|
417
484
|
result = _parse_json_response(response, url)
|
|
418
|
-
if not isinstance(result, dict) or "
|
|
485
|
+
if not isinstance(result, dict) or not isinstance(result.get("files"), dict):
|
|
419
486
|
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
420
487
|
return result
|
|
421
488
|
|
|
422
489
|
|
|
490
|
+
def post_schema_plan_apply(
|
|
491
|
+
*,
|
|
492
|
+
api_url: str,
|
|
493
|
+
api_key: str,
|
|
494
|
+
project_id: str,
|
|
495
|
+
plan_id: str,
|
|
496
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
497
|
+
) -> dict[str, Any]:
|
|
498
|
+
"""POST /api/ci/projects/{project_id}/schema/plans/{plan_id}/apply and return the APPLYING plan record."""
|
|
499
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/schema/plans/{plan_id}/apply"
|
|
500
|
+
try:
|
|
501
|
+
with _client(transport=transport) as client:
|
|
502
|
+
response = client.post(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
503
|
+
except httpx.HTTPError as exc:
|
|
504
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
505
|
+
return _schema_plan_response(response, url)
|
|
506
|
+
|
|
507
|
+
|
|
423
508
|
def post_eval_run_start(
|
|
424
509
|
*,
|
|
425
510
|
api_url: str,
|
|
@@ -635,7 +720,11 @@ def post_eval_run_cancel(
|
|
|
635
720
|
|
|
636
721
|
|
|
637
722
|
class IssueNotFoundError(ApiError):
|
|
638
|
-
"""The project has no issue
|
|
723
|
+
"""The project has no issue, occurrence or issue-analysis run with this id.
|
|
724
|
+
|
|
725
|
+
Raised off the server's exact 404 detail, so it separates "this id doesn't
|
|
726
|
+
exist" from "this project isn't in scope for your key", which 404s too.
|
|
727
|
+
"""
|
|
639
728
|
|
|
640
729
|
|
|
641
730
|
def _get_issue_json(
|
|
@@ -645,8 +734,14 @@ def _get_issue_json(
|
|
|
645
734
|
*,
|
|
646
735
|
params: Optional[dict[str, str]] = None,
|
|
647
736
|
not_found_message: Optional[str] = None,
|
|
737
|
+
not_found_detail: str = "Issue not found",
|
|
648
738
|
) -> Any:
|
|
649
|
-
"""GET an issues URL with the shared error mapping.
|
|
739
|
+
"""GET an issues URL with the shared error mapping.
|
|
740
|
+
|
|
741
|
+
`not_found_detail` is the server's exact 404 detail for "this id does not
|
|
742
|
+
exist in the project" — `"Issue not found"` for issues and occurrences,
|
|
743
|
+
`"Issue-analysis run not found"` for analysis runs.
|
|
744
|
+
"""
|
|
650
745
|
try:
|
|
651
746
|
with _client(transport=transport) as client:
|
|
652
747
|
response = client.get(url, params=params, headers={"Authorization": f"Bearer {api_key}"})
|
|
@@ -657,8 +752,8 @@ def _get_issue_json(
|
|
|
657
752
|
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
658
753
|
# Exact-match wire contract with the issue endpoints' 404 detail (see the
|
|
659
754
|
# issue routes in backend/app/endpoints/ci.py): it distinguishes a missing
|
|
660
|
-
# issue or
|
|
661
|
-
if response.status_code == 404 and not_found_message and _detail_or_text(response) ==
|
|
755
|
+
# issue, occurrence or run (exit 1) from a project-scope 404 (exit 3).
|
|
756
|
+
if response.status_code == 404 and not_found_message and _detail_or_text(response) == not_found_detail:
|
|
662
757
|
raise IssueNotFoundError(not_found_message)
|
|
663
758
|
if response.status_code in (403, 404):
|
|
664
759
|
raise _project_scope_error(response)
|
|
@@ -761,80 +856,6 @@ def post_issue_status(
|
|
|
761
856
|
return result
|
|
762
857
|
|
|
763
858
|
|
|
764
|
-
class SourceChangeNotFoundError(ApiError):
|
|
765
|
-
"""The project has no source change with this id."""
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
def get_source_changes(
|
|
769
|
-
*,
|
|
770
|
-
api_url: str,
|
|
771
|
-
api_key: str,
|
|
772
|
-
project_id: str,
|
|
773
|
-
status: Optional[str] = None,
|
|
774
|
-
limit: Optional[int] = None,
|
|
775
|
-
offset: Optional[int] = None,
|
|
776
|
-
transport: Optional[httpx.BaseTransport] = None,
|
|
777
|
-
) -> dict[str, Any]:
|
|
778
|
-
"""GET /api/ci/projects/{project_id}/source-changes and return the {items, total} page."""
|
|
779
|
-
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/source-changes"
|
|
780
|
-
params: dict[str, str] = {}
|
|
781
|
-
if status:
|
|
782
|
-
params["status"] = status
|
|
783
|
-
if limit is not None:
|
|
784
|
-
params["limit"] = str(limit)
|
|
785
|
-
if offset is not None:
|
|
786
|
-
params["offset"] = str(offset)
|
|
787
|
-
try:
|
|
788
|
-
with _client(transport=transport) as client:
|
|
789
|
-
response = client.get(url, params=params or None, headers={"Authorization": f"Bearer {api_key}"})
|
|
790
|
-
except httpx.HTTPError as exc:
|
|
791
|
-
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
792
|
-
|
|
793
|
-
if response.status_code == 401:
|
|
794
|
-
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
795
|
-
if response.status_code in (403, 404):
|
|
796
|
-
raise _project_scope_error(response)
|
|
797
|
-
if response.status_code >= 400:
|
|
798
|
-
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
799
|
-
result = _parse_json_response(response, url)
|
|
800
|
-
if not isinstance(result, dict) or not isinstance(result.get("items"), list) or "total" not in result:
|
|
801
|
-
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
802
|
-
return result
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
def get_source_change(
|
|
806
|
-
*,
|
|
807
|
-
api_url: str,
|
|
808
|
-
api_key: str,
|
|
809
|
-
project_id: str,
|
|
810
|
-
change_id: str,
|
|
811
|
-
transport: Optional[httpx.BaseTransport] = None,
|
|
812
|
-
) -> dict[str, Any]:
|
|
813
|
-
"""GET /api/ci/projects/{project_id}/source-changes/{change_id} and return the full change."""
|
|
814
|
-
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/source-changes/{change_id}"
|
|
815
|
-
try:
|
|
816
|
-
with _client(transport=transport) as client:
|
|
817
|
-
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
818
|
-
except httpx.HTTPError as exc:
|
|
819
|
-
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
820
|
-
|
|
821
|
-
if response.status_code == 401:
|
|
822
|
-
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
823
|
-
# Exact-match wire contract with the source-change endpoints' 404 detail
|
|
824
|
-
# (see backend/app/endpoints/ci.py): it distinguishes a missing change
|
|
825
|
-
# (exit 1) from a project-scope 404 (exit 3).
|
|
826
|
-
if response.status_code == 404 and _detail_or_text(response) == "Source change not found":
|
|
827
|
-
raise SourceChangeNotFoundError(f"No source change {change_id} in project {project_id}.")
|
|
828
|
-
if response.status_code in (403, 404):
|
|
829
|
-
raise _project_scope_error(response)
|
|
830
|
-
if response.status_code >= 400:
|
|
831
|
-
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
832
|
-
result = _parse_json_response(response, url)
|
|
833
|
-
if not isinstance(result, dict) or "id" not in result:
|
|
834
|
-
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
835
|
-
return result
|
|
836
|
-
|
|
837
|
-
|
|
838
859
|
def post_ontology_fmt(
|
|
839
860
|
*,
|
|
840
861
|
api_url: str,
|
|
@@ -925,3 +946,102 @@ def post_ontology_test(
|
|
|
925
946
|
if not isinstance(result, dict) or "status" not in result:
|
|
926
947
|
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
927
948
|
return result
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
class IssueAnalysisActiveError(ApiError):
|
|
952
|
+
"""An issue-analysis run is already in flight for the project (HTTP 409)."""
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
class NothingToAnalyzeError(ApiError):
|
|
956
|
+
"""Every conversation on the project has already been analyzed (HTTP 422)."""
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
_ANALYSIS_RUN_KEYS = ("id", "status", "total_chats", "chats_analyzed", "occurrences_created", "issues_touched")
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def _check_analysis_run_shape(result: Any, url: str) -> dict[str, Any]:
|
|
963
|
+
if not isinstance(result, dict) or not all(key in result for key in _ANALYSIS_RUN_KEYS):
|
|
964
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
965
|
+
return result
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def post_issue_analysis_start(
|
|
969
|
+
*,
|
|
970
|
+
api_url: str,
|
|
971
|
+
api_key: str,
|
|
972
|
+
project_id: str,
|
|
973
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
974
|
+
) -> dict[str, Any]:
|
|
975
|
+
"""POST /api/ci/projects/{project_id}/issue-analysis/runs and return the run record."""
|
|
976
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/issue-analysis/runs"
|
|
977
|
+
try:
|
|
978
|
+
with _client(transport=transport) as client:
|
|
979
|
+
response = client.post(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
980
|
+
except httpx.HTTPError as exc:
|
|
981
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
982
|
+
|
|
983
|
+
if response.status_code == 401:
|
|
984
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
985
|
+
if response.status_code == 409:
|
|
986
|
+
raise IssueAnalysisActiveError(
|
|
987
|
+
"An issue analysis is already running for this project — wait for it to finish, "
|
|
988
|
+
"or cancel it from the webapp's Issues page."
|
|
989
|
+
)
|
|
990
|
+
if response.status_code == 422 and "analyz" in response.text.lower():
|
|
991
|
+
# The route takes no body, so its 422 is the server's "nothing to
|
|
992
|
+
# analyze" answer; any other 422 falls through to the generic error.
|
|
993
|
+
raise NothingToAnalyzeError("Nothing to analyze: every conversation on this project has already been analyzed.")
|
|
994
|
+
if response.status_code in (403, 404):
|
|
995
|
+
raise _project_scope_error(response)
|
|
996
|
+
if response.status_code >= 400:
|
|
997
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
998
|
+
return _check_analysis_run_shape(_parse_json_response(response, url), url)
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def get_issue_analysis_run(
|
|
1002
|
+
*,
|
|
1003
|
+
api_url: str,
|
|
1004
|
+
api_key: str,
|
|
1005
|
+
project_id: str,
|
|
1006
|
+
run_id: str,
|
|
1007
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
1008
|
+
) -> dict[str, Any]:
|
|
1009
|
+
"""GET /api/ci/projects/{project_id}/issue-analysis/runs/{run_id} and return the run record."""
|
|
1010
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/issue-analysis/runs/{run_id}"
|
|
1011
|
+
return _check_analysis_run_shape(
|
|
1012
|
+
_get_issue_json(
|
|
1013
|
+
url,
|
|
1014
|
+
api_key,
|
|
1015
|
+
transport,
|
|
1016
|
+
not_found_message=f"No issue-analysis run {run_id} in this project.",
|
|
1017
|
+
not_found_detail="Issue-analysis run not found",
|
|
1018
|
+
),
|
|
1019
|
+
url,
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def post_issue_analysis_cancel(
|
|
1024
|
+
*,
|
|
1025
|
+
api_url: str,
|
|
1026
|
+
api_key: str,
|
|
1027
|
+
project_id: str,
|
|
1028
|
+
run_id: str,
|
|
1029
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
1030
|
+
) -> None:
|
|
1031
|
+
"""POST /api/ci/projects/{project_id}/issue-analysis/runs/{run_id}/cancel."""
|
|
1032
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/issue-analysis/runs/{run_id}/cancel"
|
|
1033
|
+
try:
|
|
1034
|
+
with _client(transport=transport) as client:
|
|
1035
|
+
response = client.post(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
1036
|
+
except httpx.HTTPError as exc:
|
|
1037
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
1038
|
+
if response.status_code == 401:
|
|
1039
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
1040
|
+
# Same exact-match contract as `get_issue_analysis_run`: a missing run is
|
|
1041
|
+
# not a project-scope problem.
|
|
1042
|
+
if response.status_code == 404 and _detail_or_text(response) == "Issue-analysis run not found":
|
|
1043
|
+
raise IssueNotFoundError(f"No issue-analysis run {run_id} in this project.")
|
|
1044
|
+
if response.status_code in (403, 404):
|
|
1045
|
+
raise _project_scope_error(response)
|
|
1046
|
+
if response.status_code >= 400:
|
|
1047
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|