cassis-cli 0.1.0__tar.gz → 0.2.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/PKG-INFO +145 -0
- cassis_cli-0.2.0/README.md +124 -0
- {cassis_cli-0.1.0 → cassis_cli-0.2.0}/cassis_cli/__init__.py +1 -1
- cassis_cli-0.2.0/cassis_cli/api.py +113 -0
- {cassis_cli-0.1.0 → cassis_cli-0.2.0}/cassis_cli/ontology.py +149 -34
- {cassis_cli-0.1.0 → cassis_cli-0.2.0}/pyproject.toml +2 -2
- cassis_cli-0.1.0/PKG-INFO +0 -98
- cassis_cli-0.1.0/README.md +0 -77
- cassis_cli-0.1.0/cassis_cli/api.py +0 -56
- {cassis_cli-0.1.0 → cassis_cli-0.2.0}/cassis_cli/main.py +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cassis-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Cassis CLI — run Cassis actions (ontology validation, upload and publish) from your CI pipelines
|
|
5
|
+
License: Proprietary
|
|
6
|
+
Keywords: cassis,ontology,ci,text-to-sql
|
|
7
|
+
Author: Cassis
|
|
8
|
+
Author-email: tech.admin@getcassis.com
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: License :: Other/Proprietary License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Dist: httpx (>=0.24,<1.0)
|
|
18
|
+
Requires-Dist: typer (>=0.12,<1.0)
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Cassis CLI
|
|
22
|
+
|
|
23
|
+
Run Cassis actions from your CI pipelines:
|
|
24
|
+
|
|
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 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.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install cassis-cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
37
|
+
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`).
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# From the root of a repository synced with Cassis (contains the ontology export directory, cassis/ by default):
|
|
44
|
+
cassis ontology check
|
|
45
|
+
|
|
46
|
+
# Or point at the checkout explicitly:
|
|
47
|
+
cassis ontology check /path/to/checkout
|
|
48
|
+
|
|
49
|
+
# Upload the ontology to a project and publish it immediately:
|
|
50
|
+
cassis ontology upload --project 019f0000-0000-7000-8000-000000000000
|
|
51
|
+
|
|
52
|
+
# Upload without publishing (the tree becomes the project's unpublished ontology, to review in Cassis):
|
|
53
|
+
cassis ontology upload --project ... --no-publish
|
|
54
|
+
|
|
55
|
+
# Label the published version:
|
|
56
|
+
cassis ontology upload --project ... --label "release 1.2"
|
|
57
|
+
|
|
58
|
+
# Machine-readable output:
|
|
59
|
+
cassis ontology check --json
|
|
60
|
+
cassis ontology upload --project ... --json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Configuration (flags take precedence over env vars):
|
|
64
|
+
|
|
65
|
+
| Flag | Env var | Default |
|
|
66
|
+
| ----------- | ---------------- | --------------------------- |
|
|
67
|
+
| `--api-key` | `CASSIS_API_KEY` | — (required) |
|
|
68
|
+
| `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
|
|
69
|
+
| `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
|
|
70
|
+
| `--project` (upload only) | `CASSIS_PROJECT_ID` | — (required) |
|
|
71
|
+
|
|
72
|
+
### Exit codes
|
|
73
|
+
|
|
74
|
+
| Code | Meaning |
|
|
75
|
+
| ---- | ------------------------------------------------------------------------------ |
|
|
76
|
+
| 0 | Ontology is valid (check) / uploaded (upload) |
|
|
77
|
+
| 1 | Validation failed (check: findings printed; upload: nothing imported) |
|
|
78
|
+
| 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) |
|
|
80
|
+
|
|
81
|
+
Both commands accept up to 2000 YAML files / 5 MB total — far above real
|
|
82
|
+
ontologies (a few hundred small files). Beyond that the CLI fails fast with
|
|
83
|
+
exit 2 before uploading anything; double-check `--base-path` if you hit it.
|
|
84
|
+
|
|
85
|
+
`upload` replaces the project's entire ontology with the uploaded tree. A
|
|
86
|
+
never-published project always goes live immediately on first upload (even
|
|
87
|
+
with `--no-publish`), matching imports from the Cassis app. Publishing is
|
|
88
|
+
idempotent: re-uploading content identical to the published version reports
|
|
89
|
+
that version instead of creating a new one, so re-running the CI job on
|
|
90
|
+
unchanged files is a no-op.
|
|
91
|
+
|
|
92
|
+
### GitHub Actions example
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
jobs:
|
|
96
|
+
ontology-check:
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
steps:
|
|
99
|
+
- uses: actions/checkout@v4
|
|
100
|
+
- uses: actions/setup-python@v5
|
|
101
|
+
with:
|
|
102
|
+
python-version: "3.12"
|
|
103
|
+
- run: pip install cassis-cli
|
|
104
|
+
- run: cassis ontology check
|
|
105
|
+
env:
|
|
106
|
+
CASSIS_API_KEY: ${{ secrets.CASSIS_API_KEY }}
|
|
107
|
+
|
|
108
|
+
ontology-publish:
|
|
109
|
+
runs-on: ubuntu-latest
|
|
110
|
+
if: github.ref == 'refs/heads/main'
|
|
111
|
+
steps:
|
|
112
|
+
- uses: actions/checkout@v4
|
|
113
|
+
- uses: actions/setup-python@v5
|
|
114
|
+
with:
|
|
115
|
+
python-version: "3.12"
|
|
116
|
+
- run: pip install cassis-cli
|
|
117
|
+
- run: cassis ontology upload
|
|
118
|
+
env:
|
|
119
|
+
CASSIS_API_KEY: ${{ secrets.CASSIS_API_KEY }}
|
|
120
|
+
CASSIS_PROJECT_ID: ${{ vars.CASSIS_PROJECT_ID }}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### GitLab CI example
|
|
124
|
+
|
|
125
|
+
```yaml
|
|
126
|
+
ontology-check:
|
|
127
|
+
image: python:3.12-slim
|
|
128
|
+
script:
|
|
129
|
+
- pip install cassis-cli
|
|
130
|
+
- cassis ontology check
|
|
131
|
+
variables:
|
|
132
|
+
CASSIS_API_KEY: $CASSIS_API_KEY
|
|
133
|
+
|
|
134
|
+
ontology-publish:
|
|
135
|
+
image: python:3.12-slim
|
|
136
|
+
rules:
|
|
137
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
138
|
+
script:
|
|
139
|
+
- pip install cassis-cli
|
|
140
|
+
- cassis ontology upload
|
|
141
|
+
variables:
|
|
142
|
+
CASSIS_API_KEY: $CASSIS_API_KEY
|
|
143
|
+
CASSIS_PROJECT_ID: $CASSIS_PROJECT_ID
|
|
144
|
+
```
|
|
145
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
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 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.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install cassis-cli
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
17
|
+
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
18
|
+
3. For `upload`: find the project ID (UUID) in the project's URL and expose it as `CASSIS_PROJECT_ID` (or pass `--project`).
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# From the root of a repository synced with Cassis (contains the ontology export directory, cassis/ by default):
|
|
24
|
+
cassis ontology check
|
|
25
|
+
|
|
26
|
+
# Or point at the checkout explicitly:
|
|
27
|
+
cassis ontology check /path/to/checkout
|
|
28
|
+
|
|
29
|
+
# Upload the ontology to a project and publish it immediately:
|
|
30
|
+
cassis ontology upload --project 019f0000-0000-7000-8000-000000000000
|
|
31
|
+
|
|
32
|
+
# Upload without publishing (the tree becomes the project's unpublished ontology, to review in Cassis):
|
|
33
|
+
cassis ontology upload --project ... --no-publish
|
|
34
|
+
|
|
35
|
+
# Label the published version:
|
|
36
|
+
cassis ontology upload --project ... --label "release 1.2"
|
|
37
|
+
|
|
38
|
+
# Machine-readable output:
|
|
39
|
+
cassis ontology check --json
|
|
40
|
+
cassis ontology upload --project ... --json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Configuration (flags take precedence over env vars):
|
|
44
|
+
|
|
45
|
+
| Flag | Env var | Default |
|
|
46
|
+
| ----------- | ---------------- | --------------------------- |
|
|
47
|
+
| `--api-key` | `CASSIS_API_KEY` | — (required) |
|
|
48
|
+
| `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
|
|
49
|
+
| `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
|
|
50
|
+
| `--project` (upload only) | `CASSIS_PROJECT_ID` | — (required) |
|
|
51
|
+
|
|
52
|
+
### Exit codes
|
|
53
|
+
|
|
54
|
+
| Code | Meaning |
|
|
55
|
+
| ---- | ------------------------------------------------------------------------------ |
|
|
56
|
+
| 0 | Ontology is valid (check) / uploaded (upload) |
|
|
57
|
+
| 1 | Validation failed (check: findings printed; upload: nothing imported) |
|
|
58
|
+
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits) |
|
|
59
|
+
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response) |
|
|
60
|
+
|
|
61
|
+
Both commands accept up to 2000 YAML files / 5 MB total — far above real
|
|
62
|
+
ontologies (a few hundred small files). Beyond that the CLI fails fast with
|
|
63
|
+
exit 2 before uploading anything; double-check `--base-path` if you hit it.
|
|
64
|
+
|
|
65
|
+
`upload` replaces the project's entire ontology with the uploaded tree. A
|
|
66
|
+
never-published project always goes live immediately on first upload (even
|
|
67
|
+
with `--no-publish`), matching imports from the Cassis app. Publishing is
|
|
68
|
+
idempotent: re-uploading content identical to the published version reports
|
|
69
|
+
that version instead of creating a new one, so re-running the CI job on
|
|
70
|
+
unchanged files is a no-op.
|
|
71
|
+
|
|
72
|
+
### GitHub Actions example
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
jobs:
|
|
76
|
+
ontology-check:
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
steps:
|
|
79
|
+
- uses: actions/checkout@v4
|
|
80
|
+
- uses: actions/setup-python@v5
|
|
81
|
+
with:
|
|
82
|
+
python-version: "3.12"
|
|
83
|
+
- run: pip install cassis-cli
|
|
84
|
+
- run: cassis ontology check
|
|
85
|
+
env:
|
|
86
|
+
CASSIS_API_KEY: ${{ secrets.CASSIS_API_KEY }}
|
|
87
|
+
|
|
88
|
+
ontology-publish:
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
if: github.ref == 'refs/heads/main'
|
|
91
|
+
steps:
|
|
92
|
+
- uses: actions/checkout@v4
|
|
93
|
+
- uses: actions/setup-python@v5
|
|
94
|
+
with:
|
|
95
|
+
python-version: "3.12"
|
|
96
|
+
- run: pip install cassis-cli
|
|
97
|
+
- run: cassis ontology upload
|
|
98
|
+
env:
|
|
99
|
+
CASSIS_API_KEY: ${{ secrets.CASSIS_API_KEY }}
|
|
100
|
+
CASSIS_PROJECT_ID: ${{ vars.CASSIS_PROJECT_ID }}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### GitLab CI example
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
ontology-check:
|
|
107
|
+
image: python:3.12-slim
|
|
108
|
+
script:
|
|
109
|
+
- pip install cassis-cli
|
|
110
|
+
- cassis ontology check
|
|
111
|
+
variables:
|
|
112
|
+
CASSIS_API_KEY: $CASSIS_API_KEY
|
|
113
|
+
|
|
114
|
+
ontology-publish:
|
|
115
|
+
image: python:3.12-slim
|
|
116
|
+
rules:
|
|
117
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
118
|
+
script:
|
|
119
|
+
- pip install cassis-cli
|
|
120
|
+
- cassis ontology upload
|
|
121
|
+
variables:
|
|
122
|
+
CASSIS_API_KEY: $CASSIS_API_KEY
|
|
123
|
+
CASSIS_PROJECT_ID: $CASSIS_PROJECT_ID
|
|
124
|
+
```
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
def post_ontology_check(
|
|
26
|
+
*,
|
|
27
|
+
api_url: str,
|
|
28
|
+
api_key: str,
|
|
29
|
+
files: dict[str, str],
|
|
30
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
"""POST the ontology tree to /api/ci/ontology-check and return the response body."""
|
|
33
|
+
url = api_url.rstrip("/") + "/api/ci/ontology-check"
|
|
34
|
+
try:
|
|
35
|
+
with httpx.Client(timeout=TIMEOUT_SECONDS, transport=transport) as client:
|
|
36
|
+
response = client.post(
|
|
37
|
+
url,
|
|
38
|
+
json={"files": files},
|
|
39
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
40
|
+
)
|
|
41
|
+
except httpx.HTTPError as exc:
|
|
42
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
43
|
+
|
|
44
|
+
if response.status_code == 401:
|
|
45
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
46
|
+
if response.status_code >= 400:
|
|
47
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
48
|
+
try:
|
|
49
|
+
result = response.json()
|
|
50
|
+
except ValueError as exc: # json.JSONDecodeError — e.g. a proxy or portal answering HTML with a 200
|
|
51
|
+
raise ApiError(
|
|
52
|
+
f"The Cassis API at {url} returned a non-JSON response — check the API URL and any proxy in between."
|
|
53
|
+
) from exc
|
|
54
|
+
if (
|
|
55
|
+
not isinstance(result, dict)
|
|
56
|
+
or not all(key in result for key in ("passed", "title", "summary"))
|
|
57
|
+
or not isinstance(result.get("findings"), list)
|
|
58
|
+
):
|
|
59
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def post_ontology_import(
|
|
64
|
+
*,
|
|
65
|
+
api_url: str,
|
|
66
|
+
api_key: str,
|
|
67
|
+
project_id: str,
|
|
68
|
+
files: dict[str, str],
|
|
69
|
+
publish: bool,
|
|
70
|
+
label: Optional[str] = None,
|
|
71
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
72
|
+
) -> dict[str, Any]:
|
|
73
|
+
"""POST the ontology tree to /api/ci/projects/{project_id}/ontology/import and return the response body."""
|
|
74
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/ontology/import"
|
|
75
|
+
body: dict[str, Any] = {"files": files, "publish": publish}
|
|
76
|
+
if label is not None:
|
|
77
|
+
body["label"] = label
|
|
78
|
+
try:
|
|
79
|
+
with httpx.Client(timeout=TIMEOUT_SECONDS, transport=transport) as client:
|
|
80
|
+
response = client.post(
|
|
81
|
+
url,
|
|
82
|
+
json=body,
|
|
83
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
84
|
+
)
|
|
85
|
+
except httpx.HTTPError as exc:
|
|
86
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
87
|
+
|
|
88
|
+
if response.status_code == 401:
|
|
89
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
90
|
+
if response.status_code == 400:
|
|
91
|
+
try:
|
|
92
|
+
detail = response.json().get("detail") or response.text[:500]
|
|
93
|
+
except ValueError:
|
|
94
|
+
detail = response.text[:500]
|
|
95
|
+
raise UploadValidationError(str(detail))
|
|
96
|
+
if response.status_code in (403, 404):
|
|
97
|
+
raise ApiError(
|
|
98
|
+
f"Project not found or not accessible (HTTP {response.status_code}). Check --project and that "
|
|
99
|
+
"the API key belongs to the project's organization and can edit it."
|
|
100
|
+
)
|
|
101
|
+
if response.status_code >= 400:
|
|
102
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
103
|
+
try:
|
|
104
|
+
result = response.json()
|
|
105
|
+
except ValueError as exc: # json.JSONDecodeError — e.g. a proxy or portal answering HTML with a 200
|
|
106
|
+
raise ApiError(
|
|
107
|
+
f"The Cassis API at {url} returned a non-JSON response — check the API URL and any proxy in between."
|
|
108
|
+
) from exc
|
|
109
|
+
if not isinstance(result, dict) or not all(
|
|
110
|
+
key in result for key in ("domain_count", "table_count", "join_count", "metric_count", "published_version")
|
|
111
|
+
):
|
|
112
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
113
|
+
return result
|
|
@@ -5,9 +5,17 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Optional
|
|
8
|
+
from uuid import UUID
|
|
8
9
|
|
|
9
10
|
import typer
|
|
10
|
-
from cassis_cli.api import
|
|
11
|
+
from cassis_cli.api import (
|
|
12
|
+
DEFAULT_API_URL,
|
|
13
|
+
ApiError,
|
|
14
|
+
AuthError,
|
|
15
|
+
UploadValidationError,
|
|
16
|
+
post_ontology_check,
|
|
17
|
+
post_ontology_import,
|
|
18
|
+
)
|
|
11
19
|
|
|
12
20
|
app = typer.Typer(no_args_is_help=True, help="Ontology commands.")
|
|
13
21
|
|
|
@@ -46,38 +54,8 @@ def _collect_files(ontology_dir: Path) -> dict[str, str]:
|
|
|
46
54
|
return files
|
|
47
55
|
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
path: Path = typer.Argument(
|
|
52
|
-
Path("."),
|
|
53
|
-
help="Repository checkout root (the directory containing the ontology export path).",
|
|
54
|
-
),
|
|
55
|
-
api_key: Optional[str] = typer.Option(
|
|
56
|
-
None,
|
|
57
|
-
"--api-key",
|
|
58
|
-
envvar="CASSIS_API_KEY",
|
|
59
|
-
help="Cassis API key (sk-k6-...). Create one in Organization settings -> API keys.",
|
|
60
|
-
),
|
|
61
|
-
api_url: str = typer.Option(
|
|
62
|
-
DEFAULT_API_URL,
|
|
63
|
-
"--api-url",
|
|
64
|
-
envvar="CASSIS_API_URL",
|
|
65
|
-
help="Cassis API base URL.",
|
|
66
|
-
),
|
|
67
|
-
base_path: str = typer.Option(
|
|
68
|
-
DEFAULT_BASE_PATH,
|
|
69
|
-
"--base-path",
|
|
70
|
-
envvar="CASSIS_BASE_PATH",
|
|
71
|
-
help="Repository directory the ontology is exported under (the project's git-sync Path setting).",
|
|
72
|
-
),
|
|
73
|
-
json_output: bool = typer.Option(False, "--json", help="Print the raw JSON response."),
|
|
74
|
-
) -> None:
|
|
75
|
-
"""Validate the ontology files in a repository checkout.
|
|
76
|
-
|
|
77
|
-
Runs the same checks as the Cassis GitHub PR check (YAML parsing,
|
|
78
|
-
round-trip, import validation). Exits 0 when valid, 1 when validation
|
|
79
|
-
fails, 2 on usage errors, 3 on transport/API errors.
|
|
80
|
-
"""
|
|
57
|
+
def _require_api_key(api_key: Optional[str]) -> str:
|
|
58
|
+
"""Exit 2 (usage) when no API key was provided."""
|
|
81
59
|
if not api_key:
|
|
82
60
|
typer.secho(
|
|
83
61
|
"No API key. Set CASSIS_API_KEY or pass --api-key "
|
|
@@ -86,7 +64,16 @@ def check(
|
|
|
86
64
|
err=True,
|
|
87
65
|
)
|
|
88
66
|
raise typer.Exit(EXIT_USAGE)
|
|
67
|
+
return api_key
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _collect_tree(path: Path, base_path: str) -> "tuple[dict[str, str], str]":
|
|
71
|
+
"""Resolve the ontology dir under the checkout and read its YAML tree.
|
|
89
72
|
|
|
73
|
+
Returns ``(files, normalized_base_path)``. Exits 2 (usage) on an empty or
|
|
74
|
+
missing dir, or a tree beyond the API request ceilings — all local checkout
|
|
75
|
+
problems, reported before anything is sent to the API.
|
|
76
|
+
"""
|
|
90
77
|
base_path = base_path.strip().strip("/")
|
|
91
78
|
if not base_path:
|
|
92
79
|
typer.secho("--base-path must not be empty.", fg=typer.colors.RED, err=True)
|
|
@@ -110,13 +97,50 @@ def check(
|
|
|
110
97
|
total_bytes = sum(len(content.encode()) for content in files.values())
|
|
111
98
|
if len(files) > MAX_FILES or total_bytes > MAX_TOTAL_BYTES:
|
|
112
99
|
typer.secho(
|
|
113
|
-
f"Ontology tree too large
|
|
100
|
+
f"Ontology tree too large: {len(files)} files / {total_bytes / (1024 * 1024):.1f} MB "
|
|
114
101
|
f"(limits: {MAX_FILES} files / {MAX_TOTAL_BYTES // (1024 * 1024)} MB). "
|
|
115
102
|
"Check that --base-path points at the ontology directory, not a larger tree.",
|
|
116
103
|
fg=typer.colors.RED,
|
|
117
104
|
err=True,
|
|
118
105
|
)
|
|
119
106
|
raise typer.Exit(EXIT_USAGE)
|
|
107
|
+
return files, base_path
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@app.command()
|
|
111
|
+
def check(
|
|
112
|
+
path: Path = typer.Argument(
|
|
113
|
+
Path("."),
|
|
114
|
+
help="Repository checkout root (the directory containing the ontology export path).",
|
|
115
|
+
),
|
|
116
|
+
api_key: Optional[str] = typer.Option(
|
|
117
|
+
None,
|
|
118
|
+
"--api-key",
|
|
119
|
+
envvar="CASSIS_API_KEY",
|
|
120
|
+
help="Cassis API key (sk-k6-...). Create one in Organization settings -> API keys.",
|
|
121
|
+
),
|
|
122
|
+
api_url: str = typer.Option(
|
|
123
|
+
DEFAULT_API_URL,
|
|
124
|
+
"--api-url",
|
|
125
|
+
envvar="CASSIS_API_URL",
|
|
126
|
+
help="Cassis API base URL.",
|
|
127
|
+
),
|
|
128
|
+
base_path: str = typer.Option(
|
|
129
|
+
DEFAULT_BASE_PATH,
|
|
130
|
+
"--base-path",
|
|
131
|
+
envvar="CASSIS_BASE_PATH",
|
|
132
|
+
help="Repository directory the ontology is exported under (the project's git-sync Path setting).",
|
|
133
|
+
),
|
|
134
|
+
json_output: bool = typer.Option(False, "--json", help="Print the raw JSON response."),
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Validate the ontology files in a repository checkout.
|
|
137
|
+
|
|
138
|
+
Runs the same checks as the Cassis GitHub PR check (YAML parsing,
|
|
139
|
+
round-trip, import validation). Exits 0 when valid, 1 when validation
|
|
140
|
+
fails, 2 on usage errors, 3 on transport/API errors.
|
|
141
|
+
"""
|
|
142
|
+
api_key = _require_api_key(api_key)
|
|
143
|
+
files, base_path = _collect_tree(path, base_path)
|
|
120
144
|
|
|
121
145
|
try:
|
|
122
146
|
result = post_ontology_check(api_url=api_url, api_key=api_key, files=files)
|
|
@@ -139,3 +163,94 @@ def check(
|
|
|
139
163
|
typer.echo(f" {location}{finding.get('message', '')} ({finding.get('stage', '?')})")
|
|
140
164
|
|
|
141
165
|
raise typer.Exit(EXIT_OK if result["passed"] else EXIT_VALIDATION_FAILED)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@app.command()
|
|
169
|
+
def upload(
|
|
170
|
+
path: Path = typer.Argument(
|
|
171
|
+
Path("."),
|
|
172
|
+
help="Repository checkout root (the directory containing the ontology export path).",
|
|
173
|
+
),
|
|
174
|
+
project_id: str = typer.Option(
|
|
175
|
+
...,
|
|
176
|
+
"--project",
|
|
177
|
+
envvar="CASSIS_PROJECT_ID",
|
|
178
|
+
help="Target Cassis project ID (UUID, shown in the project's URL).",
|
|
179
|
+
),
|
|
180
|
+
api_key: Optional[str] = typer.Option(
|
|
181
|
+
None,
|
|
182
|
+
"--api-key",
|
|
183
|
+
envvar="CASSIS_API_KEY",
|
|
184
|
+
help="Cassis API key (sk-k6-...). Create one in Organization settings -> API keys.",
|
|
185
|
+
),
|
|
186
|
+
api_url: str = typer.Option(
|
|
187
|
+
DEFAULT_API_URL,
|
|
188
|
+
"--api-url",
|
|
189
|
+
envvar="CASSIS_API_URL",
|
|
190
|
+
help="Cassis API base URL.",
|
|
191
|
+
),
|
|
192
|
+
base_path: str = typer.Option(
|
|
193
|
+
DEFAULT_BASE_PATH,
|
|
194
|
+
"--base-path",
|
|
195
|
+
envvar="CASSIS_BASE_PATH",
|
|
196
|
+
help="Repository directory the ontology is exported under (the project's git-sync Path setting).",
|
|
197
|
+
),
|
|
198
|
+
publish: bool = typer.Option(
|
|
199
|
+
True,
|
|
200
|
+
"--publish/--no-publish",
|
|
201
|
+
help="Publish the uploaded ontology immediately as a new version (default: publish).",
|
|
202
|
+
),
|
|
203
|
+
label: Optional[str] = typer.Option(None, "--label", help="Label for the published version."),
|
|
204
|
+
json_output: bool = typer.Option(False, "--json", help="Print the raw JSON response."),
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Upload the ontology files in a repository checkout to a Cassis project.
|
|
207
|
+
|
|
208
|
+
Replaces the project's unpublished ontology with the local tree (full
|
|
209
|
+
replace) and, unless --no-publish is passed, publishes it immediately as a
|
|
210
|
+
new version. Exits 0 on success, 1 when the tree fails validation, 2 on
|
|
211
|
+
usage errors, 3 on transport/API errors.
|
|
212
|
+
"""
|
|
213
|
+
api_key = _require_api_key(api_key)
|
|
214
|
+
try:
|
|
215
|
+
UUID(project_id)
|
|
216
|
+
except ValueError:
|
|
217
|
+
typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
|
|
218
|
+
raise typer.Exit(EXIT_USAGE)
|
|
219
|
+
files, base_path = _collect_tree(path, base_path)
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
result = post_ontology_import(
|
|
223
|
+
api_url=api_url,
|
|
224
|
+
api_key=api_key,
|
|
225
|
+
project_id=project_id,
|
|
226
|
+
files=files,
|
|
227
|
+
publish=publish,
|
|
228
|
+
label=label,
|
|
229
|
+
)
|
|
230
|
+
except AuthError as exc:
|
|
231
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
232
|
+
raise typer.Exit(EXIT_TRANSPORT) from exc
|
|
233
|
+
except UploadValidationError as exc:
|
|
234
|
+
typer.secho("Ontology upload rejected:", fg=typer.colors.RED, bold=True, err=True)
|
|
235
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
236
|
+
raise typer.Exit(EXIT_VALIDATION_FAILED) from exc
|
|
237
|
+
except ApiError as exc:
|
|
238
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
239
|
+
raise typer.Exit(EXIT_TRANSPORT) from exc
|
|
240
|
+
|
|
241
|
+
if json_output:
|
|
242
|
+
typer.echo(json.dumps(result, indent=2))
|
|
243
|
+
else:
|
|
244
|
+
counts = (
|
|
245
|
+
f"{result['table_count']} tables, {result['domain_count']} domains, "
|
|
246
|
+
f"{result['join_count']} joins, {result['metric_count']} metrics"
|
|
247
|
+
)
|
|
248
|
+
version = result.get("published_version")
|
|
249
|
+
if version is not None:
|
|
250
|
+
typer.secho(f"✓ Ontology uploaded and published as v{version} ({counts}).", fg=typer.colors.GREEN)
|
|
251
|
+
else:
|
|
252
|
+
typer.secho(
|
|
253
|
+
f"✓ Ontology uploaded ({counts}). Not published — it is now the project's unpublished ontology.",
|
|
254
|
+
fg=typer.colors.GREEN,
|
|
255
|
+
)
|
|
256
|
+
raise typer.Exit(EXIT_OK)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cassis-cli"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "Cassis CLI — run Cassis actions (ontology validation) from your CI pipelines"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Cassis CLI — run Cassis actions (ontology validation, upload and publish) from your CI pipelines"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "Proprietary" }
|
|
7
7
|
authors = [{ name = "Cassis", email = "tech.admin@getcassis.com" }]
|
cassis_cli-0.1.0/PKG-INFO
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: cassis-cli
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Cassis CLI — run Cassis actions (ontology validation) from your CI pipelines
|
|
5
|
-
License: Proprietary
|
|
6
|
-
Keywords: cassis,ontology,ci,text-to-sql
|
|
7
|
-
Author: Cassis
|
|
8
|
-
Author-email: tech.admin@getcassis.com
|
|
9
|
-
Requires-Python: >=3.10,<4.0
|
|
10
|
-
Classifier: License :: Other/Proprietary License
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
-
Requires-Dist: httpx (>=0.24,<1.0)
|
|
18
|
-
Requires-Dist: typer (>=0.12,<1.0)
|
|
19
|
-
Description-Content-Type: text/markdown
|
|
20
|
-
|
|
21
|
-
# Cassis CLI
|
|
22
|
-
|
|
23
|
-
Run Cassis actions from your CI pipelines. The first command, `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.
|
|
24
|
-
|
|
25
|
-
## Install
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
pip install cassis-cli
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Setup
|
|
32
|
-
|
|
33
|
-
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
34
|
-
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
35
|
-
|
|
36
|
-
## Usage
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
# From the root of a repository synced with Cassis (contains the ontology export directory, cassis/ by default):
|
|
40
|
-
cassis ontology check
|
|
41
|
-
|
|
42
|
-
# Or point at the checkout explicitly:
|
|
43
|
-
cassis ontology check /path/to/checkout
|
|
44
|
-
|
|
45
|
-
# Machine-readable output:
|
|
46
|
-
cassis ontology check --json
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Configuration (flags take precedence over env vars):
|
|
50
|
-
|
|
51
|
-
| Flag | Env var | Default |
|
|
52
|
-
| ----------- | ---------------- | --------------------------- |
|
|
53
|
-
| `--api-key` | `CASSIS_API_KEY` | — (required) |
|
|
54
|
-
| `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
|
|
55
|
-
| `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
|
|
56
|
-
|
|
57
|
-
### Exit codes
|
|
58
|
-
|
|
59
|
-
| Code | Meaning |
|
|
60
|
-
| ---- | ------------------------------------------------------------------------------ |
|
|
61
|
-
| 0 | Ontology is valid |
|
|
62
|
-
| 1 | Validation failed (findings printed) |
|
|
63
|
-
| 2 | Usage error (missing API key, no ontology directory, unreadable file, tree over the size limits) |
|
|
64
|
-
| 3 | Transport/API error (unreachable API, invalid key, unexpected response) |
|
|
65
|
-
|
|
66
|
-
The check accepts up to 2000 YAML files / 5 MB total — far above real
|
|
67
|
-
ontologies (a few hundred small files). Beyond that the CLI fails fast with
|
|
68
|
-
exit 2 before uploading anything; double-check `--base-path` if you hit it.
|
|
69
|
-
|
|
70
|
-
### GitHub Actions example
|
|
71
|
-
|
|
72
|
-
```yaml
|
|
73
|
-
jobs:
|
|
74
|
-
ontology-check:
|
|
75
|
-
runs-on: ubuntu-latest
|
|
76
|
-
steps:
|
|
77
|
-
- uses: actions/checkout@v4
|
|
78
|
-
- uses: actions/setup-python@v5
|
|
79
|
-
with:
|
|
80
|
-
python-version: "3.12"
|
|
81
|
-
- run: pip install cassis-cli
|
|
82
|
-
- run: cassis ontology check
|
|
83
|
-
env:
|
|
84
|
-
CASSIS_API_KEY: ${{ secrets.CASSIS_API_KEY }}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### GitLab CI example
|
|
88
|
-
|
|
89
|
-
```yaml
|
|
90
|
-
ontology-check:
|
|
91
|
-
image: python:3.12-slim
|
|
92
|
-
script:
|
|
93
|
-
- pip install cassis-cli
|
|
94
|
-
- cassis ontology check
|
|
95
|
-
variables:
|
|
96
|
-
CASSIS_API_KEY: $CASSIS_API_KEY
|
|
97
|
-
```
|
|
98
|
-
|
cassis_cli-0.1.0/README.md
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# Cassis CLI
|
|
2
|
-
|
|
3
|
-
Run Cassis actions from your CI pipelines. The first command, `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.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
pip install cassis-cli
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Setup
|
|
12
|
-
|
|
13
|
-
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
14
|
-
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
15
|
-
|
|
16
|
-
## Usage
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
# From the root of a repository synced with Cassis (contains the ontology export directory, cassis/ by default):
|
|
20
|
-
cassis ontology check
|
|
21
|
-
|
|
22
|
-
# Or point at the checkout explicitly:
|
|
23
|
-
cassis ontology check /path/to/checkout
|
|
24
|
-
|
|
25
|
-
# Machine-readable output:
|
|
26
|
-
cassis ontology check --json
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Configuration (flags take precedence over env vars):
|
|
30
|
-
|
|
31
|
-
| Flag | Env var | Default |
|
|
32
|
-
| ----------- | ---------------- | --------------------------- |
|
|
33
|
-
| `--api-key` | `CASSIS_API_KEY` | — (required) |
|
|
34
|
-
| `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
|
|
35
|
-
| `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
|
|
36
|
-
|
|
37
|
-
### Exit codes
|
|
38
|
-
|
|
39
|
-
| Code | Meaning |
|
|
40
|
-
| ---- | ------------------------------------------------------------------------------ |
|
|
41
|
-
| 0 | Ontology is valid |
|
|
42
|
-
| 1 | Validation failed (findings printed) |
|
|
43
|
-
| 2 | Usage error (missing API key, no ontology directory, unreadable file, tree over the size limits) |
|
|
44
|
-
| 3 | Transport/API error (unreachable API, invalid key, unexpected response) |
|
|
45
|
-
|
|
46
|
-
The check accepts up to 2000 YAML files / 5 MB total — far above real
|
|
47
|
-
ontologies (a few hundred small files). Beyond that the CLI fails fast with
|
|
48
|
-
exit 2 before uploading anything; double-check `--base-path` if you hit it.
|
|
49
|
-
|
|
50
|
-
### GitHub Actions example
|
|
51
|
-
|
|
52
|
-
```yaml
|
|
53
|
-
jobs:
|
|
54
|
-
ontology-check:
|
|
55
|
-
runs-on: ubuntu-latest
|
|
56
|
-
steps:
|
|
57
|
-
- uses: actions/checkout@v4
|
|
58
|
-
- uses: actions/setup-python@v5
|
|
59
|
-
with:
|
|
60
|
-
python-version: "3.12"
|
|
61
|
-
- run: pip install cassis-cli
|
|
62
|
-
- run: cassis ontology check
|
|
63
|
-
env:
|
|
64
|
-
CASSIS_API_KEY: ${{ secrets.CASSIS_API_KEY }}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### GitLab CI example
|
|
68
|
-
|
|
69
|
-
```yaml
|
|
70
|
-
ontology-check:
|
|
71
|
-
image: python:3.12-slim
|
|
72
|
-
script:
|
|
73
|
-
- pip install cassis-cli
|
|
74
|
-
- cassis ontology check
|
|
75
|
-
variables:
|
|
76
|
-
CASSIS_API_KEY: $CASSIS_API_KEY
|
|
77
|
-
```
|
|
@@ -1,56 +0,0 @@
|
|
|
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
|
-
def post_ontology_check(
|
|
22
|
-
*,
|
|
23
|
-
api_url: str,
|
|
24
|
-
api_key: str,
|
|
25
|
-
files: dict[str, str],
|
|
26
|
-
transport: Optional[httpx.BaseTransport] = None,
|
|
27
|
-
) -> dict[str, Any]:
|
|
28
|
-
"""POST the ontology tree to /api/ci/ontology-check and return the response body."""
|
|
29
|
-
url = api_url.rstrip("/") + "/api/ci/ontology-check"
|
|
30
|
-
try:
|
|
31
|
-
with httpx.Client(timeout=TIMEOUT_SECONDS, transport=transport) as client:
|
|
32
|
-
response = client.post(
|
|
33
|
-
url,
|
|
34
|
-
json={"files": files},
|
|
35
|
-
headers={"Authorization": f"Bearer {api_key}"},
|
|
36
|
-
)
|
|
37
|
-
except httpx.HTTPError as exc:
|
|
38
|
-
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
39
|
-
|
|
40
|
-
if response.status_code == 401:
|
|
41
|
-
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
42
|
-
if response.status_code >= 400:
|
|
43
|
-
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
44
|
-
try:
|
|
45
|
-
result = response.json()
|
|
46
|
-
except ValueError as exc: # json.JSONDecodeError — e.g. a proxy or portal answering HTML with a 200
|
|
47
|
-
raise ApiError(
|
|
48
|
-
f"The Cassis API at {url} returned a non-JSON response — check the API URL and any proxy in between."
|
|
49
|
-
) from exc
|
|
50
|
-
if (
|
|
51
|
-
not isinstance(result, dict)
|
|
52
|
-
or not all(key in result for key in ("passed", "title", "summary"))
|
|
53
|
-
or not isinstance(result.get("findings"), list)
|
|
54
|
-
):
|
|
55
|
-
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
56
|
-
return result
|
|
File without changes
|