cassis-cli 1.4.0__tar.gz → 1.5.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.4.0 → cassis_cli-1.5.0}/PKG-INFO +20 -9
- {cassis_cli-1.4.0 → cassis_cli-1.5.0}/README.md +19 -8
- cassis_cli-1.5.0/cassis_cli/__init__.py +8 -0
- {cassis_cli-1.4.0 → cassis_cli-1.5.0}/cassis_cli/api.py +138 -3
- cassis_cli-1.5.0/cassis_cli/issues.py +362 -0
- {cassis_cli-1.4.0 → cassis_cli-1.5.0}/cassis_cli/main.py +2 -0
- {cassis_cli-1.4.0 → cassis_cli-1.5.0}/cassis_cli/schema.py +9 -10
- {cassis_cli-1.4.0 → cassis_cli-1.5.0}/pyproject.toml +1 -1
- cassis_cli-1.4.0/cassis_cli/__init__.py +0 -3
- {cassis_cli-1.4.0 → cassis_cli-1.5.0}/LICENSE +0 -0
- {cassis_cli-1.4.0 → cassis_cli-1.5.0}/NOTICE +0 -0
- {cassis_cli-1.4.0 → cassis_cli-1.5.0}/cassis_cli/common.py +0 -0
- {cassis_cli-1.4.0 → cassis_cli-1.5.0}/cassis_cli/eval.py +0 -0
- {cassis_cli-1.4.0 → cassis_cli-1.5.0}/cassis_cli/guide.py +0 -0
- {cassis_cli-1.4.0 → cassis_cli-1.5.0}/cassis_cli/ontology.py +0 -0
- {cassis_cli-1.4.0 → cassis_cli-1.5.0}/cassis_cli/ontology_design_guide.md +0 -0
- {cassis_cli-1.4.0 → cassis_cli-1.5.0}/cassis_cli/projects.py +0 -0
- {cassis_cli-1.4.0 → cassis_cli-1.5.0}/cassis_cli/status.py +0 -0
- {cassis_cli-1.4.0 → cassis_cli-1.5.0}/cassis_cli/verify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cassis-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.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
|
|
@@ -35,9 +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
|
|
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 the detection run to finish and exits 0 only when it completed — the schema is stored and applied atomically with run completion, so exit 0 means the DDL parsed and the project now uses it.
|
|
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
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 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.
|
|
41
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.
|
|
42
43
|
|
|
43
44
|
## Install
|
|
@@ -60,7 +61,7 @@ The ontology tree under `<base-path>` (default `cassis/`) is:
|
|
|
60
61
|
|
|
61
62
|
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
62
63
|
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
63
|
-
3. For `pull`, `upload`, `schema pull`, `eval run`, `ontology test`,
|
|
64
|
+
3. For `pull`, `upload`, `schema pull`, `eval run`, `ontology test`, the `eval` case commands (`add-case`, `list-cases`, `delete-case`), and the `issues` commands: the project ID (UUID) is taken from `<base-path>/project.yml` in the checkout (written by `pull` and by publishing) — so once a repo is pulled you don't need to pass it. To override, or before the first pull, set `CASSIS_PROJECT_ID` or pass `--project` (find the UUID with `cassis projects list`, or in the project's URL). `ontology check` uses the same resolution but treats it as optional: unbound checkouts get the project-less validation (no schema reference warnings).
|
|
64
65
|
|
|
65
66
|
## Usage
|
|
66
67
|
|
|
@@ -122,15 +123,25 @@ cassis eval delete-case 019f0000-0000-7000-8000-0000000000ca --project ...
|
|
|
122
123
|
# Pull the source schema into <base-path>/.schema.json (gitignored local snapshot):
|
|
123
124
|
cassis schema pull
|
|
124
125
|
|
|
125
|
-
# Push a DDL file to detect source-schema changes (DDL-only projects)
|
|
126
|
+
# Push a DDL file to detect source-schema changes (DDL-only projects);
|
|
127
|
+
# exits 0 only once the run completed and the schema is applied:
|
|
126
128
|
cassis schema push schema.sql
|
|
127
129
|
|
|
128
|
-
# Push and return immediately (poll in the webapp):
|
|
129
|
-
cassis schema push schema.sql --no-wait
|
|
130
|
-
|
|
131
130
|
# List the projects the API key can reach (id, name, published version, dialect):
|
|
132
131
|
cassis projects list
|
|
133
132
|
|
|
133
|
+
# Triage the issues Cassis raised (filter by --status/--impact/--cause; --json for raw output):
|
|
134
|
+
cassis issues list --status open
|
|
135
|
+
cassis issues show 019f0000-0000-7000-8000-0000000000e1
|
|
136
|
+
|
|
137
|
+
# Read what the agent saw for one occurrence (ids from `issues show`):
|
|
138
|
+
cassis issues evidence 019f0000-0000-7000-8000-0000000000e1 019f0000-0000-7000-8000-0000000000c1
|
|
139
|
+
|
|
140
|
+
# Close the loop once the fix is published (or reopen):
|
|
141
|
+
cassis issues resolve 019f0000-0000-7000-8000-0000000000e1
|
|
142
|
+
cassis issues dismiss 019f0000-0000-7000-8000-0000000000e1
|
|
143
|
+
cassis issues reopen 019f0000-0000-7000-8000-0000000000e1
|
|
144
|
+
|
|
134
145
|
# Published version vs local checkout (add --watch to poll until your merge is published):
|
|
135
146
|
cassis status
|
|
136
147
|
cassis status --watch --timeout 600
|
|
@@ -147,7 +158,7 @@ Configuration (flags take precedence over env vars):
|
|
|
147
158
|
| `--api-key` | `CASSIS_API_KEY` | — (required) |
|
|
148
159
|
| `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
|
|
149
160
|
| `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
|
|
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) |
|
|
161
|
+
| `--project` (check, pull, upload, schema pull, eval run, eval add-case, eval list-cases, eval delete-case, test, issues) | `CASSIS_PROJECT_ID` | the id in `<base-path>/project.yml` (required before the first pull; `check` alone falls back to the project-less validation when unbound) |
|
|
151
162
|
|
|
152
163
|
`cassis eval run` also accepts `--case <id>` (repeatable; run only the named
|
|
153
164
|
cases, ids from `eval list-cases` or `add-case`), `--label` (run label in the Evals page; defaults
|
|
@@ -177,7 +188,7 @@ cassis ontology fmt --check
|
|
|
177
188
|
| Code | Meaning |
|
|
178
189
|
| ---- | ------------------------------------------------------------------------------ |
|
|
179
190
|
| 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) |
|
|
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) |
|
|
191
|
+
| 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) |
|
|
181
192
|
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits) |
|
|
182
193
|
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, out of credits, or `--timeout` reached |
|
|
183
194
|
|
|
@@ -13,9 +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
|
|
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 the detection run to finish and exits 0 only when it completed — the schema is stored and applied atomically with run completion, so exit 0 means the DDL parsed and the project now uses it.
|
|
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
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 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.
|
|
19
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.
|
|
20
21
|
|
|
21
22
|
## Install
|
|
@@ -38,7 +39,7 @@ The ontology tree under `<base-path>` (default `cassis/`) is:
|
|
|
38
39
|
|
|
39
40
|
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
40
41
|
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
41
|
-
3. For `pull`, `upload`, `schema pull`, `eval run`, `ontology test`,
|
|
42
|
+
3. For `pull`, `upload`, `schema pull`, `eval run`, `ontology test`, the `eval` case commands (`add-case`, `list-cases`, `delete-case`), and the `issues` commands: the project ID (UUID) is taken from `<base-path>/project.yml` in the checkout (written by `pull` and by publishing) — so once a repo is pulled you don't need to pass it. To override, or before the first pull, set `CASSIS_PROJECT_ID` or pass `--project` (find the UUID with `cassis projects list`, or in the project's URL). `ontology check` uses the same resolution but treats it as optional: unbound checkouts get the project-less validation (no schema reference warnings).
|
|
42
43
|
|
|
43
44
|
## Usage
|
|
44
45
|
|
|
@@ -100,15 +101,25 @@ cassis eval delete-case 019f0000-0000-7000-8000-0000000000ca --project ...
|
|
|
100
101
|
# Pull the source schema into <base-path>/.schema.json (gitignored local snapshot):
|
|
101
102
|
cassis schema pull
|
|
102
103
|
|
|
103
|
-
# Push a DDL file to detect source-schema changes (DDL-only projects)
|
|
104
|
+
# Push a DDL file to detect source-schema changes (DDL-only projects);
|
|
105
|
+
# exits 0 only once the run completed and the schema is applied:
|
|
104
106
|
cassis schema push schema.sql
|
|
105
107
|
|
|
106
|
-
# Push and return immediately (poll in the webapp):
|
|
107
|
-
cassis schema push schema.sql --no-wait
|
|
108
|
-
|
|
109
108
|
# List the projects the API key can reach (id, name, published version, dialect):
|
|
110
109
|
cassis projects list
|
|
111
110
|
|
|
111
|
+
# Triage the issues Cassis raised (filter by --status/--impact/--cause; --json for raw output):
|
|
112
|
+
cassis issues list --status open
|
|
113
|
+
cassis issues show 019f0000-0000-7000-8000-0000000000e1
|
|
114
|
+
|
|
115
|
+
# Read what the agent saw for one occurrence (ids from `issues show`):
|
|
116
|
+
cassis issues evidence 019f0000-0000-7000-8000-0000000000e1 019f0000-0000-7000-8000-0000000000c1
|
|
117
|
+
|
|
118
|
+
# Close the loop once the fix is published (or reopen):
|
|
119
|
+
cassis issues resolve 019f0000-0000-7000-8000-0000000000e1
|
|
120
|
+
cassis issues dismiss 019f0000-0000-7000-8000-0000000000e1
|
|
121
|
+
cassis issues reopen 019f0000-0000-7000-8000-0000000000e1
|
|
122
|
+
|
|
112
123
|
# Published version vs local checkout (add --watch to poll until your merge is published):
|
|
113
124
|
cassis status
|
|
114
125
|
cassis status --watch --timeout 600
|
|
@@ -125,7 +136,7 @@ Configuration (flags take precedence over env vars):
|
|
|
125
136
|
| `--api-key` | `CASSIS_API_KEY` | — (required) |
|
|
126
137
|
| `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
|
|
127
138
|
| `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
|
|
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) |
|
|
139
|
+
| `--project` (check, pull, upload, schema pull, eval run, eval add-case, eval list-cases, eval delete-case, test, issues) | `CASSIS_PROJECT_ID` | the id in `<base-path>/project.yml` (required before the first pull; `check` alone falls back to the project-less validation when unbound) |
|
|
129
140
|
|
|
130
141
|
`cassis eval run` also accepts `--case <id>` (repeatable; run only the named
|
|
131
142
|
cases, ids from `eval list-cases` or `add-case`), `--label` (run label in the Evals page; defaults
|
|
@@ -155,7 +166,7 @@ cassis ontology fmt --check
|
|
|
155
166
|
| Code | Meaning |
|
|
156
167
|
| ---- | ------------------------------------------------------------------------------ |
|
|
157
168
|
| 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) |
|
|
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) |
|
|
169
|
+
| 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) |
|
|
159
170
|
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits) |
|
|
160
171
|
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, out of credits, or `--timeout` reached |
|
|
161
172
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Cassis CLI — run Cassis actions from your CI pipelines."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("cassis-cli")
|
|
7
|
+
except PackageNotFoundError: # uninstalled source tree (no dist metadata)
|
|
8
|
+
__version__ = "0.0.0"
|
|
@@ -11,6 +11,14 @@ from cassis_cli import __version__
|
|
|
11
11
|
DEFAULT_API_URL = "https://app.getcassis.com"
|
|
12
12
|
TIMEOUT_SECONDS = 60.0
|
|
13
13
|
|
|
14
|
+
# Whole-tree endpoints (check, fmt, import) parse and re-serialize every file
|
|
15
|
+
# server-side, so their duration scales with the ontology: a few hundred files
|
|
16
|
+
# is tens of seconds of pure CPU, and the 60s default cut real runs off. The
|
|
17
|
+
# ceiling is deliberately far above what the edge allows (CloudFront's
|
|
18
|
+
# origin_read_timeout, see infra) — a client-side timeout should never be the
|
|
19
|
+
# thing that reports a slow server as "unreachable".
|
|
20
|
+
ONTOLOGY_TREE_TIMEOUT_SECONDS = 300.0
|
|
21
|
+
|
|
14
22
|
USER_AGENT = f"cassis-cli/{__version__}"
|
|
15
23
|
|
|
16
24
|
# Response header the CI endpoints set to the newest cassis-cli on PyPI.
|
|
@@ -149,7 +157,7 @@ def post_ontology_check(
|
|
|
149
157
|
else:
|
|
150
158
|
url = api_url.rstrip("/") + "/api/ci/ontology-check"
|
|
151
159
|
try:
|
|
152
|
-
with _client(transport=transport) as client:
|
|
160
|
+
with _client(timeout=ONTOLOGY_TREE_TIMEOUT_SECONDS, transport=transport) as client:
|
|
153
161
|
response = client.post(
|
|
154
162
|
url,
|
|
155
163
|
json={"files": files},
|
|
@@ -190,7 +198,7 @@ def post_ontology_import(
|
|
|
190
198
|
if label is not None:
|
|
191
199
|
body["label"] = label
|
|
192
200
|
try:
|
|
193
|
-
with _client(transport=transport) as client:
|
|
201
|
+
with _client(timeout=ONTOLOGY_TREE_TIMEOUT_SECONDS, transport=transport) as client:
|
|
194
202
|
response = client.post(
|
|
195
203
|
url,
|
|
196
204
|
json=body,
|
|
@@ -610,6 +618,133 @@ def post_eval_run_cancel(
|
|
|
610
618
|
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
611
619
|
|
|
612
620
|
|
|
621
|
+
class IssueNotFoundError(ApiError):
|
|
622
|
+
"""The project has no issue (or occurrence) with this id."""
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _get_issue_json(
|
|
626
|
+
url: str,
|
|
627
|
+
api_key: str,
|
|
628
|
+
transport: Optional[httpx.BaseTransport],
|
|
629
|
+
*,
|
|
630
|
+
params: Optional[dict[str, str]] = None,
|
|
631
|
+
not_found_message: Optional[str] = None,
|
|
632
|
+
) -> Any:
|
|
633
|
+
"""GET an issues URL with the shared error mapping."""
|
|
634
|
+
try:
|
|
635
|
+
with _client(transport=transport) as client:
|
|
636
|
+
response = client.get(url, params=params, headers={"Authorization": f"Bearer {api_key}"})
|
|
637
|
+
except httpx.HTTPError as exc:
|
|
638
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
639
|
+
|
|
640
|
+
if response.status_code == 401:
|
|
641
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
642
|
+
# Exact-match wire contract with the issue endpoints' 404 detail (see the
|
|
643
|
+
# issue routes in backend/app/endpoints/ci.py): it distinguishes a missing
|
|
644
|
+
# issue or occurrence (exit 1) from a project-scope 404 (exit 3).
|
|
645
|
+
if response.status_code == 404 and not_found_message and _detail_or_text(response) == "Issue not found":
|
|
646
|
+
raise IssueNotFoundError(not_found_message)
|
|
647
|
+
if response.status_code in (403, 404):
|
|
648
|
+
raise _project_scope_error(response)
|
|
649
|
+
if response.status_code >= 400:
|
|
650
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
651
|
+
return _parse_json_response(response, url)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def get_issues(
|
|
655
|
+
*,
|
|
656
|
+
api_url: str,
|
|
657
|
+
api_key: str,
|
|
658
|
+
project_id: str,
|
|
659
|
+
status: Optional[str] = None,
|
|
660
|
+
impact: Optional[str] = None,
|
|
661
|
+
cause: Optional[str] = None,
|
|
662
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
663
|
+
) -> list[dict[str, Any]]:
|
|
664
|
+
"""GET /api/ci/projects/{project_id}/issues and return the issue list."""
|
|
665
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/issues"
|
|
666
|
+
params = {key: value for key, value in (("status", status), ("impact", impact), ("cause", cause)) if value}
|
|
667
|
+
result = _get_issue_json(url, api_key, transport, params=params)
|
|
668
|
+
if not isinstance(result, list) or not all(isinstance(issue, dict) and "id" in issue for issue in result):
|
|
669
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
670
|
+
return result
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def get_issue(
|
|
674
|
+
*,
|
|
675
|
+
api_url: str,
|
|
676
|
+
api_key: str,
|
|
677
|
+
project_id: str,
|
|
678
|
+
issue_id: str,
|
|
679
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
680
|
+
) -> dict[str, Any]:
|
|
681
|
+
"""GET /api/ci/projects/{project_id}/issues/{issue_id} and return the issue with its occurrences."""
|
|
682
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/issues/{issue_id}"
|
|
683
|
+
result = _get_issue_json(
|
|
684
|
+
url,
|
|
685
|
+
api_key,
|
|
686
|
+
transport,
|
|
687
|
+
not_found_message=f"No issue {issue_id} in this project (or occurrence not found).",
|
|
688
|
+
)
|
|
689
|
+
if not isinstance(result, dict) or "id" not in result:
|
|
690
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
691
|
+
return result
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def get_issue_evidence(
|
|
695
|
+
*,
|
|
696
|
+
api_url: str,
|
|
697
|
+
api_key: str,
|
|
698
|
+
project_id: str,
|
|
699
|
+
issue_id: str,
|
|
700
|
+
occurrence_id: str,
|
|
701
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
702
|
+
) -> dict[str, Any]:
|
|
703
|
+
"""GET the evidence of one occurrence and return it."""
|
|
704
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/issues/{issue_id}/occurrences/{occurrence_id}/evidence"
|
|
705
|
+
result = _get_issue_json(
|
|
706
|
+
url,
|
|
707
|
+
api_key,
|
|
708
|
+
transport,
|
|
709
|
+
not_found_message=f"No issue {issue_id} in this project (or occurrence not found).",
|
|
710
|
+
)
|
|
711
|
+
if not isinstance(result, dict):
|
|
712
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
713
|
+
return result
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def post_issue_status(
|
|
717
|
+
*,
|
|
718
|
+
api_url: str,
|
|
719
|
+
api_key: str,
|
|
720
|
+
project_id: str,
|
|
721
|
+
issue_id: str,
|
|
722
|
+
status: str,
|
|
723
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
724
|
+
) -> dict[str, Any]:
|
|
725
|
+
"""POST /api/ci/projects/{project_id}/issues/{issue_id}/status and return the updated issue."""
|
|
726
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/issues/{issue_id}/status"
|
|
727
|
+
try:
|
|
728
|
+
with _client(transport=transport) as client:
|
|
729
|
+
response = client.post(url, json={"status": status}, headers={"Authorization": f"Bearer {api_key}"})
|
|
730
|
+
except httpx.HTTPError as exc:
|
|
731
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
732
|
+
|
|
733
|
+
if response.status_code == 401:
|
|
734
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
735
|
+
# Same exact-match wire contract as the issue GETs above.
|
|
736
|
+
if response.status_code == 404 and _detail_or_text(response) == "Issue not found":
|
|
737
|
+
raise IssueNotFoundError(f"No issue {issue_id} in this project (or occurrence not found).")
|
|
738
|
+
if response.status_code in (403, 404):
|
|
739
|
+
raise _project_scope_error(response)
|
|
740
|
+
if response.status_code >= 400:
|
|
741
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
742
|
+
result = _parse_json_response(response, url)
|
|
743
|
+
if not isinstance(result, dict) or "status" not in result:
|
|
744
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
745
|
+
return result
|
|
746
|
+
|
|
747
|
+
|
|
613
748
|
def post_ontology_fmt(
|
|
614
749
|
*,
|
|
615
750
|
api_url: str,
|
|
@@ -620,7 +755,7 @@ def post_ontology_fmt(
|
|
|
620
755
|
"""POST the ontology tree to /api/ci/ontology-fmt and return the response body."""
|
|
621
756
|
url = api_url.rstrip("/") + "/api/ci/ontology-fmt"
|
|
622
757
|
try:
|
|
623
|
-
with _client(transport=transport) as client:
|
|
758
|
+
with _client(timeout=ONTOLOGY_TREE_TIMEOUT_SECONDS, transport=transport) as client:
|
|
624
759
|
response = client.post(
|
|
625
760
|
url,
|
|
626
761
|
json={"files": files},
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""`cassis issues` — triage the issues Cassis raised on the project, from the terminal.
|
|
2
|
+
|
|
3
|
+
Issues are what the product found wrong while answering questions (an ontology
|
|
4
|
+
gap, missing data). Listing, reading the evidence behind an occurrence, and
|
|
5
|
+
resolving or dismissing them from a checkout keeps the fix loop next to the
|
|
6
|
+
ontology files instead of in the webapp.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from cassis_cli.api import (
|
|
17
|
+
DEFAULT_API_URL,
|
|
18
|
+
ApiError,
|
|
19
|
+
AuthError,
|
|
20
|
+
IssueNotFoundError,
|
|
21
|
+
get_issue,
|
|
22
|
+
get_issue_evidence,
|
|
23
|
+
get_issues,
|
|
24
|
+
post_issue_status,
|
|
25
|
+
)
|
|
26
|
+
from cassis_cli.common import (
|
|
27
|
+
DEFAULT_BASE_PATH,
|
|
28
|
+
EXIT_OK,
|
|
29
|
+
EXIT_TRANSPORT,
|
|
30
|
+
EXIT_USAGE,
|
|
31
|
+
EXIT_VALIDATION_FAILED,
|
|
32
|
+
require_api_key,
|
|
33
|
+
resolve_project_id,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
app = typer.Typer(no_args_is_help=True, help="Triage the project's issues.")
|
|
37
|
+
|
|
38
|
+
STATUSES = ("open", "resolved", "dismissed")
|
|
39
|
+
IMPACTS = ("wrong_answer", "unreliable_answer", "no_answer", "inefficient")
|
|
40
|
+
CAUSES = ("ontology_gap", "missing_data")
|
|
41
|
+
|
|
42
|
+
# The five options every command in this subapp takes. Shared instances: typer
|
|
43
|
+
# reads the option metadata at decoration time, so one constant per flag keeps
|
|
44
|
+
# the six commands from repeating the same block.
|
|
45
|
+
_PATH_OPTION = typer.Option(
|
|
46
|
+
Path("."),
|
|
47
|
+
"--path",
|
|
48
|
+
help="Repository checkout root (holds <base-path>/project.yml for the --project default).",
|
|
49
|
+
)
|
|
50
|
+
_PROJECT_OPTION = typer.Option(
|
|
51
|
+
None,
|
|
52
|
+
"--project",
|
|
53
|
+
envvar="CASSIS_PROJECT_ID",
|
|
54
|
+
help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
|
|
55
|
+
)
|
|
56
|
+
_API_KEY_OPTION = typer.Option(
|
|
57
|
+
None,
|
|
58
|
+
"--api-key",
|
|
59
|
+
envvar="CASSIS_API_KEY",
|
|
60
|
+
help="Cassis API key (sk-k6-...). Create one in Organization settings -> API keys.",
|
|
61
|
+
)
|
|
62
|
+
_API_URL_OPTION = typer.Option(
|
|
63
|
+
DEFAULT_API_URL,
|
|
64
|
+
"--api-url",
|
|
65
|
+
envvar="CASSIS_API_URL",
|
|
66
|
+
help="Cassis API base URL.",
|
|
67
|
+
)
|
|
68
|
+
_BASE_PATH_OPTION = typer.Option(
|
|
69
|
+
DEFAULT_BASE_PATH,
|
|
70
|
+
"--base-path",
|
|
71
|
+
envvar="CASSIS_BASE_PATH",
|
|
72
|
+
help="Repository directory the ontology is exported under (holds project.yml for the --project default).",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _validate_choice(value: Optional[str], allowed: "tuple[str, ...]", flag: str) -> Optional[str]:
|
|
77
|
+
"""Exit 2 (usage) when a filter value is not one the API knows."""
|
|
78
|
+
if value is None or value in allowed:
|
|
79
|
+
return value
|
|
80
|
+
typer.secho(f"{flag} must be one of {', '.join(allowed)}, got {value!r}.", fg=typer.colors.RED, err=True)
|
|
81
|
+
raise typer.Exit(EXIT_USAGE)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _api_failure(exc: ApiError) -> "typer.Exit":
|
|
85
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
86
|
+
return typer.Exit(EXIT_TRANSPORT)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _not_found_failure(exc: IssueNotFoundError) -> "typer.Exit":
|
|
90
|
+
typer.secho(str(exc), fg=typer.colors.YELLOW, err=True)
|
|
91
|
+
return typer.Exit(EXIT_VALIDATION_FAILED)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _one_line(value: Any) -> str:
|
|
95
|
+
return str(value or "").replace("\n", " ").strip()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _field(label: str, value: Any, *, blank_line: bool = False) -> None:
|
|
99
|
+
"""Print a labelled block, indenting a multi-line value under its label."""
|
|
100
|
+
if value in (None, "", [], {}):
|
|
101
|
+
return
|
|
102
|
+
if blank_line:
|
|
103
|
+
typer.echo("")
|
|
104
|
+
text = value if isinstance(value, str) else json.dumps(value, indent=2)
|
|
105
|
+
lines = text.strip().splitlines()
|
|
106
|
+
if len(lines) == 1:
|
|
107
|
+
typer.echo(f"{label}: {lines[0]}")
|
|
108
|
+
else:
|
|
109
|
+
typer.echo(f"{label}:")
|
|
110
|
+
for line in lines:
|
|
111
|
+
typer.echo(f" {line}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@app.command(name="list")
|
|
115
|
+
def list_issues(
|
|
116
|
+
status: Optional[str] = typer.Option(None, "--status", help=f"Filter by status ({', '.join(STATUSES)})."),
|
|
117
|
+
impact: Optional[str] = typer.Option(None, "--impact", help=f"Filter by impact ({', '.join(IMPACTS)})."),
|
|
118
|
+
cause: Optional[str] = typer.Option(None, "--cause", help=f"Filter by cause ({', '.join(CAUSES)})."),
|
|
119
|
+
path: Path = _PATH_OPTION,
|
|
120
|
+
project_id: Optional[str] = _PROJECT_OPTION,
|
|
121
|
+
api_key: Optional[str] = _API_KEY_OPTION,
|
|
122
|
+
api_url: str = _API_URL_OPTION,
|
|
123
|
+
base_path: str = _BASE_PATH_OPTION,
|
|
124
|
+
json_output: bool = typer.Option(False, "--json", help="Print the issues as raw JSON."),
|
|
125
|
+
) -> None:
|
|
126
|
+
"""List the project's issues, prioritized by impact then recurrence.
|
|
127
|
+
|
|
128
|
+
Prints each issue's id, impact, occurrence count, status and title; the id
|
|
129
|
+
is what `cassis issues show`, `resolve`, `dismiss` and `reopen` take. Exits
|
|
130
|
+
0 on success, 2 on usage errors, 3 on transport/API errors.
|
|
131
|
+
"""
|
|
132
|
+
status = _validate_choice(status, STATUSES, "--status")
|
|
133
|
+
impact = _validate_choice(impact, IMPACTS, "--impact")
|
|
134
|
+
cause = _validate_choice(cause, CAUSES, "--cause")
|
|
135
|
+
api_key = require_api_key(api_key)
|
|
136
|
+
project_id = resolve_project_id(project_id, path / Path(base_path), quiet=json_output)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
issues = get_issues(
|
|
140
|
+
api_url=api_url,
|
|
141
|
+
api_key=api_key,
|
|
142
|
+
project_id=project_id,
|
|
143
|
+
status=status,
|
|
144
|
+
impact=impact,
|
|
145
|
+
cause=cause,
|
|
146
|
+
)
|
|
147
|
+
except (AuthError, ApiError) as exc:
|
|
148
|
+
raise _api_failure(exc) from exc
|
|
149
|
+
|
|
150
|
+
if json_output:
|
|
151
|
+
typer.echo(json.dumps(issues, indent=2))
|
|
152
|
+
raise typer.Exit(EXIT_OK)
|
|
153
|
+
|
|
154
|
+
if not issues:
|
|
155
|
+
typer.echo("No issues match." if (status or impact or cause) else "No issues on this project.")
|
|
156
|
+
raise typer.Exit(EXIT_OK)
|
|
157
|
+
|
|
158
|
+
for issue in issues:
|
|
159
|
+
occurrences = issue.get("occurrence_count_cache") or 0
|
|
160
|
+
typer.echo(
|
|
161
|
+
f"{issue.get('id')} {issue.get('impact')} x{occurrences} "
|
|
162
|
+
f"{issue.get('status')} {_one_line(issue.get('title'))}"
|
|
163
|
+
)
|
|
164
|
+
raise typer.Exit(EXIT_OK)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@app.command()
|
|
168
|
+
def show(
|
|
169
|
+
issue_id: str = typer.Argument(..., help="Id of the issue to show (from `cassis issues list`)."),
|
|
170
|
+
path: Path = _PATH_OPTION,
|
|
171
|
+
project_id: Optional[str] = _PROJECT_OPTION,
|
|
172
|
+
api_key: Optional[str] = _API_KEY_OPTION,
|
|
173
|
+
api_url: str = _API_URL_OPTION,
|
|
174
|
+
base_path: str = _BASE_PATH_OPTION,
|
|
175
|
+
json_output: bool = typer.Option(False, "--json", help="Print the issue as raw JSON."),
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Show one issue: its diagnosis, suggested action, and occurrences.
|
|
178
|
+
|
|
179
|
+
Each occurrence's id feeds `cassis issues evidence`, which prints what the
|
|
180
|
+
agent saw. Exits 0 on success, 1 when the issue does not exist in the
|
|
181
|
+
project, 2 on usage errors, 3 on transport/API errors.
|
|
182
|
+
"""
|
|
183
|
+
api_key = require_api_key(api_key)
|
|
184
|
+
project_id = resolve_project_id(project_id, path / Path(base_path), quiet=json_output)
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
issue = get_issue(api_url=api_url, api_key=api_key, project_id=project_id, issue_id=issue_id)
|
|
188
|
+
except IssueNotFoundError as exc:
|
|
189
|
+
raise _not_found_failure(exc) from exc
|
|
190
|
+
except (AuthError, ApiError) as exc:
|
|
191
|
+
raise _api_failure(exc) from exc
|
|
192
|
+
|
|
193
|
+
if json_output:
|
|
194
|
+
typer.echo(json.dumps(issue, indent=2))
|
|
195
|
+
raise typer.Exit(EXIT_OK)
|
|
196
|
+
|
|
197
|
+
occurrences = issue.get("occurrences") or []
|
|
198
|
+
typer.echo(f"{issue.get('id')} {_one_line(issue.get('title'))}")
|
|
199
|
+
typer.echo(
|
|
200
|
+
f"{issue.get('status')} {issue.get('impact')} {issue.get('cause')} "
|
|
201
|
+
f"{issue.get('occurrence_count_cache', len(occurrences))} occurrence(s)"
|
|
202
|
+
)
|
|
203
|
+
_field("Description", issue.get("description"), blank_line=True)
|
|
204
|
+
_field("Suggested action", issue.get("suggested_action"), blank_line=True)
|
|
205
|
+
typer.echo("")
|
|
206
|
+
typer.echo("Fix proposal: available (review it in Cassis)" if issue.get("fix_proposal") else "Fix proposal: none")
|
|
207
|
+
if occurrences:
|
|
208
|
+
typer.echo("")
|
|
209
|
+
typer.echo("Occurrences:")
|
|
210
|
+
for occurrence in occurrences:
|
|
211
|
+
typer.echo(f" {occurrence.get('id')} {_one_line(occurrence.get('symptom'))}")
|
|
212
|
+
raise typer.Exit(EXIT_OK)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@app.command()
|
|
216
|
+
def evidence(
|
|
217
|
+
issue_id: str = typer.Argument(..., help="Id of the issue (from `cassis issues list`)."),
|
|
218
|
+
occurrence_id: str = typer.Argument(..., help="Id of the occurrence (from `cassis issues show`)."),
|
|
219
|
+
path: Path = _PATH_OPTION,
|
|
220
|
+
project_id: Optional[str] = _PROJECT_OPTION,
|
|
221
|
+
api_key: Optional[str] = _API_KEY_OPTION,
|
|
222
|
+
api_url: str = _API_URL_OPTION,
|
|
223
|
+
base_path: str = _BASE_PATH_OPTION,
|
|
224
|
+
json_output: bool = typer.Option(False, "--json", help="Print the evidence as raw JSON."),
|
|
225
|
+
) -> None:
|
|
226
|
+
"""Print the evidence behind one occurrence of an issue.
|
|
227
|
+
|
|
228
|
+
The evidence joined from the occurrence's source: the generated SQL and
|
|
229
|
+
its results, the agent's process log, and either the user's rating (chat)
|
|
230
|
+
or the judge's verdict (eval). Exits 0 on success, 1 when the issue or
|
|
231
|
+
occurrence does not exist in the project, 2 on usage errors, 3 on
|
|
232
|
+
transport/API errors.
|
|
233
|
+
"""
|
|
234
|
+
api_key = require_api_key(api_key)
|
|
235
|
+
project_id = resolve_project_id(project_id, path / Path(base_path), quiet=json_output)
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
record = get_issue_evidence(
|
|
239
|
+
api_url=api_url,
|
|
240
|
+
api_key=api_key,
|
|
241
|
+
project_id=project_id,
|
|
242
|
+
issue_id=issue_id,
|
|
243
|
+
occurrence_id=occurrence_id,
|
|
244
|
+
)
|
|
245
|
+
except IssueNotFoundError as exc:
|
|
246
|
+
raise _not_found_failure(exc) from exc
|
|
247
|
+
except (AuthError, ApiError) as exc:
|
|
248
|
+
raise _api_failure(exc) from exc
|
|
249
|
+
|
|
250
|
+
if json_output:
|
|
251
|
+
typer.echo(json.dumps(record, indent=2))
|
|
252
|
+
raise typer.Exit(EXIT_OK)
|
|
253
|
+
|
|
254
|
+
for key, value in record.items():
|
|
255
|
+
_field(key.replace("_", " ").capitalize(), value)
|
|
256
|
+
raise typer.Exit(EXIT_OK)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _set_status(
|
|
260
|
+
*,
|
|
261
|
+
issue_id: str,
|
|
262
|
+
status: str,
|
|
263
|
+
path: Path,
|
|
264
|
+
project_id: Optional[str],
|
|
265
|
+
api_key: Optional[str],
|
|
266
|
+
api_url: str,
|
|
267
|
+
base_path: str,
|
|
268
|
+
) -> None:
|
|
269
|
+
"""Post a new status for the issue and confirm it on one line."""
|
|
270
|
+
api_key = require_api_key(api_key)
|
|
271
|
+
project_id = resolve_project_id(project_id, path / Path(base_path))
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
issue = post_issue_status(
|
|
275
|
+
api_url=api_url,
|
|
276
|
+
api_key=api_key,
|
|
277
|
+
project_id=project_id,
|
|
278
|
+
issue_id=issue_id,
|
|
279
|
+
status=status,
|
|
280
|
+
)
|
|
281
|
+
except IssueNotFoundError as exc:
|
|
282
|
+
raise _not_found_failure(exc) from exc
|
|
283
|
+
except (AuthError, ApiError) as exc:
|
|
284
|
+
raise _api_failure(exc) from exc
|
|
285
|
+
|
|
286
|
+
typer.secho(f"✓ Issue {issue_id} is now {issue.get('status', status)}.", fg=typer.colors.GREEN)
|
|
287
|
+
raise typer.Exit(EXIT_OK)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@app.command()
|
|
291
|
+
def resolve(
|
|
292
|
+
issue_id: str = typer.Argument(..., help="Id of the issue to resolve (from `cassis issues list`)."),
|
|
293
|
+
path: Path = _PATH_OPTION,
|
|
294
|
+
project_id: Optional[str] = _PROJECT_OPTION,
|
|
295
|
+
api_key: Optional[str] = _API_KEY_OPTION,
|
|
296
|
+
api_url: str = _API_URL_OPTION,
|
|
297
|
+
base_path: str = _BASE_PATH_OPTION,
|
|
298
|
+
) -> None:
|
|
299
|
+
"""Mark an issue resolved — the ontology change that fixes it has landed.
|
|
300
|
+
|
|
301
|
+
Exits 0 on success, 1 when the issue does not exist in the project, 2 on
|
|
302
|
+
usage errors, 3 on transport/API errors.
|
|
303
|
+
"""
|
|
304
|
+
_set_status(
|
|
305
|
+
issue_id=issue_id,
|
|
306
|
+
status="resolved",
|
|
307
|
+
path=path,
|
|
308
|
+
project_id=project_id,
|
|
309
|
+
api_key=api_key,
|
|
310
|
+
api_url=api_url,
|
|
311
|
+
base_path=base_path,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@app.command()
|
|
316
|
+
def dismiss(
|
|
317
|
+
issue_id: str = typer.Argument(..., help="Id of the issue to dismiss (from `cassis issues list`)."),
|
|
318
|
+
path: Path = _PATH_OPTION,
|
|
319
|
+
project_id: Optional[str] = _PROJECT_OPTION,
|
|
320
|
+
api_key: Optional[str] = _API_KEY_OPTION,
|
|
321
|
+
api_url: str = _API_URL_OPTION,
|
|
322
|
+
base_path: str = _BASE_PATH_OPTION,
|
|
323
|
+
) -> None:
|
|
324
|
+
"""Dismiss an issue — not worth acting on.
|
|
325
|
+
|
|
326
|
+
Exits 0 on success, 1 when the issue does not exist in the project, 2 on
|
|
327
|
+
usage errors, 3 on transport/API errors.
|
|
328
|
+
"""
|
|
329
|
+
_set_status(
|
|
330
|
+
issue_id=issue_id,
|
|
331
|
+
status="dismissed",
|
|
332
|
+
path=path,
|
|
333
|
+
project_id=project_id,
|
|
334
|
+
api_key=api_key,
|
|
335
|
+
api_url=api_url,
|
|
336
|
+
base_path=base_path,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@app.command()
|
|
341
|
+
def reopen(
|
|
342
|
+
issue_id: str = typer.Argument(..., help="Id of the issue to reopen (from `cassis issues list`)."),
|
|
343
|
+
path: Path = _PATH_OPTION,
|
|
344
|
+
project_id: Optional[str] = _PROJECT_OPTION,
|
|
345
|
+
api_key: Optional[str] = _API_KEY_OPTION,
|
|
346
|
+
api_url: str = _API_URL_OPTION,
|
|
347
|
+
base_path: str = _BASE_PATH_OPTION,
|
|
348
|
+
) -> None:
|
|
349
|
+
"""Reopen a resolved or dismissed issue.
|
|
350
|
+
|
|
351
|
+
Exits 0 on success, 1 when the issue does not exist in the project, 2 on
|
|
352
|
+
usage errors, 3 on transport/API errors.
|
|
353
|
+
"""
|
|
354
|
+
_set_status(
|
|
355
|
+
issue_id=issue_id,
|
|
356
|
+
status="open",
|
|
357
|
+
path=path,
|
|
358
|
+
project_id=project_id,
|
|
359
|
+
api_key=api_key,
|
|
360
|
+
api_url=api_url,
|
|
361
|
+
base_path=base_path,
|
|
362
|
+
)
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import typer
|
|
6
6
|
from cassis_cli import __version__
|
|
7
7
|
from cassis_cli.eval import app as eval_app
|
|
8
|
+
from cassis_cli.issues import app as issues_app
|
|
8
9
|
from cassis_cli.ontology import app as ontology_app
|
|
9
10
|
from cassis_cli.projects import app as projects_app
|
|
10
11
|
from cassis_cli.schema import app as schema_app
|
|
@@ -19,6 +20,7 @@ app.add_typer(ontology_app, name="ontology")
|
|
|
19
20
|
app.add_typer(eval_app, name="eval")
|
|
20
21
|
app.add_typer(schema_app, name="schema")
|
|
21
22
|
app.add_typer(projects_app, name="projects")
|
|
23
|
+
app.add_typer(issues_app, name="issues")
|
|
22
24
|
app.command()(status)
|
|
23
25
|
app.command()(verify)
|
|
24
26
|
|
|
@@ -175,8 +175,7 @@ def push(
|
|
|
175
175
|
envvar="CASSIS_BASE_PATH",
|
|
176
176
|
help="Repository directory the ontology is exported under (the project's git-sync Path setting).",
|
|
177
177
|
),
|
|
178
|
-
|
|
179
|
-
poll_interval: float = typer.Option(5.0, "--poll-interval", help="Seconds between polls with --wait."),
|
|
178
|
+
poll_interval: float = typer.Option(5.0, "--poll-interval", help="Seconds between polls."),
|
|
180
179
|
timeout: float = typer.Option(600.0, "--timeout", help="Give up waiting after this many seconds."),
|
|
181
180
|
json_output: bool = typer.Option(False, "--json", help="Print the run record as raw JSON."),
|
|
182
181
|
) -> None:
|
|
@@ -186,9 +185,14 @@ def push(
|
|
|
186
185
|
project's complete source schema. Cassis diffs it against the ontology:
|
|
187
186
|
added, dropped, and changed objects appear in Ontology > Review > Data
|
|
188
187
|
source for approval. Re-uploading a corrected DDL supersedes the previous
|
|
189
|
-
one. Only works on DDL-only projects (no warehouse connection).
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
one. Only works on DDL-only projects (no warehouse connection).
|
|
189
|
+
|
|
190
|
+
Always waits for the detection run to finish: the server parses the DDL
|
|
191
|
+
inside the run (a large file takes a while, and an unparseable one fails
|
|
192
|
+
the run rather than the upload request), so exit 0 means the schema parsed
|
|
193
|
+
AND was applied — there is no fire-and-forget mode. Exits 0 on success, 1
|
|
194
|
+
on a failed detection run, 2 on usage errors, 3 on transport/API errors or
|
|
195
|
+
a timeout.
|
|
192
196
|
"""
|
|
193
197
|
api_key = require_api_key(api_key)
|
|
194
198
|
project_id = resolve_project_id(project_id, path / base_path)
|
|
@@ -218,11 +222,6 @@ def push(
|
|
|
218
222
|
run_id = run["run_id"]
|
|
219
223
|
typer.echo(f"Detection run started: {run_id}")
|
|
220
224
|
|
|
221
|
-
if not wait:
|
|
222
|
-
if json_output:
|
|
223
|
-
typer.echo(json.dumps(run, indent=2))
|
|
224
|
-
raise typer.Exit(EXIT_OK)
|
|
225
|
-
|
|
226
225
|
run = _wait_for_detection_run(
|
|
227
226
|
api_url=api_url,
|
|
228
227
|
api_key=api_key,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|