cassis-cli 1.1.0__tar.gz → 1.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-1.2.0/LICENSE +202 -0
- cassis_cli-1.2.0/NOTICE +4 -0
- cassis_cli-1.1.0/README.md → cassis_cli-1.2.0/PKG-INFO +51 -4
- cassis_cli-1.1.0/PKG-INFO → cassis_cli-1.2.0/README.md +27 -24
- {cassis_cli-1.1.0 → cassis_cli-1.2.0}/cassis_cli/__init__.py +1 -1
- {cassis_cli-1.1.0 → cassis_cli-1.2.0}/cassis_cli/api.py +102 -2
- {cassis_cli-1.1.0 → cassis_cli-1.2.0}/cassis_cli/common.py +11 -4
- {cassis_cli-1.1.0 → cassis_cli-1.2.0}/cassis_cli/eval.py +132 -0
- {cassis_cli-1.1.0 → cassis_cli-1.2.0}/cassis_cli/guide.py +1 -1
- {cassis_cli-1.1.0 → cassis_cli-1.2.0}/cassis_cli/main.py +2 -0
- {cassis_cli-1.1.0 → cassis_cli-1.2.0}/cassis_cli/ontology.py +43 -3
- {cassis_cli-1.1.0 → cassis_cli-1.2.0}/cassis_cli/ontology_design_guide.md +35 -1
- cassis_cli-1.2.0/cassis_cli/schema.py +129 -0
- {cassis_cli-1.1.0 → cassis_cli-1.2.0}/pyproject.toml +13 -2
cassis_cli-1.2.0/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright 2026 Polymorph SAS
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
cassis_cli-1.2.0/NOTICE
ADDED
|
@@ -1,8 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cassis-cli
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Cassis CLI — run Cassis actions (ontology validation, upload and publish) from your CI pipelines
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
License-File: NOTICE
|
|
8
|
+
Keywords: cassis,ontology,ci,text-to-sql
|
|
9
|
+
Author: Cassis
|
|
10
|
+
Author-email: tech.admin@getcassis.com
|
|
11
|
+
Requires-Python: >=3.10,<4.0
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Topic :: Database
|
|
16
|
+
Requires-Dist: httpx (>=0.24,<1.0)
|
|
17
|
+
Requires-Dist: typer (>=0.12,<1.0)
|
|
18
|
+
Project-URL: Documentation, https://github.com/GetCassis/cassis-cli#readme
|
|
19
|
+
Project-URL: Homepage, https://getcassis.com
|
|
20
|
+
Project-URL: Repository, https://github.com/GetCassis/cassis-cli
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
1
23
|
# Cassis CLI
|
|
2
24
|
|
|
3
25
|
Run Cassis actions from your CI pipelines:
|
|
4
26
|
|
|
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.
|
|
27
|
+
- `cassis ontology check` validates the ontology files in your repository with the exact same checks as the Cassis GitHub PR check (YAML parsing, round-trip, import validation) — so you can gate merges in any CI system, not just GitHub. In a checkout bound to a project (`project.yml`, `--project`, or `CASSIS_PROJECT_ID`), it also cross-checks the tree against the project's source schema: references to tables or columns the warehouse doesn't have print as **warnings** — advisory only (the object may simply not be built or synced yet), never a failed check.
|
|
28
|
+
- `cassis schema pull` downloads the data source's full source schema (as Cassis last introspected it) into `<base-path>/.schema.json` — a **gitignored** local snapshot (the command maintains the ignore entry) with a `pulled_at` stamp. The warehouse stays authoritative; the snapshot is a cache for offline/bulk work — e.g. a coding agent grepping table and column names during a modeling pass instead of paging through the MCP `get_source_schema` tool. Re-run to refresh.
|
|
6
29
|
- `cassis ontology fmt` rewrites the ontology files in canonical form (think `black`/`gofmt` for the ontology), so hand or agent edits pass the round-trip check.
|
|
7
30
|
- `cassis ontology upload` uploads the ontology files to a Cassis project (full replace) and, by default, publishes them immediately as a new version — so a merge to your main branch can go live in one CI step.
|
|
8
31
|
- `cassis ontology pull` downloads the project's unpublished ontology into your repository checkout (full sync — stale local ontology files are pruned), so you can start editing from the current state, or bootstrap a repo that isn't git-synced (e.g. Bitbucket).
|
|
@@ -11,6 +34,7 @@ Run Cassis actions from your CI pipelines:
|
|
|
11
34
|
- `cassis eval run` runs the project's eval suite against your local ontology files (scored in-memory — nothing is pushed to Cassis) and prints per-question results, so you can test the changes on your git branch before merging.
|
|
12
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.
|
|
13
36
|
- `cassis eval add-case` adds a gold question/SQL case to the project's eval suite — after fixing an ontology issue, add the question users were failing on so `eval run` guards it from regressing.
|
|
37
|
+
- `cassis eval list-cases` and `cassis eval delete-case` maintain the suite: list the current cases with their ids, and prune one that is stale or wrong (e.g. its gold SQL encodes a definition the ontology has since changed).
|
|
14
38
|
|
|
15
39
|
## Install
|
|
16
40
|
|
|
@@ -32,7 +56,7 @@ The ontology tree under `<base-path>` (default `cassis/`) is:
|
|
|
32
56
|
|
|
33
57
|
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
34
58
|
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
35
|
-
3. For `pull`, `upload`, `eval run`, `ontology test`, and `eval add-case
|
|
59
|
+
3. For `pull`, `upload`, `schema pull`, `eval run`, `ontology test`, and the `eval` case commands (`add-case`, `list-cases`, `delete-case`): the project ID (UUID) is taken from `<base-path>/project.yml` in the checkout (written by `pull` and by publishing) — so once a repo is pulled you don't need to pass it. To override, or before the first pull, set `CASSIS_PROJECT_ID` or pass `--project` (find the UUID in the project's URL). `ontology check` uses the same resolution but treats it as optional: unbound checkouts get the project-less validation (no schema reference warnings).
|
|
36
60
|
|
|
37
61
|
## Usage
|
|
38
62
|
|
|
@@ -79,6 +103,13 @@ cassis ontology test --project ... -q "How much was refunded last month?" -q "Ne
|
|
|
79
103
|
# Add a gold case to the eval suite (rejected if the exact question already exists):
|
|
80
104
|
cassis eval add-case --project ... -q "How much was refunded last month?" \
|
|
81
105
|
--gold-sql "SELECT SUM(refunded_cents) / 100.0 FROM public.orders WHERE ..."
|
|
106
|
+
|
|
107
|
+
# List the suite's cases (id + question; --json adds the gold SQL), then prune one:
|
|
108
|
+
cassis eval list-cases --project ...
|
|
109
|
+
cassis eval delete-case 019f0000-0000-7000-8000-0000000000ca --project ...
|
|
110
|
+
|
|
111
|
+
# Pull the source schema into <base-path>/.schema.json (gitignored local snapshot):
|
|
112
|
+
cassis schema pull
|
|
82
113
|
```
|
|
83
114
|
|
|
84
115
|
Configuration (flags take precedence over env vars):
|
|
@@ -88,7 +119,7 @@ Configuration (flags take precedence over env vars):
|
|
|
88
119
|
| `--api-key` | `CASSIS_API_KEY` | — (required) |
|
|
89
120
|
| `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
|
|
90
121
|
| `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
|
|
91
|
-
| `--project` (pull, upload, eval run, eval add-case, test) | `CASSIS_PROJECT_ID` |
|
|
122
|
+
| `--project` (check, pull, upload, schema pull, eval run, eval add-case, eval list-cases, eval delete-case, test) | `CASSIS_PROJECT_ID` | the id in `<base-path>/project.yml` (required before the first pull; `check` alone falls back to the project-less validation when unbound) |
|
|
92
123
|
|
|
93
124
|
`cassis eval run` also accepts `--label` (run label in the Evals page; defaults
|
|
94
125
|
to the branch name from the CI environment or the local git checkout; rejected
|
|
@@ -117,7 +148,7 @@ cassis ontology fmt --check
|
|
|
117
148
|
| Code | Meaning |
|
|
118
149
|
| ---- | ------------------------------------------------------------------------------ |
|
|
119
150
|
| 0 | Ontology is valid (check) / pulled (pull) / uploaded (upload) / eval run completed all-passed (eval run) / every probe completed (test — whatever its outcome; probes are informational, don't gate CI on them) |
|
|
120
|
-
| 1 | Validation failed (check: findings printed; upload: nothing imported; eval run: invalid tree, failed cases, or failed/cancelled run; test: invalid tree or a probe failed) |
|
|
151
|
+
| 1 | Validation failed (check: findings printed; upload: nothing imported; eval run: invalid tree, failed cases, or failed/cancelled run; test: invalid tree or a probe failed; add-case: duplicate question or gold SQL that does not run; delete-case: no such case in the project) |
|
|
121
152
|
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits) |
|
|
122
153
|
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, out of credits, or `--timeout` reached |
|
|
123
154
|
|
|
@@ -211,3 +242,19 @@ ontology-publish:
|
|
|
211
242
|
CASSIS_API_KEY: $CASSIS_API_KEY
|
|
212
243
|
CASSIS_PROJECT_ID: $CASSIS_PROJECT_ID
|
|
213
244
|
```
|
|
245
|
+
|
|
246
|
+
## About this repository
|
|
247
|
+
|
|
248
|
+
[github.com/GetCassis/cassis-cli](https://github.com/GetCassis/cassis-cli) is a
|
|
249
|
+
read-only mirror, synced automatically from the Cassis monorepo where the CLI is
|
|
250
|
+
developed. Issues are welcome and watched; pull requests can't be merged here, so
|
|
251
|
+
open an issue (or mail tech.admin@getcassis.com) and we'll port the patch upstream
|
|
252
|
+
with credit.
|
|
253
|
+
|
|
254
|
+
Only the CLI is open source. The Cassis backend it talks to is proprietary and
|
|
255
|
+
requires an account.
|
|
256
|
+
|
|
257
|
+
## License
|
|
258
|
+
|
|
259
|
+
Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
|
260
|
+
|
|
@@ -1,28 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: cassis-cli
|
|
3
|
-
Version: 1.1.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
1
|
# Cassis CLI
|
|
22
2
|
|
|
23
3
|
Run Cassis actions from your CI pipelines:
|
|
24
4
|
|
|
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.
|
|
5
|
+
- `cassis ontology check` validates the ontology files in your repository with the exact same checks as the Cassis GitHub PR check (YAML parsing, round-trip, import validation) — so you can gate merges in any CI system, not just GitHub. In a checkout bound to a project (`project.yml`, `--project`, or `CASSIS_PROJECT_ID`), it also cross-checks the tree against the project's source schema: references to tables or columns the warehouse doesn't have print as **warnings** — advisory only (the object may simply not be built or synced yet), never a failed check.
|
|
6
|
+
- `cassis schema pull` downloads the data source's full source schema (as Cassis last introspected it) into `<base-path>/.schema.json` — a **gitignored** local snapshot (the command maintains the ignore entry) with a `pulled_at` stamp. The warehouse stays authoritative; the snapshot is a cache for offline/bulk work — e.g. a coding agent grepping table and column names during a modeling pass instead of paging through the MCP `get_source_schema` tool. Re-run to refresh.
|
|
26
7
|
- `cassis ontology fmt` rewrites the ontology files in canonical form (think `black`/`gofmt` for the ontology), so hand or agent edits pass the round-trip check.
|
|
27
8
|
- `cassis ontology upload` uploads the ontology files to a Cassis project (full replace) and, by default, publishes them immediately as a new version — so a merge to your main branch can go live in one CI step.
|
|
28
9
|
- `cassis ontology pull` downloads the project's unpublished ontology into your repository checkout (full sync — stale local ontology files are pruned), so you can start editing from the current state, or bootstrap a repo that isn't git-synced (e.g. Bitbucket).
|
|
@@ -31,6 +12,7 @@ Run Cassis actions from your CI pipelines:
|
|
|
31
12
|
- `cassis eval run` runs the project's eval suite against your local ontology files (scored in-memory — nothing is pushed to Cassis) and prints per-question results, so you can test the changes on your git branch before merging.
|
|
32
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.
|
|
33
14
|
- `cassis eval add-case` adds a gold question/SQL case to the project's eval suite — after fixing an ontology issue, add the question users were failing on so `eval run` guards it from regressing.
|
|
15
|
+
- `cassis eval list-cases` and `cassis eval delete-case` maintain the suite: list the current cases with their ids, and prune one that is stale or wrong (e.g. its gold SQL encodes a definition the ontology has since changed).
|
|
34
16
|
|
|
35
17
|
## Install
|
|
36
18
|
|
|
@@ -52,7 +34,7 @@ The ontology tree under `<base-path>` (default `cassis/`) is:
|
|
|
52
34
|
|
|
53
35
|
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
54
36
|
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
55
|
-
3. For `pull`, `upload`, `eval run`, `ontology test`, and `eval add-case
|
|
37
|
+
3. For `pull`, `upload`, `schema pull`, `eval run`, `ontology test`, and the `eval` case commands (`add-case`, `list-cases`, `delete-case`): the project ID (UUID) is taken from `<base-path>/project.yml` in the checkout (written by `pull` and by publishing) — so once a repo is pulled you don't need to pass it. To override, or before the first pull, set `CASSIS_PROJECT_ID` or pass `--project` (find the UUID 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).
|
|
56
38
|
|
|
57
39
|
## Usage
|
|
58
40
|
|
|
@@ -99,6 +81,13 @@ cassis ontology test --project ... -q "How much was refunded last month?" -q "Ne
|
|
|
99
81
|
# Add a gold case to the eval suite (rejected if the exact question already exists):
|
|
100
82
|
cassis eval add-case --project ... -q "How much was refunded last month?" \
|
|
101
83
|
--gold-sql "SELECT SUM(refunded_cents) / 100.0 FROM public.orders WHERE ..."
|
|
84
|
+
|
|
85
|
+
# List the suite's cases (id + question; --json adds the gold SQL), then prune one:
|
|
86
|
+
cassis eval list-cases --project ...
|
|
87
|
+
cassis eval delete-case 019f0000-0000-7000-8000-0000000000ca --project ...
|
|
88
|
+
|
|
89
|
+
# Pull the source schema into <base-path>/.schema.json (gitignored local snapshot):
|
|
90
|
+
cassis schema pull
|
|
102
91
|
```
|
|
103
92
|
|
|
104
93
|
Configuration (flags take precedence over env vars):
|
|
@@ -108,7 +97,7 @@ Configuration (flags take precedence over env vars):
|
|
|
108
97
|
| `--api-key` | `CASSIS_API_KEY` | — (required) |
|
|
109
98
|
| `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
|
|
110
99
|
| `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
|
|
111
|
-
| `--project` (pull, upload, eval run, eval add-case, test) | `CASSIS_PROJECT_ID` |
|
|
100
|
+
| `--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) |
|
|
112
101
|
|
|
113
102
|
`cassis eval run` also accepts `--label` (run label in the Evals page; defaults
|
|
114
103
|
to the branch name from the CI environment or the local git checkout; rejected
|
|
@@ -137,7 +126,7 @@ cassis ontology fmt --check
|
|
|
137
126
|
| Code | Meaning |
|
|
138
127
|
| ---- | ------------------------------------------------------------------------------ |
|
|
139
128
|
| 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) |
|
|
140
|
-
| 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) |
|
|
129
|
+
| 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) |
|
|
141
130
|
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits) |
|
|
142
131
|
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, out of credits, or `--timeout` reached |
|
|
143
132
|
|
|
@@ -232,3 +221,17 @@ ontology-publish:
|
|
|
232
221
|
CASSIS_PROJECT_ID: $CASSIS_PROJECT_ID
|
|
233
222
|
```
|
|
234
223
|
|
|
224
|
+
## About this repository
|
|
225
|
+
|
|
226
|
+
[github.com/GetCassis/cassis-cli](https://github.com/GetCassis/cassis-cli) is a
|
|
227
|
+
read-only mirror, synced automatically from the Cassis monorepo where the CLI is
|
|
228
|
+
developed. Issues are welcome and watched; pull requests can't be merged here, so
|
|
229
|
+
open an issue (or mail tech.admin@getcassis.com) and we'll port the patch upstream
|
|
230
|
+
with credit.
|
|
231
|
+
|
|
232
|
+
Only the CLI is open source. The Cassis backend it talks to is proprietary and
|
|
233
|
+
requires an account.
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
|
@@ -134,10 +134,19 @@ def post_ontology_check(
|
|
|
134
134
|
api_url: str,
|
|
135
135
|
api_key: str,
|
|
136
136
|
files: dict[str, str],
|
|
137
|
+
project_id: Optional[str] = None,
|
|
137
138
|
transport: Optional[httpx.BaseTransport] = None,
|
|
138
139
|
) -> dict[str, Any]:
|
|
139
|
-
"""POST the ontology tree to
|
|
140
|
-
|
|
140
|
+
"""POST the ontology tree to the check endpoint and return the response body.
|
|
141
|
+
|
|
142
|
+
With ``project_id``, calls the project-scoped route, which additionally
|
|
143
|
+
cross-checks the tree against the project's source schema and returns
|
|
144
|
+
advisory ``warnings``; without it, the pure tree check.
|
|
145
|
+
"""
|
|
146
|
+
if project_id:
|
|
147
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/ontology/check"
|
|
148
|
+
else:
|
|
149
|
+
url = api_url.rstrip("/") + "/api/ci/ontology-check"
|
|
141
150
|
try:
|
|
142
151
|
with _client(transport=transport) as client:
|
|
143
152
|
response = client.post(
|
|
@@ -150,6 +159,8 @@ def post_ontology_check(
|
|
|
150
159
|
|
|
151
160
|
if response.status_code == 401:
|
|
152
161
|
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
162
|
+
if project_id and response.status_code in (403, 404):
|
|
163
|
+
raise _project_scope_error(response)
|
|
153
164
|
if response.status_code >= 400:
|
|
154
165
|
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
155
166
|
result = _parse_json_response(response, url)
|
|
@@ -232,6 +243,47 @@ def get_ontology_export(
|
|
|
232
243
|
return result["files"]
|
|
233
244
|
|
|
234
245
|
|
|
246
|
+
class NoSourceSchemaError(ApiError):
|
|
247
|
+
"""The project's data source has no introspected schema to pull."""
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def get_schema_export(
|
|
251
|
+
*,
|
|
252
|
+
api_url: str,
|
|
253
|
+
api_key: str,
|
|
254
|
+
project_id: str,
|
|
255
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
256
|
+
) -> dict[str, Any]:
|
|
257
|
+
"""GET /api/ci/projects/{project_id}/schema and return the response body."""
|
|
258
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/schema"
|
|
259
|
+
try:
|
|
260
|
+
with _client(transport=transport) as client:
|
|
261
|
+
response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
262
|
+
except httpx.HTTPError as exc:
|
|
263
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
264
|
+
|
|
265
|
+
if response.status_code == 401:
|
|
266
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
267
|
+
# "No source schema" is the marker the server's export endpoint puts in its
|
|
268
|
+
# 404 detail (backend endpoints/ci.py::export_source_schema — reworded only
|
|
269
|
+
# with a paired CLI release). Without this routing, a schema-less project
|
|
270
|
+
# would surface as the misleading "check --project / key access" hint below.
|
|
271
|
+
if response.status_code == 404 and "No source schema" in response.text:
|
|
272
|
+
raise NoSourceSchemaError(str(_detail_or_text(response)))
|
|
273
|
+
if response.status_code in (400, 403, 404):
|
|
274
|
+
raise _project_scope_error(response)
|
|
275
|
+
if response.status_code >= 400:
|
|
276
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
277
|
+
result = _parse_json_response(response, url)
|
|
278
|
+
if (
|
|
279
|
+
not isinstance(result, dict)
|
|
280
|
+
or not isinstance(result.get("tables"), list)
|
|
281
|
+
or not isinstance(result.get("schema_version"), dict)
|
|
282
|
+
):
|
|
283
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
284
|
+
return result
|
|
285
|
+
|
|
286
|
+
|
|
235
287
|
def post_eval_run_start(
|
|
236
288
|
*,
|
|
237
289
|
api_url: str,
|
|
@@ -321,6 +373,54 @@ def post_eval_case_create(
|
|
|
321
373
|
return result
|
|
322
374
|
|
|
323
375
|
|
|
376
|
+
class EvalCaseNotFoundError(ApiError):
|
|
377
|
+
"""The project has no current eval case with this id."""
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def get_eval_cases(
|
|
381
|
+
*,
|
|
382
|
+
api_url: str,
|
|
383
|
+
api_key: str,
|
|
384
|
+
project_id: str,
|
|
385
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
386
|
+
) -> list[dict[str, Any]]:
|
|
387
|
+
"""GET /api/ci/projects/{project_id}/eval/cases and return the case list."""
|
|
388
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/eval/cases"
|
|
389
|
+
result = _get_eval_json(url, api_key, transport)
|
|
390
|
+
if not isinstance(result, list):
|
|
391
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
392
|
+
return result
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def delete_eval_case(
|
|
396
|
+
*,
|
|
397
|
+
api_url: str,
|
|
398
|
+
api_key: str,
|
|
399
|
+
project_id: str,
|
|
400
|
+
case_id: str,
|
|
401
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
402
|
+
) -> None:
|
|
403
|
+
"""DELETE /api/ci/projects/{project_id}/eval/cases/{case_id}."""
|
|
404
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/eval/cases/{case_id}"
|
|
405
|
+
try:
|
|
406
|
+
with _client(transport=transport) as client:
|
|
407
|
+
response = client.delete(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
408
|
+
except httpx.HTTPError as exc:
|
|
409
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
410
|
+
|
|
411
|
+
if response.status_code == 401:
|
|
412
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
413
|
+
# Exact-match wire contract with the DELETE endpoint's 404 detail (see
|
|
414
|
+
# `delete_eval_case` in backend/app/endpoints/ci.py): it distinguishes a
|
|
415
|
+
# missing case (exit 1) from a project-scope 404 (exit 3).
|
|
416
|
+
if response.status_code == 404 and _detail_or_text(response) == "Eval case not found":
|
|
417
|
+
raise EvalCaseNotFoundError(f"No current eval case {case_id} in this project (already deleted, or wrong id?).")
|
|
418
|
+
if response.status_code in (403, 404):
|
|
419
|
+
raise _project_scope_error(response)
|
|
420
|
+
if response.status_code >= 400:
|
|
421
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
422
|
+
|
|
423
|
+
|
|
324
424
|
def _get_eval_json(url: str, api_key: str, transport: Optional[httpx.BaseTransport]) -> Any:
|
|
325
425
|
"""GET an eval-run URL with the shared error mapping."""
|
|
326
426
|
try:
|
|
@@ -93,20 +93,27 @@ def read_project_id_from_dir(ontology_dir: Path) -> Optional[str]:
|
|
|
93
93
|
return None
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
def resolve_project_id(
|
|
96
|
+
def resolve_project_id(
|
|
97
|
+
project_id: Optional[str], ontology_dir: Path, *, optional: bool = False, quiet: bool = False
|
|
98
|
+
) -> Optional[str]:
|
|
97
99
|
"""Resolve the target project id, defaulting to the checkout's ``project.yml``.
|
|
98
100
|
|
|
99
101
|
Precedence: an explicit ``--project`` / ``CASSIS_PROJECT_ID`` wins; otherwise
|
|
100
102
|
the ``project_id`` recorded in ``<base-path>/project.yml`` (written by
|
|
101
103
|
``pull`` / publish) is used, and where it came from is noted on stderr so a
|
|
102
|
-
stale value in a copied repo is visible
|
|
103
|
-
|
|
104
|
+
stale value in a copied repo is visible (``quiet`` suppresses the note for
|
|
105
|
+
machine-readable output). Exits 2 (usage) when the value isn't a UUID, or —
|
|
106
|
+
unless ``optional`` — when no value is available at all; with ``optional``,
|
|
107
|
+
an unbound checkout returns None (``check`` falls back to the project-less
|
|
108
|
+
validation).
|
|
104
109
|
"""
|
|
105
110
|
from_file = False
|
|
106
111
|
if not project_id:
|
|
107
112
|
project_id = read_project_id_from_dir(ontology_dir)
|
|
108
113
|
from_file = project_id is not None
|
|
109
114
|
if not project_id:
|
|
115
|
+
if optional:
|
|
116
|
+
return None
|
|
110
117
|
typer.secho(
|
|
111
118
|
f"No project. Pass --project (or set CASSIS_PROJECT_ID), or run in a checkout whose "
|
|
112
119
|
f"{ontology_dir.name}/project.yml records it (written by `cassis ontology pull` or a publish).",
|
|
@@ -119,7 +126,7 @@ def resolve_project_id(project_id: Optional[str], ontology_dir: Path) -> str:
|
|
|
119
126
|
except ValueError:
|
|
120
127
|
typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
|
|
121
128
|
raise typer.Exit(EXIT_USAGE)
|
|
122
|
-
if from_file:
|
|
129
|
+
if from_file and not quiet:
|
|
123
130
|
typer.secho(f"Using project {project_id} from {ontology_dir.name}/project.yml.", fg=typer.colors.CYAN, err=True)
|
|
124
131
|
return project_id
|
|
125
132
|
|
|
@@ -8,6 +8,7 @@ import subprocess
|
|
|
8
8
|
import time
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any, Optional
|
|
11
|
+
from uuid import UUID
|
|
11
12
|
|
|
12
13
|
import typer
|
|
13
14
|
from cassis_cli.api import (
|
|
@@ -16,8 +17,11 @@ from cassis_cli.api import (
|
|
|
16
17
|
AuthError,
|
|
17
18
|
EvalCaseExistsError,
|
|
18
19
|
EvalCaseGoldSqlError,
|
|
20
|
+
EvalCaseNotFoundError,
|
|
19
21
|
EvalRunActiveError,
|
|
20
22
|
EvalStartValidationError,
|
|
23
|
+
delete_eval_case,
|
|
24
|
+
get_eval_cases,
|
|
21
25
|
get_eval_run,
|
|
22
26
|
get_eval_run_results,
|
|
23
27
|
post_eval_case_create,
|
|
@@ -221,6 +225,134 @@ def add_case(
|
|
|
221
225
|
raise typer.Exit(EXIT_OK)
|
|
222
226
|
|
|
223
227
|
|
|
228
|
+
@app.command(name="list-cases")
|
|
229
|
+
def list_cases(
|
|
230
|
+
path: Path = typer.Argument(
|
|
231
|
+
Path("."),
|
|
232
|
+
help="Repository checkout root (holds <base-path>/project.yml for the --project default).",
|
|
233
|
+
),
|
|
234
|
+
project_id: Optional[str] = typer.Option(
|
|
235
|
+
None,
|
|
236
|
+
"--project",
|
|
237
|
+
envvar="CASSIS_PROJECT_ID",
|
|
238
|
+
help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
|
|
239
|
+
),
|
|
240
|
+
api_key: Optional[str] = typer.Option(
|
|
241
|
+
None,
|
|
242
|
+
"--api-key",
|
|
243
|
+
envvar="CASSIS_API_KEY",
|
|
244
|
+
help="Cassis API key (sk-k6-...). Create one in Organization settings -> API keys.",
|
|
245
|
+
),
|
|
246
|
+
api_url: str = typer.Option(
|
|
247
|
+
DEFAULT_API_URL,
|
|
248
|
+
"--api-url",
|
|
249
|
+
envvar="CASSIS_API_URL",
|
|
250
|
+
help="Cassis API base URL.",
|
|
251
|
+
),
|
|
252
|
+
base_path: str = typer.Option(
|
|
253
|
+
DEFAULT_BASE_PATH,
|
|
254
|
+
"--base-path",
|
|
255
|
+
envvar="CASSIS_BASE_PATH",
|
|
256
|
+
help="Repository directory the ontology is exported under (holds project.yml for the --project default).",
|
|
257
|
+
),
|
|
258
|
+
json_output: bool = typer.Option(False, "--json", help="Print the cases as raw JSON (includes gold SQL)."),
|
|
259
|
+
) -> None:
|
|
260
|
+
"""List the project's eval cases: the suite `cassis eval run` scores.
|
|
261
|
+
|
|
262
|
+
Prints each case's id and question (--json adds the gold SQL); the id is
|
|
263
|
+
what `cassis eval delete-case` takes. Exits 0 on success, 2 on usage
|
|
264
|
+
errors, 3 on transport/API errors.
|
|
265
|
+
"""
|
|
266
|
+
api_key = require_api_key(api_key)
|
|
267
|
+
project_id = resolve_project_id(project_id, path / Path(base_path))
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
cases = get_eval_cases(api_url=api_url, api_key=api_key, project_id=project_id)
|
|
271
|
+
except AuthError as exc:
|
|
272
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
273
|
+
raise typer.Exit(EXIT_TRANSPORT) from exc
|
|
274
|
+
except ApiError as exc:
|
|
275
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
276
|
+
raise typer.Exit(EXIT_TRANSPORT) from exc
|
|
277
|
+
|
|
278
|
+
if json_output:
|
|
279
|
+
typer.echo(json.dumps(cases, indent=2))
|
|
280
|
+
else:
|
|
281
|
+
if not cases:
|
|
282
|
+
typer.echo("No eval cases yet — add one with `cassis eval add-case`.")
|
|
283
|
+
for case in cases:
|
|
284
|
+
question = str(case.get("question", "")).replace("\n", " ")
|
|
285
|
+
typer.echo(f"{case.get('id')} {question}")
|
|
286
|
+
raise typer.Exit(EXIT_OK)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@app.command(name="delete-case")
|
|
290
|
+
def delete_case(
|
|
291
|
+
case_id: str = typer.Argument(
|
|
292
|
+
...,
|
|
293
|
+
help="Id of the eval case to delete (shown by `cassis eval list-cases`).",
|
|
294
|
+
),
|
|
295
|
+
path: Path = typer.Option(
|
|
296
|
+
Path("."),
|
|
297
|
+
"--path",
|
|
298
|
+
help="Repository checkout root (holds <base-path>/project.yml for the --project default).",
|
|
299
|
+
),
|
|
300
|
+
project_id: Optional[str] = typer.Option(
|
|
301
|
+
None,
|
|
302
|
+
"--project",
|
|
303
|
+
envvar="CASSIS_PROJECT_ID",
|
|
304
|
+
help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
|
|
305
|
+
),
|
|
306
|
+
api_key: Optional[str] = typer.Option(
|
|
307
|
+
None,
|
|
308
|
+
"--api-key",
|
|
309
|
+
envvar="CASSIS_API_KEY",
|
|
310
|
+
help="Cassis API key (sk-k6-...). Create one in Organization settings -> API keys.",
|
|
311
|
+
),
|
|
312
|
+
api_url: str = typer.Option(
|
|
313
|
+
DEFAULT_API_URL,
|
|
314
|
+
"--api-url",
|
|
315
|
+
envvar="CASSIS_API_URL",
|
|
316
|
+
help="Cassis API base URL.",
|
|
317
|
+
),
|
|
318
|
+
base_path: str = typer.Option(
|
|
319
|
+
DEFAULT_BASE_PATH,
|
|
320
|
+
"--base-path",
|
|
321
|
+
envvar="CASSIS_BASE_PATH",
|
|
322
|
+
help="Repository directory the ontology is exported under (holds project.yml for the --project default).",
|
|
323
|
+
),
|
|
324
|
+
) -> None:
|
|
325
|
+
"""Delete an eval case from the project's suite.
|
|
326
|
+
|
|
327
|
+
For pruning a case that is stale or wrong — e.g. its gold SQL encodes a
|
|
328
|
+
definition the ontology has since changed. Exits 0 on deletion, 1 when
|
|
329
|
+
the case does not exist in the project, 2 on usage errors, 3 on
|
|
330
|
+
transport/API errors.
|
|
331
|
+
"""
|
|
332
|
+
api_key = require_api_key(api_key)
|
|
333
|
+
project_id = resolve_project_id(project_id, path / Path(base_path))
|
|
334
|
+
try:
|
|
335
|
+
UUID(case_id)
|
|
336
|
+
except ValueError:
|
|
337
|
+
typer.secho(f"CASE_ID must be a UUID, got {case_id!r}.", fg=typer.colors.RED, err=True)
|
|
338
|
+
raise typer.Exit(EXIT_USAGE)
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
delete_eval_case(api_url=api_url, api_key=api_key, project_id=project_id, case_id=case_id)
|
|
342
|
+
except EvalCaseNotFoundError as exc:
|
|
343
|
+
typer.secho(str(exc), fg=typer.colors.YELLOW, err=True)
|
|
344
|
+
raise typer.Exit(EXIT_VALIDATION_FAILED) from exc
|
|
345
|
+
except AuthError as exc:
|
|
346
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
347
|
+
raise typer.Exit(EXIT_TRANSPORT) from exc
|
|
348
|
+
except ApiError as exc:
|
|
349
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
350
|
+
raise typer.Exit(EXIT_TRANSPORT) from exc
|
|
351
|
+
|
|
352
|
+
typer.secho(f"✓ Deleted eval case {case_id}.", fg=typer.colors.GREEN)
|
|
353
|
+
raise typer.Exit(EXIT_OK)
|
|
354
|
+
|
|
355
|
+
|
|
224
356
|
@app.command()
|
|
225
357
|
def run(
|
|
226
358
|
path: Path = typer.Argument(
|
|
@@ -31,7 +31,7 @@ GUIDE_FILENAME = "AGENTS.md"
|
|
|
31
31
|
# Monotonic version of the doctrine text below. Bump it whenever
|
|
32
32
|
# ontology_design_guide.md changes (a backend test enforces the pairing) — it
|
|
33
33
|
# is what lets an older writer recognize a newer guide and leave it alone.
|
|
34
|
-
DOCTRINE_VERSION =
|
|
34
|
+
DOCTRINE_VERSION = 5
|
|
35
35
|
|
|
36
36
|
# Must stay byte-identical to backend/app/services/ontology_guide.py::_BANNER —
|
|
37
37
|
# the server-side git export writes the same file, and differing banners would
|
|
@@ -6,6 +6,7 @@ import typer
|
|
|
6
6
|
from cassis_cli import __version__
|
|
7
7
|
from cassis_cli.eval import app as eval_app
|
|
8
8
|
from cassis_cli.ontology import app as ontology_app
|
|
9
|
+
from cassis_cli.schema import app as schema_app
|
|
9
10
|
|
|
10
11
|
app = typer.Typer(
|
|
11
12
|
no_args_is_help=True,
|
|
@@ -13,6 +14,7 @@ app = typer.Typer(
|
|
|
13
14
|
)
|
|
14
15
|
app.add_typer(ontology_app, name="ontology")
|
|
15
16
|
app.add_typer(eval_app, name="eval")
|
|
17
|
+
app.add_typer(schema_app, name="schema")
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
@app.command()
|
|
@@ -56,6 +56,12 @@ def check(
|
|
|
56
56
|
Path("."),
|
|
57
57
|
help="Repository checkout root (the directory containing the ontology export path).",
|
|
58
58
|
),
|
|
59
|
+
project_id: Optional[str] = typer.Option(
|
|
60
|
+
None,
|
|
61
|
+
"--project",
|
|
62
|
+
envvar="CASSIS_PROJECT_ID",
|
|
63
|
+
help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
|
|
64
|
+
),
|
|
59
65
|
api_key: Optional[str] = typer.Option(
|
|
60
66
|
None,
|
|
61
67
|
"--api-key",
|
|
@@ -79,14 +85,27 @@ def check(
|
|
|
79
85
|
"""Validate the ontology files in a repository checkout.
|
|
80
86
|
|
|
81
87
|
Runs the same checks as the Cassis GitHub PR check (YAML parsing,
|
|
82
|
-
round-trip, import validation).
|
|
83
|
-
|
|
88
|
+
round-trip, import validation). When the checkout is bound to a project
|
|
89
|
+
(``project.yml``, ``--project``, or ``CASSIS_PROJECT_ID``), the tree is
|
|
90
|
+
additionally cross-checked against the project's source schema; unmatched
|
|
91
|
+
references print as warnings and never fail the check. Exits 0 when valid,
|
|
92
|
+
1 when validation fails, 2 on usage errors, 3 on transport/API errors.
|
|
84
93
|
"""
|
|
85
94
|
api_key = _require_api_key(api_key)
|
|
86
95
|
files, base_path = _collect_tree(path, base_path)
|
|
87
96
|
|
|
97
|
+
# Soft resolution: an unbound checkout is not an error — the check falls
|
|
98
|
+
# back to the project-less route (no schema reference stage).
|
|
99
|
+
project_id = _resolve_project_id(project_id, path / base_path, optional=True, quiet=json_output)
|
|
100
|
+
if not project_id and not json_output:
|
|
101
|
+
typer.secho(
|
|
102
|
+
"No project binding — schema reference checks skipped (bind with `cassis ontology pull` or --project).",
|
|
103
|
+
fg=typer.colors.CYAN,
|
|
104
|
+
err=True,
|
|
105
|
+
)
|
|
106
|
+
|
|
88
107
|
try:
|
|
89
|
-
result = post_ontology_check(api_url=api_url, api_key=api_key, files=files)
|
|
108
|
+
result = post_ontology_check(api_url=api_url, api_key=api_key, files=files, project_id=project_id)
|
|
90
109
|
except AuthError as exc:
|
|
91
110
|
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
92
111
|
raise typer.Exit(EXIT_TRANSPORT) from exc
|
|
@@ -94,10 +113,21 @@ def check(
|
|
|
94
113
|
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
95
114
|
raise typer.Exit(EXIT_TRANSPORT) from exc
|
|
96
115
|
|
|
116
|
+
warnings = result.get("warnings") or []
|
|
97
117
|
if json_output:
|
|
98
118
|
typer.echo(json.dumps(result, indent=2))
|
|
99
119
|
elif result["passed"]:
|
|
100
120
|
typer.secho(f"✓ {result['summary']}", fg=typer.colors.GREEN)
|
|
121
|
+
# Disambiguate silence: "no warnings" must never read as "references
|
|
122
|
+
# verified" when the stage didn't run.
|
|
123
|
+
if project_id and result.get("references_checked") and not warnings:
|
|
124
|
+
typer.secho("✓ Schema references resolve against the source schema.", fg=typer.colors.GREEN)
|
|
125
|
+
elif project_id and not result.get("references_checked"):
|
|
126
|
+
typer.secho(
|
|
127
|
+
"Schema reference check skipped — the project has no source schema yet.",
|
|
128
|
+
fg=typer.colors.YELLOW,
|
|
129
|
+
err=True,
|
|
130
|
+
)
|
|
101
131
|
else:
|
|
102
132
|
typer.secho(result["title"], fg=typer.colors.RED, bold=True)
|
|
103
133
|
typer.echo(result["summary"])
|
|
@@ -105,6 +135,16 @@ def check(
|
|
|
105
135
|
location = f"{base_path}/{finding.get('path')}: " if finding.get("path") else ""
|
|
106
136
|
typer.echo(f" {location}{finding.get('message', '')} ({finding.get('stage', '?')})")
|
|
107
137
|
|
|
138
|
+
if warnings and not json_output:
|
|
139
|
+
typer.secho(
|
|
140
|
+
f"{len(warnings)} schema reference warning(s) — advisory, expected if the objects "
|
|
141
|
+
"haven't been built or synced yet:",
|
|
142
|
+
fg=typer.colors.YELLOW,
|
|
143
|
+
bold=True,
|
|
144
|
+
)
|
|
145
|
+
for warning in warnings:
|
|
146
|
+
typer.secho(f" {warning.get('message', '')}", fg=typer.colors.YELLOW)
|
|
147
|
+
|
|
108
148
|
raise typer.Exit(EXIT_OK if result["passed"] else EXIT_VALIDATION_FAILED)
|
|
109
149
|
|
|
110
150
|
|
|
@@ -357,6 +357,16 @@ description, so a wrong example is worse than none.
|
|
|
357
357
|
real mechanic diverges.
|
|
358
358
|
- **Don't describe things that don't exist.** Every table, column, and value you
|
|
359
359
|
reference must be real.
|
|
360
|
+
- **A hedge is not a safeguard.** "Likely `'M:SS.mmm'`", "probably", "appears to
|
|
361
|
+
be" reach the agent as the claim itself; the qualifier does not survive into
|
|
362
|
+
its reasoning. If a storage format, a unit, or a reason for nullability is not
|
|
363
|
+
in the source, leave it out and record the open question instead.
|
|
364
|
+
- **Never invent a contrast with a column you did not read.** When a new column
|
|
365
|
+
shares a name with one on another table (`position_order`, `status_id`,
|
|
366
|
+
`lap`), the tempting sentence is "here it means X, unlike there". A schema
|
|
367
|
+
dump cannot support that. Open the sibling's description and reuse it if it
|
|
368
|
+
applies; if you can't confirm the relationship, say it's unverified. A false
|
|
369
|
+
distinction drawn confidently survives review as a documented decision.
|
|
360
370
|
|
|
361
371
|
---
|
|
362
372
|
|
|
@@ -386,7 +396,31 @@ description, so a wrong example is worse than none.
|
|
|
386
396
|
|
|
387
397
|
---
|
|
388
398
|
|
|
389
|
-
## 12.
|
|
399
|
+
## 12. Extension passes: structure before content
|
|
400
|
+
|
|
401
|
+
Adding a source or a whole subject area inverts the scoping bullet above. §11 is
|
|
402
|
+
written for maintenance edits, where a structural change is genuinely adjacent;
|
|
403
|
+
on an extension pass the tree *is* the change — every table and metric names a
|
|
404
|
+
`domain_path`, so content filled into a hierarchy you already doubt all has to
|
|
405
|
+
move. Same human opt-in, front-loaded:
|
|
406
|
+
|
|
407
|
+
1. **Scope.** Agree what is being added, from which sources, in what order.
|
|
408
|
+
2. **Structure.** Evaluate the existing hierarchy against what is arriving and
|
|
409
|
+
propose the tree changes — new domains, splits, moves — for approval *before*
|
|
410
|
+
any content. On approval, create the new domain files frontmatter-only (empty
|
|
411
|
+
body), so every `domain_path` resolves while you fill.
|
|
412
|
+
3. **Fill bottom-up.** Column and table facts, then metrics and joins, then each
|
|
413
|
+
domain README body last, written from what is left over. That residue is by
|
|
414
|
+
construction the cross-table connective tissue domain level owns, so the order
|
|
415
|
+
enforces the one-home rule instead of leaving it to vigilance. Bottom-up is an
|
|
416
|
+
authoring order, not a holding rule: a fact learned out of order still lands at
|
|
417
|
+
its owning layer immediately (§11). If filling shows the approved tree is wrong
|
|
418
|
+
— a domain overloads, or wants to split — stop and return to 2 rather than keep
|
|
419
|
+
filling into it.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## 13. Working in a git-synced repo
|
|
390
424
|
|
|
391
425
|
The repository is the source of truth. Edit the files, then verify before opening
|
|
392
426
|
a pull request — the CLI runs the same checks the platform does, from your
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""`cassis schema` — local snapshot of the data source's source schema.
|
|
2
|
+
|
|
3
|
+
The source schema is OBSERVED state (the warehouse is authoritative), so the
|
|
4
|
+
snapshot is a gitignored cache, never a committed file: `pull` writes
|
|
5
|
+
`<base-path>/.schema.json` and keeps it out of git via the ontology dir's
|
|
6
|
+
`.gitignore`. Agents working in a checkout grep it instead of paging through
|
|
7
|
+
the MCP `get_source_schema` tool; `pulled_at` records how stale it is.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
from cassis_cli.api import DEFAULT_API_URL, ApiError, AuthError, get_schema_export
|
|
19
|
+
from cassis_cli.common import (
|
|
20
|
+
DEFAULT_BASE_PATH,
|
|
21
|
+
EXIT_OK,
|
|
22
|
+
EXIT_TRANSPORT,
|
|
23
|
+
EXIT_USAGE,
|
|
24
|
+
require_api_key,
|
|
25
|
+
resolve_project_id,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
app = typer.Typer(help="Pull a local, gitignored snapshot of the data source's schema.")
|
|
29
|
+
|
|
30
|
+
SNAPSHOT_FILENAME = ".schema.json"
|
|
31
|
+
_GITIGNORE_HEADER = "# Cassis local caches (observed state — never commit)"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command()
|
|
35
|
+
def pull(
|
|
36
|
+
path: Path = typer.Argument(
|
|
37
|
+
Path("."),
|
|
38
|
+
help="Repository checkout root (the directory containing the ontology export path).",
|
|
39
|
+
),
|
|
40
|
+
project_id: Optional[str] = typer.Option(
|
|
41
|
+
None,
|
|
42
|
+
"--project",
|
|
43
|
+
envvar="CASSIS_PROJECT_ID",
|
|
44
|
+
help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
|
|
45
|
+
),
|
|
46
|
+
api_key: Optional[str] = typer.Option(
|
|
47
|
+
None,
|
|
48
|
+
"--api-key",
|
|
49
|
+
envvar="CASSIS_API_KEY",
|
|
50
|
+
help="Cassis API key (sk-k6-...). Create one in Organization settings -> API keys.",
|
|
51
|
+
),
|
|
52
|
+
api_url: str = typer.Option(
|
|
53
|
+
DEFAULT_API_URL,
|
|
54
|
+
"--api-url",
|
|
55
|
+
envvar="CASSIS_API_URL",
|
|
56
|
+
help="Cassis API base URL.",
|
|
57
|
+
),
|
|
58
|
+
base_path: str = typer.Option(
|
|
59
|
+
DEFAULT_BASE_PATH,
|
|
60
|
+
"--base-path",
|
|
61
|
+
envvar="CASSIS_BASE_PATH",
|
|
62
|
+
help="Repository directory the ontology is exported under (the project's git-sync Path setting).",
|
|
63
|
+
),
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Download the source schema into `<base-path>/.schema.json` (gitignored).
|
|
66
|
+
|
|
67
|
+
The snapshot is the schema as Cassis last introspected it from the
|
|
68
|
+
warehouse (or parsed from an uploaded DDL) — every table with its columns
|
|
69
|
+
and types, plus a `pulled_at` stamp so staleness is visible. Re-run after
|
|
70
|
+
a warehouse sync to refresh. Exits 0 on success, 2 on usage errors, 3 on
|
|
71
|
+
transport/API errors.
|
|
72
|
+
"""
|
|
73
|
+
api_key = require_api_key(api_key)
|
|
74
|
+
ontology_dir = path / base_path
|
|
75
|
+
project_id = resolve_project_id(project_id, ontology_dir)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
result = get_schema_export(api_url=api_url, api_key=api_key, project_id=project_id)
|
|
79
|
+
except (AuthError, ApiError) as exc:
|
|
80
|
+
# NoSourceSchemaError lands here too: the server's message already says
|
|
81
|
+
# what to do (sync or upload a DDL); the class exists so api.py doesn't
|
|
82
|
+
# bury it under the misleading project-scope hint.
|
|
83
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
84
|
+
raise typer.Exit(EXIT_TRANSPORT) from exc
|
|
85
|
+
|
|
86
|
+
snapshot = {
|
|
87
|
+
"pulled_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
88
|
+
"project_id": project_id,
|
|
89
|
+
"schema_version": result["schema_version"],
|
|
90
|
+
"tables": result["tables"],
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
ontology_dir.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
snapshot_path = ontology_dir / SNAPSHOT_FILENAME
|
|
96
|
+
snapshot_path.write_text(json.dumps(snapshot, indent=2) + "\n", encoding="utf-8")
|
|
97
|
+
ensure_gitignored(ontology_dir)
|
|
98
|
+
except OSError as exc:
|
|
99
|
+
# Usage-class exit (2), like the other local-file failures in the exit
|
|
100
|
+
# table — the API call succeeded, the checkout is what's broken.
|
|
101
|
+
typer.secho(f"Could not write the snapshot: {exc}", fg=typer.colors.RED, err=True)
|
|
102
|
+
raise typer.Exit(EXIT_USAGE) from exc
|
|
103
|
+
|
|
104
|
+
table_count = len(result["tables"])
|
|
105
|
+
column_count = sum(len(t.get("columns") or []) for t in result["tables"])
|
|
106
|
+
version = result["schema_version"].get("version")
|
|
107
|
+
typer.secho(
|
|
108
|
+
f"✓ Pulled source schema v{version}: {table_count} tables, {column_count} columns "
|
|
109
|
+
f"-> {snapshot_path} (gitignored)",
|
|
110
|
+
fg=typer.colors.GREEN,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def ensure_gitignored(ontology_dir: Path) -> None:
|
|
115
|
+
"""Make sure the snapshot never lands in git: keep `.gitignore` covering it.
|
|
116
|
+
|
|
117
|
+
Appends to (or creates) the ontology dir's own `.gitignore` — local to the
|
|
118
|
+
export directory, so it survives repo-level `.gitignore` rewrites and needs
|
|
119
|
+
no knowledge of the checkout layout.
|
|
120
|
+
"""
|
|
121
|
+
gitignore = ontology_dir / ".gitignore"
|
|
122
|
+
try:
|
|
123
|
+
existing = gitignore.read_text(encoding="utf-8")
|
|
124
|
+
except (OSError, UnicodeDecodeError) as _exc: # `as` keeps black from stripping the parens (3.14-only syntax)
|
|
125
|
+
existing = ""
|
|
126
|
+
if SNAPSHOT_FILENAME in existing.splitlines():
|
|
127
|
+
return
|
|
128
|
+
prefix = "" if not existing else existing.rstrip("\n") + "\n"
|
|
129
|
+
gitignore.write_text(f"{prefix}{_GITIGNORE_HEADER}\n{SNAPSHOT_FILENAME}\n", encoding="utf-8")
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cassis-cli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
description = "Cassis CLI — run Cassis actions (ontology validation, upload and publish) from your CI pipelines"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
license = { text = "
|
|
6
|
+
license = { text = "Apache-2.0" }
|
|
7
7
|
authors = [{ name = "Cassis", email = "tech.admin@getcassis.com" }]
|
|
8
8
|
keywords = ["cassis", "ontology", "ci", "text-to-sql"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"License :: OSI Approved :: Apache Software License",
|
|
11
|
+
"Environment :: Console",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Topic :: Database",
|
|
14
|
+
]
|
|
9
15
|
# Customer CI images — keep this floor low and the code 3.10-compatible,
|
|
10
16
|
# regardless of the monorepo's own Python pin.
|
|
11
17
|
requires-python = ">=3.10,<4.0"
|
|
@@ -14,6 +20,11 @@ dependencies = [
|
|
|
14
20
|
"httpx (>=0.24,<1.0)",
|
|
15
21
|
]
|
|
16
22
|
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://getcassis.com"
|
|
25
|
+
Repository = "https://github.com/GetCassis/cassis-cli"
|
|
26
|
+
Documentation = "https://github.com/GetCassis/cassis-cli#readme"
|
|
27
|
+
|
|
17
28
|
[project.scripts]
|
|
18
29
|
cassis = "cassis_cli.main:app"
|
|
19
30
|
|