cassis-cli 0.2.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cassis_cli-0.2.0 → cassis_cli-0.4.0}/PKG-INFO +76 -9
- cassis_cli-0.4.0/README.md +191 -0
- {cassis_cli-0.2.0 → cassis_cli-0.4.0}/cassis_cli/__init__.py +1 -1
- cassis_cli-0.4.0/cassis_cli/api.py +322 -0
- cassis_cli-0.4.0/cassis_cli/common.py +95 -0
- cassis_cli-0.4.0/cassis_cli/eval.py +356 -0
- {cassis_cli-0.2.0 → cassis_cli-0.4.0}/cassis_cli/main.py +2 -0
- cassis_cli-0.4.0/cassis_cli/ontology.py +374 -0
- {cassis_cli-0.2.0 → cassis_cli-0.4.0}/pyproject.toml +1 -1
- cassis_cli-0.2.0/README.md +0 -124
- cassis_cli-0.2.0/cassis_cli/api.py +0 -113
- cassis_cli-0.2.0/cassis_cli/ontology.py +0 -256
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cassis-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Cassis CLI — run Cassis actions (ontology validation, upload and publish) from your CI pipelines
|
|
5
5
|
License: Proprietary
|
|
6
6
|
Keywords: cassis,ontology,ci,text-to-sql
|
|
@@ -23,7 +23,10 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
Run Cassis actions from your CI pipelines:
|
|
24
24
|
|
|
25
25
|
- `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.
|
|
26
|
+
- `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.
|
|
26
27
|
- `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.
|
|
28
|
+
- `cassis ontology pull` downloads the project's unpublished ontology into your repository checkout (full sync — stale local YAML files are pruned), so you can start editing from the current state, or bootstrap a repo that isn't git-synced (e.g. Bitbucket).
|
|
29
|
+
- `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.
|
|
27
30
|
|
|
28
31
|
## Install
|
|
29
32
|
|
|
@@ -35,7 +38,7 @@ pip install cassis-cli
|
|
|
35
38
|
|
|
36
39
|
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
37
40
|
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
38
|
-
3. For `upload`: find the project ID (UUID) in the project's URL and expose it as `CASSIS_PROJECT_ID` (or pass `--project`).
|
|
41
|
+
3. For `pull`, `upload` and `eval run`: find the project ID (UUID) in the project's URL and expose it as `CASSIS_PROJECT_ID` (or pass `--project`).
|
|
39
42
|
|
|
40
43
|
## Usage
|
|
41
44
|
|
|
@@ -46,6 +49,10 @@ cassis ontology check
|
|
|
46
49
|
# Or point at the checkout explicitly:
|
|
47
50
|
cassis ontology check /path/to/checkout
|
|
48
51
|
|
|
52
|
+
# Download the project's unpublished ontology into the checkout (full sync;
|
|
53
|
+
# review with git diff — pass --no-prune to keep local files it would delete):
|
|
54
|
+
cassis ontology pull --project 019f0000-0000-7000-8000-000000000000
|
|
55
|
+
|
|
49
56
|
# Upload the ontology to a project and publish it immediately:
|
|
50
57
|
cassis ontology upload --project 019f0000-0000-7000-8000-000000000000
|
|
51
58
|
|
|
@@ -57,7 +64,19 @@ cassis ontology upload --project ... --label "release 1.2"
|
|
|
57
64
|
|
|
58
65
|
# Machine-readable output:
|
|
59
66
|
cassis ontology check --json
|
|
67
|
+
cassis ontology pull --project ... --json
|
|
60
68
|
cassis ontology upload --project ... --json
|
|
69
|
+
cassis eval run --project ... --json
|
|
70
|
+
|
|
71
|
+
# Run the eval suite against the local ontology files and wait for results
|
|
72
|
+
# (the run is labelled with your git branch name in the Evals page):
|
|
73
|
+
cassis eval run --project ...
|
|
74
|
+
|
|
75
|
+
# Run against an existing Cassis ontology branch, or the unpublished ontology:
|
|
76
|
+
cassis eval run --project ... --branch feature-x
|
|
77
|
+
|
|
78
|
+
# Start the run and return immediately (poll in the webapp):
|
|
79
|
+
cassis eval run --project ... --no-wait
|
|
61
80
|
```
|
|
62
81
|
|
|
63
82
|
Configuration (flags take precedence over env vars):
|
|
@@ -67,20 +86,43 @@ Configuration (flags take precedence over env vars):
|
|
|
67
86
|
| `--api-key` | `CASSIS_API_KEY` | — (required) |
|
|
68
87
|
| `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
|
|
69
88
|
| `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
|
|
70
|
-
| `--project` (upload
|
|
89
|
+
| `--project` (pull, upload, eval run) | `CASSIS_PROJECT_ID` | — (required) |
|
|
90
|
+
|
|
91
|
+
`cassis eval run` also accepts `--label` (run label in the Evals page; defaults
|
|
92
|
+
to the branch name from the CI environment or the local git checkout; rejected
|
|
93
|
+
with `--branch`, whose runs are labelled with the branch name), `--wait/--no-wait`, `--poll-interval` (5 s),
|
|
94
|
+
`--timeout` (30 min — the run keeps going server-side if the CLI stops waiting),
|
|
95
|
+
and Ctrl-C cancels the run (exit 130). It prints a deep link to the run's page
|
|
96
|
+
in the Evals UI; `--app-url` / `CASSIS_APP_URL` overrides the link's base URL
|
|
97
|
+
when the webapp is not served from the API host (defaults to `--api-url`).
|
|
98
|
+
|
|
99
|
+
### Formatting
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Rewrite the ontology files in canonical form (in place)
|
|
103
|
+
cassis ontology fmt
|
|
104
|
+
|
|
105
|
+
# CI mode: fail (exit 1) if any file is not canonical, write nothing
|
|
106
|
+
cassis ontology fmt --check
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`fmt` uses the exact serializer the validation round-trip compares against, so a formatted tree cannot fail that stage. Formatting does not run import validation — `check` remains the pass/fail gate for semantic problems (dangling references, incomplete metrics).
|
|
110
|
+
|
|
111
|
+
**Review the diff before committing**: canonical form keeps exactly the fields Cassis understands. Unknown fields (typos) are dropped — the rewrite makes them visible in `git diff` instead of losing them silently at sync time. Files with duplicate YAML keys are rejected (fix them by hand: the formatter can't know which value you meant).
|
|
71
112
|
|
|
72
113
|
### Exit codes
|
|
73
114
|
|
|
74
115
|
| Code | Meaning |
|
|
75
116
|
| ---- | ------------------------------------------------------------------------------ |
|
|
76
|
-
| 0 | Ontology is valid (check) / uploaded (upload)
|
|
77
|
-
| 1 | Validation failed (check: findings printed; upload: nothing imported)
|
|
117
|
+
| 0 | Ontology is valid (check) / pulled (pull) / uploaded (upload) / eval run completed all-passed (eval run) |
|
|
118
|
+
| 1 | Validation failed (check: findings printed; upload: nothing imported; eval run: invalid tree, failed cases, or failed/cancelled run) |
|
|
78
119
|
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits) |
|
|
79
|
-
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response) |
|
|
120
|
+
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, or `--timeout` reached |
|
|
80
121
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
exit 2 before uploading anything;
|
|
122
|
+
Commands that send the local tree (`check`, `upload`, `eval run`) accept up to
|
|
123
|
+
2000 YAML files / 5 MB total — far above real ontologies (a few hundred small
|
|
124
|
+
files). Beyond that the CLI fails fast with exit 2 before uploading anything;
|
|
125
|
+
double-check `--base-path` if you hit it.
|
|
84
126
|
|
|
85
127
|
`upload` replaces the project's entire ontology with the uploaded tree. A
|
|
86
128
|
never-published project always goes live immediately on first upload (even
|
|
@@ -105,6 +147,20 @@ jobs:
|
|
|
105
147
|
env:
|
|
106
148
|
CASSIS_API_KEY: ${{ secrets.CASSIS_API_KEY }}
|
|
107
149
|
|
|
150
|
+
ontology-eval:
|
|
151
|
+
runs-on: ubuntu-latest
|
|
152
|
+
if: github.event_name == 'pull_request'
|
|
153
|
+
steps:
|
|
154
|
+
- uses: actions/checkout@v4
|
|
155
|
+
- uses: actions/setup-python@v5
|
|
156
|
+
with:
|
|
157
|
+
python-version: "3.12"
|
|
158
|
+
- run: pip install cassis-cli
|
|
159
|
+
- run: cassis eval run
|
|
160
|
+
env:
|
|
161
|
+
CASSIS_API_KEY: ${{ secrets.CASSIS_API_KEY }}
|
|
162
|
+
CASSIS_PROJECT_ID: ${{ vars.CASSIS_PROJECT_ID }}
|
|
163
|
+
|
|
108
164
|
ontology-publish:
|
|
109
165
|
runs-on: ubuntu-latest
|
|
110
166
|
if: github.ref == 'refs/heads/main'
|
|
@@ -131,6 +187,17 @@ ontology-check:
|
|
|
131
187
|
variables:
|
|
132
188
|
CASSIS_API_KEY: $CASSIS_API_KEY
|
|
133
189
|
|
|
190
|
+
ontology-eval:
|
|
191
|
+
image: python:3.12-slim
|
|
192
|
+
rules:
|
|
193
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
194
|
+
script:
|
|
195
|
+
- pip install cassis-cli
|
|
196
|
+
- cassis eval run
|
|
197
|
+
variables:
|
|
198
|
+
CASSIS_API_KEY: $CASSIS_API_KEY
|
|
199
|
+
CASSIS_PROJECT_ID: $CASSIS_PROJECT_ID
|
|
200
|
+
|
|
134
201
|
ontology-publish:
|
|
135
202
|
image: python:3.12-slim
|
|
136
203
|
rules:
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Cassis CLI
|
|
2
|
+
|
|
3
|
+
Run Cassis actions from your CI pipelines:
|
|
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.
|
|
6
|
+
- `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
|
+
- `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
|
+
- `cassis ontology pull` downloads the project's unpublished ontology into your repository checkout (full sync — stale local YAML files are pruned), so you can start editing from the current state, or bootstrap a repo that isn't git-synced (e.g. Bitbucket).
|
|
9
|
+
- `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.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install cassis-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
20
|
+
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
21
|
+
3. For `pull`, `upload` and `eval run`: find the project ID (UUID) in the project's URL and expose it as `CASSIS_PROJECT_ID` (or pass `--project`).
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# From the root of a repository synced with Cassis (contains the ontology export directory, cassis/ by default):
|
|
27
|
+
cassis ontology check
|
|
28
|
+
|
|
29
|
+
# Or point at the checkout explicitly:
|
|
30
|
+
cassis ontology check /path/to/checkout
|
|
31
|
+
|
|
32
|
+
# Download the project's unpublished ontology into the checkout (full sync;
|
|
33
|
+
# review with git diff — pass --no-prune to keep local files it would delete):
|
|
34
|
+
cassis ontology pull --project 019f0000-0000-7000-8000-000000000000
|
|
35
|
+
|
|
36
|
+
# Upload the ontology to a project and publish it immediately:
|
|
37
|
+
cassis ontology upload --project 019f0000-0000-7000-8000-000000000000
|
|
38
|
+
|
|
39
|
+
# Upload without publishing (the tree becomes the project's unpublished ontology, to review in Cassis):
|
|
40
|
+
cassis ontology upload --project ... --no-publish
|
|
41
|
+
|
|
42
|
+
# Label the published version:
|
|
43
|
+
cassis ontology upload --project ... --label "release 1.2"
|
|
44
|
+
|
|
45
|
+
# Machine-readable output:
|
|
46
|
+
cassis ontology check --json
|
|
47
|
+
cassis ontology pull --project ... --json
|
|
48
|
+
cassis ontology upload --project ... --json
|
|
49
|
+
cassis eval run --project ... --json
|
|
50
|
+
|
|
51
|
+
# Run the eval suite against the local ontology files and wait for results
|
|
52
|
+
# (the run is labelled with your git branch name in the Evals page):
|
|
53
|
+
cassis eval run --project ...
|
|
54
|
+
|
|
55
|
+
# Run against an existing Cassis ontology branch, or the unpublished ontology:
|
|
56
|
+
cassis eval run --project ... --branch feature-x
|
|
57
|
+
|
|
58
|
+
# Start the run and return immediately (poll in the webapp):
|
|
59
|
+
cassis eval run --project ... --no-wait
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Configuration (flags take precedence over env vars):
|
|
63
|
+
|
|
64
|
+
| Flag | Env var | Default |
|
|
65
|
+
| ----------- | ---------------- | --------------------------- |
|
|
66
|
+
| `--api-key` | `CASSIS_API_KEY` | — (required) |
|
|
67
|
+
| `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
|
|
68
|
+
| `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
|
|
69
|
+
| `--project` (pull, upload, eval run) | `CASSIS_PROJECT_ID` | — (required) |
|
|
70
|
+
|
|
71
|
+
`cassis eval run` also accepts `--label` (run label in the Evals page; defaults
|
|
72
|
+
to the branch name from the CI environment or the local git checkout; rejected
|
|
73
|
+
with `--branch`, whose runs are labelled with the branch name), `--wait/--no-wait`, `--poll-interval` (5 s),
|
|
74
|
+
`--timeout` (30 min — the run keeps going server-side if the CLI stops waiting),
|
|
75
|
+
and Ctrl-C cancels the run (exit 130). It prints a deep link to the run's page
|
|
76
|
+
in the Evals UI; `--app-url` / `CASSIS_APP_URL` overrides the link's base URL
|
|
77
|
+
when the webapp is not served from the API host (defaults to `--api-url`).
|
|
78
|
+
|
|
79
|
+
### Formatting
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Rewrite the ontology files in canonical form (in place)
|
|
83
|
+
cassis ontology fmt
|
|
84
|
+
|
|
85
|
+
# CI mode: fail (exit 1) if any file is not canonical, write nothing
|
|
86
|
+
cassis ontology fmt --check
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`fmt` uses the exact serializer the validation round-trip compares against, so a formatted tree cannot fail that stage. Formatting does not run import validation — `check` remains the pass/fail gate for semantic problems (dangling references, incomplete metrics).
|
|
90
|
+
|
|
91
|
+
**Review the diff before committing**: canonical form keeps exactly the fields Cassis understands. Unknown fields (typos) are dropped — the rewrite makes them visible in `git diff` instead of losing them silently at sync time. Files with duplicate YAML keys are rejected (fix them by hand: the formatter can't know which value you meant).
|
|
92
|
+
|
|
93
|
+
### Exit codes
|
|
94
|
+
|
|
95
|
+
| Code | Meaning |
|
|
96
|
+
| ---- | ------------------------------------------------------------------------------ |
|
|
97
|
+
| 0 | Ontology is valid (check) / pulled (pull) / uploaded (upload) / eval run completed all-passed (eval run) |
|
|
98
|
+
| 1 | Validation failed (check: findings printed; upload: nothing imported; eval run: invalid tree, failed cases, or failed/cancelled run) |
|
|
99
|
+
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits) |
|
|
100
|
+
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, or `--timeout` reached |
|
|
101
|
+
|
|
102
|
+
Commands that send the local tree (`check`, `upload`, `eval run`) accept up to
|
|
103
|
+
2000 YAML files / 5 MB total — far above real ontologies (a few hundred small
|
|
104
|
+
files). Beyond that the CLI fails fast with exit 2 before uploading anything;
|
|
105
|
+
double-check `--base-path` if you hit it.
|
|
106
|
+
|
|
107
|
+
`upload` replaces the project's entire ontology with the uploaded tree. A
|
|
108
|
+
never-published project always goes live immediately on first upload (even
|
|
109
|
+
with `--no-publish`), matching imports from the Cassis app. Publishing is
|
|
110
|
+
idempotent: re-uploading content identical to the published version reports
|
|
111
|
+
that version instead of creating a new one, so re-running the CI job on
|
|
112
|
+
unchanged files is a no-op.
|
|
113
|
+
|
|
114
|
+
### GitHub Actions example
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
jobs:
|
|
118
|
+
ontology-check:
|
|
119
|
+
runs-on: ubuntu-latest
|
|
120
|
+
steps:
|
|
121
|
+
- uses: actions/checkout@v4
|
|
122
|
+
- uses: actions/setup-python@v5
|
|
123
|
+
with:
|
|
124
|
+
python-version: "3.12"
|
|
125
|
+
- run: pip install cassis-cli
|
|
126
|
+
- run: cassis ontology check
|
|
127
|
+
env:
|
|
128
|
+
CASSIS_API_KEY: ${{ secrets.CASSIS_API_KEY }}
|
|
129
|
+
|
|
130
|
+
ontology-eval:
|
|
131
|
+
runs-on: ubuntu-latest
|
|
132
|
+
if: github.event_name == 'pull_request'
|
|
133
|
+
steps:
|
|
134
|
+
- uses: actions/checkout@v4
|
|
135
|
+
- uses: actions/setup-python@v5
|
|
136
|
+
with:
|
|
137
|
+
python-version: "3.12"
|
|
138
|
+
- run: pip install cassis-cli
|
|
139
|
+
- run: cassis eval run
|
|
140
|
+
env:
|
|
141
|
+
CASSIS_API_KEY: ${{ secrets.CASSIS_API_KEY }}
|
|
142
|
+
CASSIS_PROJECT_ID: ${{ vars.CASSIS_PROJECT_ID }}
|
|
143
|
+
|
|
144
|
+
ontology-publish:
|
|
145
|
+
runs-on: ubuntu-latest
|
|
146
|
+
if: github.ref == 'refs/heads/main'
|
|
147
|
+
steps:
|
|
148
|
+
- uses: actions/checkout@v4
|
|
149
|
+
- uses: actions/setup-python@v5
|
|
150
|
+
with:
|
|
151
|
+
python-version: "3.12"
|
|
152
|
+
- run: pip install cassis-cli
|
|
153
|
+
- run: cassis ontology upload
|
|
154
|
+
env:
|
|
155
|
+
CASSIS_API_KEY: ${{ secrets.CASSIS_API_KEY }}
|
|
156
|
+
CASSIS_PROJECT_ID: ${{ vars.CASSIS_PROJECT_ID }}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### GitLab CI example
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
ontology-check:
|
|
163
|
+
image: python:3.12-slim
|
|
164
|
+
script:
|
|
165
|
+
- pip install cassis-cli
|
|
166
|
+
- cassis ontology check
|
|
167
|
+
variables:
|
|
168
|
+
CASSIS_API_KEY: $CASSIS_API_KEY
|
|
169
|
+
|
|
170
|
+
ontology-eval:
|
|
171
|
+
image: python:3.12-slim
|
|
172
|
+
rules:
|
|
173
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
174
|
+
script:
|
|
175
|
+
- pip install cassis-cli
|
|
176
|
+
- cassis eval run
|
|
177
|
+
variables:
|
|
178
|
+
CASSIS_API_KEY: $CASSIS_API_KEY
|
|
179
|
+
CASSIS_PROJECT_ID: $CASSIS_PROJECT_ID
|
|
180
|
+
|
|
181
|
+
ontology-publish:
|
|
182
|
+
image: python:3.12-slim
|
|
183
|
+
rules:
|
|
184
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
185
|
+
script:
|
|
186
|
+
- pip install cassis-cli
|
|
187
|
+
- cassis ontology upload
|
|
188
|
+
variables:
|
|
189
|
+
CASSIS_API_KEY: $CASSIS_API_KEY
|
|
190
|
+
CASSIS_PROJECT_ID: $CASSIS_PROJECT_ID
|
|
191
|
+
```
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Thin HTTP client for the Cassis API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
DEFAULT_API_URL = "https://app.getcassis.com"
|
|
10
|
+
TIMEOUT_SECONDS = 60.0
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApiError(Exception):
|
|
14
|
+
"""Transport or HTTP-level failure talking to the Cassis API."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuthError(ApiError):
|
|
18
|
+
"""The API rejected the API key (401)."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UploadValidationError(ApiError):
|
|
22
|
+
"""The API rejected the uploaded ontology tree as invalid (400)."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EvalStartValidationError(ApiError):
|
|
26
|
+
"""The API rejected the eval-run start request (400): invalid tree or no test cases.
|
|
27
|
+
|
|
28
|
+
``detail`` keeps the structured payload (``{"message", "findings"}`` for an
|
|
29
|
+
invalid tree, or a plain string) for display.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, detail: object) -> None:
|
|
33
|
+
super().__init__(str(detail))
|
|
34
|
+
self.detail = detail
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class EvalRunActiveError(ApiError):
|
|
38
|
+
"""Another eval run is already active for the project (409)."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _project_scope_error(response: httpx.Response) -> ApiError:
|
|
42
|
+
# Surface the server's own message when it names the missing resource
|
|
43
|
+
# (e.g. "Branch 'x' not found") — the generic hint covers the rest.
|
|
44
|
+
try:
|
|
45
|
+
detail = response.json().get("detail")
|
|
46
|
+
except ValueError:
|
|
47
|
+
detail = None
|
|
48
|
+
prefix = f"{detail} " if isinstance(detail, str) and detail else ""
|
|
49
|
+
return ApiError(
|
|
50
|
+
f"{prefix}(HTTP {response.status_code}). Check --project and that "
|
|
51
|
+
"the API key belongs to the project's organization and can edit it."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_json_response(response: httpx.Response, url: str) -> Any:
|
|
56
|
+
try:
|
|
57
|
+
return response.json()
|
|
58
|
+
except ValueError as exc: # json.JSONDecodeError — e.g. a proxy or portal answering HTML with a 200
|
|
59
|
+
raise ApiError(
|
|
60
|
+
f"The Cassis API at {url} returned a non-JSON response — check the API URL and any proxy in between."
|
|
61
|
+
) from exc
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def post_ontology_check(
|
|
65
|
+
*,
|
|
66
|
+
api_url: str,
|
|
67
|
+
api_key: str,
|
|
68
|
+
files: dict[str, str],
|
|
69
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
"""POST the ontology tree to /api/ci/ontology-check and return the response body."""
|
|
72
|
+
url = api_url.rstrip("/") + "/api/ci/ontology-check"
|
|
73
|
+
try:
|
|
74
|
+
with httpx.Client(timeout=TIMEOUT_SECONDS, transport=transport) as client:
|
|
75
|
+
response = client.post(
|
|
76
|
+
url,
|
|
77
|
+
json={"files": files},
|
|
78
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
79
|
+
)
|
|
80
|
+
except httpx.HTTPError as exc:
|
|
81
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
82
|
+
|
|
83
|
+
if response.status_code == 401:
|
|
84
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
85
|
+
if response.status_code >= 400:
|
|
86
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
87
|
+
result = _parse_json_response(response, url)
|
|
88
|
+
if (
|
|
89
|
+
not isinstance(result, dict)
|
|
90
|
+
or not all(key in result for key in ("passed", "title", "summary"))
|
|
91
|
+
or not isinstance(result.get("findings"), list)
|
|
92
|
+
):
|
|
93
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def post_ontology_import(
|
|
98
|
+
*,
|
|
99
|
+
api_url: str,
|
|
100
|
+
api_key: str,
|
|
101
|
+
project_id: str,
|
|
102
|
+
files: dict[str, str],
|
|
103
|
+
publish: bool,
|
|
104
|
+
label: Optional[str] = None,
|
|
105
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
106
|
+
) -> dict[str, Any]:
|
|
107
|
+
"""POST the ontology tree to /api/ci/projects/{project_id}/ontology/import and return the response body."""
|
|
108
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/ontology/import"
|
|
109
|
+
body: dict[str, Any] = {"files": files, "publish": publish}
|
|
110
|
+
if label is not None:
|
|
111
|
+
body["label"] = label
|
|
112
|
+
try:
|
|
113
|
+
with httpx.Client(timeout=TIMEOUT_SECONDS, transport=transport) as client:
|
|
114
|
+
response = client.post(
|
|
115
|
+
url,
|
|
116
|
+
json=body,
|
|
117
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
118
|
+
)
|
|
119
|
+
except httpx.HTTPError as exc:
|
|
120
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
121
|
+
|
|
122
|
+
if response.status_code == 401:
|
|
123
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
124
|
+
if response.status_code == 400:
|
|
125
|
+
try:
|
|
126
|
+
detail = response.json().get("detail") or response.text[:500]
|
|
127
|
+
except ValueError:
|
|
128
|
+
detail = response.text[:500]
|
|
129
|
+
raise UploadValidationError(str(detail))
|
|
130
|
+
if response.status_code in (403, 404):
|
|
131
|
+
raise _project_scope_error(response)
|
|
132
|
+
if response.status_code >= 400:
|
|
133
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
134
|
+
result = _parse_json_response(response, url)
|
|
135
|
+
if not isinstance(result, dict) or not all(
|
|
136
|
+
key in result for key in ("domain_count", "table_count", "join_count", "metric_count", "published_version")
|
|
137
|
+
):
|
|
138
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_ontology_export(
|
|
143
|
+
*,
|
|
144
|
+
api_url: str,
|
|
145
|
+
api_key: str,
|
|
146
|
+
project_id: str,
|
|
147
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
148
|
+
) -> dict[str, str]:
|
|
149
|
+
"""GET /api/ci/projects/{project_id}/ontology/export and return the files tree."""
|
|
150
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/ontology/export"
|
|
151
|
+
try:
|
|
152
|
+
with httpx.Client(timeout=TIMEOUT_SECONDS, transport=transport) as client:
|
|
153
|
+
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
154
|
+
except httpx.HTTPError as exc:
|
|
155
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
156
|
+
|
|
157
|
+
if response.status_code == 401:
|
|
158
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
159
|
+
if response.status_code in (403, 404):
|
|
160
|
+
raise _project_scope_error(response)
|
|
161
|
+
if response.status_code >= 400:
|
|
162
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
163
|
+
result = _parse_json_response(response, url)
|
|
164
|
+
if not isinstance(result, dict) or not isinstance(result.get("files"), dict):
|
|
165
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
166
|
+
return result["files"]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def post_eval_run_start(
|
|
170
|
+
*,
|
|
171
|
+
api_url: str,
|
|
172
|
+
api_key: str,
|
|
173
|
+
project_id: str,
|
|
174
|
+
files: Optional[dict[str, str]] = None,
|
|
175
|
+
branch: Optional[str] = None,
|
|
176
|
+
label: Optional[str] = None,
|
|
177
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
178
|
+
) -> dict[str, Any]:
|
|
179
|
+
"""POST to /api/ci/projects/{project_id}/eval/runs and return the run record."""
|
|
180
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/eval/runs"
|
|
181
|
+
body: dict[str, Any] = {}
|
|
182
|
+
if files is not None:
|
|
183
|
+
body["files"] = files
|
|
184
|
+
if branch is not None:
|
|
185
|
+
body["branch"] = branch
|
|
186
|
+
if label is not None:
|
|
187
|
+
body["label"] = label
|
|
188
|
+
try:
|
|
189
|
+
with httpx.Client(timeout=TIMEOUT_SECONDS, transport=transport) as client:
|
|
190
|
+
response = client.post(url, json=body, headers={"Authorization": f"Bearer {api_key}"})
|
|
191
|
+
except httpx.HTTPError as exc:
|
|
192
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
193
|
+
|
|
194
|
+
if response.status_code == 401:
|
|
195
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
196
|
+
if response.status_code == 400:
|
|
197
|
+
try:
|
|
198
|
+
detail = response.json().get("detail") or response.text[:500]
|
|
199
|
+
except ValueError:
|
|
200
|
+
detail = response.text[:500]
|
|
201
|
+
raise EvalStartValidationError(detail)
|
|
202
|
+
if response.status_code == 409:
|
|
203
|
+
raise EvalRunActiveError(
|
|
204
|
+
"An eval run is already active for this project — wait for it to finish or cancel it "
|
|
205
|
+
"(in the webapp's Evals page, or with the run id printed when it was started)."
|
|
206
|
+
)
|
|
207
|
+
if response.status_code in (403, 404):
|
|
208
|
+
raise _project_scope_error(response)
|
|
209
|
+
if response.status_code >= 400:
|
|
210
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
211
|
+
result = _parse_json_response(response, url)
|
|
212
|
+
if not isinstance(result, dict) or not all(key in result for key in ("run_id", "status", "total_cases")):
|
|
213
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
214
|
+
return result
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _get_eval_json(url: str, api_key: str, transport: Optional[httpx.BaseTransport]) -> Any:
|
|
218
|
+
"""GET an eval-run URL with the shared error mapping."""
|
|
219
|
+
try:
|
|
220
|
+
with httpx.Client(timeout=TIMEOUT_SECONDS, transport=transport) as client:
|
|
221
|
+
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
222
|
+
except httpx.HTTPError as exc:
|
|
223
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
224
|
+
|
|
225
|
+
if response.status_code == 401:
|
|
226
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
227
|
+
if response.status_code in (403, 404):
|
|
228
|
+
raise _project_scope_error(response)
|
|
229
|
+
if response.status_code >= 400:
|
|
230
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
231
|
+
return _parse_json_response(response, url)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def get_eval_run(
|
|
235
|
+
*,
|
|
236
|
+
api_url: str,
|
|
237
|
+
api_key: str,
|
|
238
|
+
project_id: str,
|
|
239
|
+
run_id: str,
|
|
240
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
241
|
+
) -> dict[str, Any]:
|
|
242
|
+
"""GET /api/ci/projects/{project_id}/eval/runs/{run_id} and return the run record."""
|
|
243
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/eval/runs/{run_id}"
|
|
244
|
+
result = _get_eval_json(url, api_key, transport)
|
|
245
|
+
if not isinstance(result, dict) or not all(key in result for key in ("run_id", "status", "total_cases")):
|
|
246
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
247
|
+
return result
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def get_eval_run_results(
|
|
251
|
+
*,
|
|
252
|
+
api_url: str,
|
|
253
|
+
api_key: str,
|
|
254
|
+
project_id: str,
|
|
255
|
+
run_id: str,
|
|
256
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
257
|
+
) -> list[dict[str, Any]]:
|
|
258
|
+
"""GET /api/ci/projects/{project_id}/eval/runs/{run_id}/results and return the result list."""
|
|
259
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/eval/runs/{run_id}/results"
|
|
260
|
+
result = _get_eval_json(url, api_key, transport)
|
|
261
|
+
if not isinstance(result, list):
|
|
262
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
263
|
+
return result
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def post_eval_run_cancel(
|
|
267
|
+
*,
|
|
268
|
+
api_url: str,
|
|
269
|
+
api_key: str,
|
|
270
|
+
project_id: str,
|
|
271
|
+
run_id: str,
|
|
272
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
273
|
+
) -> None:
|
|
274
|
+
"""POST /api/ci/projects/{project_id}/eval/runs/{run_id}/cancel."""
|
|
275
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/eval/runs/{run_id}/cancel"
|
|
276
|
+
try:
|
|
277
|
+
with httpx.Client(timeout=TIMEOUT_SECONDS, transport=transport) as client:
|
|
278
|
+
response = client.post(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
279
|
+
except httpx.HTTPError as exc:
|
|
280
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
281
|
+
if response.status_code == 401:
|
|
282
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
283
|
+
if response.status_code in (403, 404):
|
|
284
|
+
raise _project_scope_error(response)
|
|
285
|
+
if response.status_code >= 400:
|
|
286
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def post_ontology_fmt(
|
|
290
|
+
*,
|
|
291
|
+
api_url: str,
|
|
292
|
+
api_key: str,
|
|
293
|
+
files: dict[str, str],
|
|
294
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
295
|
+
) -> dict[str, Any]:
|
|
296
|
+
"""POST the ontology tree to /api/ci/ontology-fmt and return the response body."""
|
|
297
|
+
url = api_url.rstrip("/") + "/api/ci/ontology-fmt"
|
|
298
|
+
try:
|
|
299
|
+
with httpx.Client(timeout=TIMEOUT_SECONDS, transport=transport) as client:
|
|
300
|
+
response = client.post(
|
|
301
|
+
url,
|
|
302
|
+
json={"files": files},
|
|
303
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
304
|
+
)
|
|
305
|
+
except httpx.HTTPError as exc:
|
|
306
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
307
|
+
|
|
308
|
+
if response.status_code == 401:
|
|
309
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
310
|
+
if response.status_code >= 400:
|
|
311
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
312
|
+
result = _parse_json_response(response, url)
|
|
313
|
+
if (
|
|
314
|
+
not isinstance(result, dict)
|
|
315
|
+
or "ok" not in result
|
|
316
|
+
or not isinstance(result.get("findings"), list)
|
|
317
|
+
or not isinstance(result.get("changed_paths"), list)
|
|
318
|
+
or not isinstance(result.get("removed_paths"), list)
|
|
319
|
+
or (result["ok"] and not isinstance(result.get("files"), dict))
|
|
320
|
+
):
|
|
321
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
322
|
+
return result
|