cassis-cli 1.0.0__tar.gz → 1.1.1__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.1.1/LICENSE +202 -0
- cassis_cli-1.1.1/NOTICE +4 -0
- cassis_cli-1.0.0/README.md → cassis_cli-1.1.1/PKG-INFO +52 -4
- cassis_cli-1.0.0/PKG-INFO → cassis_cli-1.1.1/README.md +28 -24
- {cassis_cli-1.0.0 → cassis_cli-1.1.1}/cassis_cli/__init__.py +1 -1
- {cassis_cli-1.0.0 → cassis_cli-1.1.1}/cassis_cli/api.py +2 -0
- cassis_cli-1.1.1/cassis_cli/common.py +177 -0
- {cassis_cli-1.0.0 → cassis_cli-1.1.1}/cassis_cli/eval.py +20 -18
- {cassis_cli-1.0.0 → cassis_cli-1.1.1}/cassis_cli/guide.py +2 -2
- {cassis_cli-1.0.0 → cassis_cli-1.1.1}/cassis_cli/ontology.py +33 -30
- {cassis_cli-1.0.0 → cassis_cli-1.1.1}/cassis_cli/ontology_design_guide.md +45 -14
- {cassis_cli-1.0.0 → cassis_cli-1.1.1}/pyproject.toml +13 -2
- cassis_cli-1.0.0/cassis_cli/common.py +0 -95
- {cassis_cli-1.0.0 → cassis_cli-1.1.1}/cassis_cli/main.py +0 -0
cassis_cli-1.1.1/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.1.1/NOTICE
ADDED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cassis-cli
|
|
3
|
+
Version: 1.1.1
|
|
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:
|
|
@@ -5,8 +27,8 @@ Run Cassis actions from your CI pipelines:
|
|
|
5
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.
|
|
6
28
|
- `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
29
|
- `cassis ontology upload` uploads the ontology files to a Cassis project (full replace) and, by default, publishes them immediately as a new version — so a merge to your main branch can go live in one CI step.
|
|
8
|
-
- `cassis ontology pull` downloads the project's unpublished ontology into your repository checkout (full sync — stale local
|
|
9
|
-
- `cassis ontology pull` and `cassis ontology fmt` also write `<base-path>/AGENTS.md`, the Cassis ontology modeling guide, into the checkout (default `cassis/AGENTS.md`) — a managed file (generated banner; the CLI overwrites local edits) so a repo-aware coding agent loads current Cassis modeling doctrine by convention. It sits inside the ontology directory but is not part of the ontology tree (the
|
|
30
|
+
- `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
|
+
- `cassis ontology pull` and `cassis ontology fmt` also write `<base-path>/AGENTS.md`, the Cassis ontology modeling guide, into the checkout (default `cassis/AGENTS.md`) — a managed file (generated banner; the CLI overwrites local edits) so a repo-aware coding agent loads current Cassis modeling doctrine by convention. It sits inside the ontology directory but is not part of the ontology tree (which is the YAML files plus the domain Markdown files `domains/**/README.md`), so it is never uploaded, validated, or pruned. Commit it alongside your ontology changes. The guide text ships inside the CLI package, so its version tracks the **installed cassis-cli version** — upgrade the CLI (`pip install -U cassis-cli`) and re-run `fmt` to pick up doctrine updates; an unpinned `pip install cassis-cli` in CI gets them automatically. The banner stamps a doctrine version, and the CLI never *downgrades* the file: if the checkout's `AGENTS.md` was written by a newer doctrine (a newer CLI, or Cassis itself on a publish), `fmt`/`pull` leave it in place, print an upgrade notice, and `fmt --check` still passes.
|
|
10
32
|
- The CLI identifies itself to the API (`User-Agent: cassis-cli/<version>`), and successful API responses advertise the newest published version — when you are behind, commands print a one-line upgrade notice on stderr (purely informational; output and exit codes are unchanged).
|
|
11
33
|
- `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
34
|
- `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.
|
|
@@ -18,11 +40,21 @@ Run Cassis actions from your CI pipelines:
|
|
|
18
40
|
pip install cassis-cli
|
|
19
41
|
```
|
|
20
42
|
|
|
43
|
+
## Ontology file format
|
|
44
|
+
|
|
45
|
+
The ontology tree under `<base-path>` (default `cassis/`) is:
|
|
46
|
+
|
|
47
|
+
- **Project identity** — `project.yml`: the Cassis project id and format version. Written by `pull` and by server-side publish (the contexts that know the id); a local `fmt` won't create it.
|
|
48
|
+
- **Domains** — Markdown files: every domain is the `README.md` of its folder — `domains/README.md` for the root, `domains/<path>/README.md` for each sub-domain. Each has a small YAML frontmatter block (`type`, `title`, `description`) and a Markdown body carrying the domain's `context_md`; a generated section at the bottom links the domain's tables and metrics (kept current by `fmt`/`pull` — edit your prose above it, and the PR check fails if the links are stale, so re-run `fmt`). The layout is a Cassis profile inspired by [OKF](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf): the files render on GitHub and read in any Markdown editor, but Cassis validates them strictly (unknown keys are flagged, not preserved).
|
|
49
|
+
- **Tables, joins, metrics** — YAML, unchanged: `tables/<schema>/<table>.yml`, `joins.yml`, `metrics/<name>.yml`.
|
|
50
|
+
|
|
51
|
+
**Migrating an existing repo** (domains were YAML `_project.yml` / `_domain.yml` before cassis-cli 1.1.0): upgrade and run `cassis ontology fmt` (or `cassis ontology pull` if you have no local edits) — it rewrites the domain files to Markdown and removes the old ones. Review the diff and commit. Cassis reads the old YAML domain files too, so an un-migrated repo keeps working until you convert it. **Uploading requires cassis-cli ≥ 1.1.0** — the server rejects an older CLI (which would drop the Markdown domain files) with a clear upgrade error.
|
|
52
|
+
|
|
21
53
|
## Setup
|
|
22
54
|
|
|
23
55
|
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
24
56
|
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
25
|
-
3. For `pull`, `upload` and `eval
|
|
57
|
+
3. For `pull`, `upload`, `eval run`, `ontology test`, and `eval add-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).
|
|
26
58
|
|
|
27
59
|
## Usage
|
|
28
60
|
|
|
@@ -112,7 +144,7 @@ cassis ontology fmt --check
|
|
|
112
144
|
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, out of credits, or `--timeout` reached |
|
|
113
145
|
|
|
114
146
|
Commands that send the local tree (`check`, `fmt`, `upload`, `eval run`, `test`) accept up to
|
|
115
|
-
2000
|
|
147
|
+
2000 ontology files / 5 MB total — far above real ontologies (a few hundred small
|
|
116
148
|
files). Beyond that the CLI fails fast with exit 2 before uploading anything;
|
|
117
149
|
double-check `--base-path` if you hit it.
|
|
118
150
|
|
|
@@ -201,3 +233,19 @@ ontology-publish:
|
|
|
201
233
|
CASSIS_API_KEY: $CASSIS_API_KEY
|
|
202
234
|
CASSIS_PROJECT_ID: $CASSIS_PROJECT_ID
|
|
203
235
|
```
|
|
236
|
+
|
|
237
|
+
## About this repository
|
|
238
|
+
|
|
239
|
+
[github.com/GetCassis/cassis-cli](https://github.com/GetCassis/cassis-cli) is a
|
|
240
|
+
read-only mirror, synced automatically from the Cassis monorepo where the CLI is
|
|
241
|
+
developed. Issues are welcome and watched; pull requests can't be merged here, so
|
|
242
|
+
open an issue (or mail tech.admin@getcassis.com) and we'll port the patch upstream
|
|
243
|
+
with credit.
|
|
244
|
+
|
|
245
|
+
Only the CLI is open source. The Cassis backend it talks to is proprietary and
|
|
246
|
+
requires an account.
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
|
251
|
+
|
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: cassis-cli
|
|
3
|
-
Version: 1.0.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:
|
|
@@ -25,8 +5,8 @@ Run Cassis actions from your CI pipelines:
|
|
|
25
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.
|
|
26
6
|
- `cassis ontology fmt` rewrites the ontology files in canonical form (think `black`/`gofmt` for the ontology), so hand or agent edits pass the round-trip check.
|
|
27
7
|
- `cassis ontology upload` uploads the ontology files to a Cassis project (full replace) and, by default, publishes them immediately as a new version — so a merge to your main branch can go live in one CI step.
|
|
28
|
-
- `cassis ontology pull` downloads the project's unpublished ontology into your repository checkout (full sync — stale local
|
|
29
|
-
- `cassis ontology pull` and `cassis ontology fmt` also write `<base-path>/AGENTS.md`, the Cassis ontology modeling guide, into the checkout (default `cassis/AGENTS.md`) — a managed file (generated banner; the CLI overwrites local edits) so a repo-aware coding agent loads current Cassis modeling doctrine by convention. It sits inside the ontology directory but is not part of the ontology tree (the
|
|
8
|
+
- `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).
|
|
9
|
+
- `cassis ontology pull` and `cassis ontology fmt` also write `<base-path>/AGENTS.md`, the Cassis ontology modeling guide, into the checkout (default `cassis/AGENTS.md`) — a managed file (generated banner; the CLI overwrites local edits) so a repo-aware coding agent loads current Cassis modeling doctrine by convention. It sits inside the ontology directory but is not part of the ontology tree (which is the YAML files plus the domain Markdown files `domains/**/README.md`), so it is never uploaded, validated, or pruned. Commit it alongside your ontology changes. The guide text ships inside the CLI package, so its version tracks the **installed cassis-cli version** — upgrade the CLI (`pip install -U cassis-cli`) and re-run `fmt` to pick up doctrine updates; an unpinned `pip install cassis-cli` in CI gets them automatically. The banner stamps a doctrine version, and the CLI never *downgrades* the file: if the checkout's `AGENTS.md` was written by a newer doctrine (a newer CLI, or Cassis itself on a publish), `fmt`/`pull` leave it in place, print an upgrade notice, and `fmt --check` still passes.
|
|
30
10
|
- The CLI identifies itself to the API (`User-Agent: cassis-cli/<version>`), and successful API responses advertise the newest published version — when you are behind, commands print a one-line upgrade notice on stderr (purely informational; output and exit codes are unchanged).
|
|
31
11
|
- `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
12
|
- `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.
|
|
@@ -38,11 +18,21 @@ Run Cassis actions from your CI pipelines:
|
|
|
38
18
|
pip install cassis-cli
|
|
39
19
|
```
|
|
40
20
|
|
|
21
|
+
## Ontology file format
|
|
22
|
+
|
|
23
|
+
The ontology tree under `<base-path>` (default `cassis/`) is:
|
|
24
|
+
|
|
25
|
+
- **Project identity** — `project.yml`: the Cassis project id and format version. Written by `pull` and by server-side publish (the contexts that know the id); a local `fmt` won't create it.
|
|
26
|
+
- **Domains** — Markdown files: every domain is the `README.md` of its folder — `domains/README.md` for the root, `domains/<path>/README.md` for each sub-domain. Each has a small YAML frontmatter block (`type`, `title`, `description`) and a Markdown body carrying the domain's `context_md`; a generated section at the bottom links the domain's tables and metrics (kept current by `fmt`/`pull` — edit your prose above it, and the PR check fails if the links are stale, so re-run `fmt`). The layout is a Cassis profile inspired by [OKF](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf): the files render on GitHub and read in any Markdown editor, but Cassis validates them strictly (unknown keys are flagged, not preserved).
|
|
27
|
+
- **Tables, joins, metrics** — YAML, unchanged: `tables/<schema>/<table>.yml`, `joins.yml`, `metrics/<name>.yml`.
|
|
28
|
+
|
|
29
|
+
**Migrating an existing repo** (domains were YAML `_project.yml` / `_domain.yml` before cassis-cli 1.1.0): upgrade and run `cassis ontology fmt` (or `cassis ontology pull` if you have no local edits) — it rewrites the domain files to Markdown and removes the old ones. Review the diff and commit. Cassis reads the old YAML domain files too, so an un-migrated repo keeps working until you convert it. **Uploading requires cassis-cli ≥ 1.1.0** — the server rejects an older CLI (which would drop the Markdown domain files) with a clear upgrade error.
|
|
30
|
+
|
|
41
31
|
## Setup
|
|
42
32
|
|
|
43
33
|
1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
|
|
44
34
|
2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
|
|
45
|
-
3. For `pull`, `upload` and `eval
|
|
35
|
+
3. For `pull`, `upload`, `eval run`, `ontology test`, and `eval add-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).
|
|
46
36
|
|
|
47
37
|
## Usage
|
|
48
38
|
|
|
@@ -132,7 +122,7 @@ cassis ontology fmt --check
|
|
|
132
122
|
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, out of credits, or `--timeout` reached |
|
|
133
123
|
|
|
134
124
|
Commands that send the local tree (`check`, `fmt`, `upload`, `eval run`, `test`) accept up to
|
|
135
|
-
2000
|
|
125
|
+
2000 ontology files / 5 MB total — far above real ontologies (a few hundred small
|
|
136
126
|
files). Beyond that the CLI fails fast with exit 2 before uploading anything;
|
|
137
127
|
double-check `--base-path` if you hit it.
|
|
138
128
|
|
|
@@ -222,3 +212,17 @@ ontology-publish:
|
|
|
222
212
|
CASSIS_PROJECT_ID: $CASSIS_PROJECT_ID
|
|
223
213
|
```
|
|
224
214
|
|
|
215
|
+
## About this repository
|
|
216
|
+
|
|
217
|
+
[github.com/GetCassis/cassis-cli](https://github.com/GetCassis/cassis-cli) is a
|
|
218
|
+
read-only mirror, synced automatically from the Cassis monorepo where the CLI is
|
|
219
|
+
developed. Issues are welcome and watched; pull requests can't be merged here, so
|
|
220
|
+
open an issue (or mail tech.admin@getcassis.com) and we'll port the patch upstream
|
|
221
|
+
with credit.
|
|
222
|
+
|
|
223
|
+
Only the CLI is open source. The Cassis backend it talks to is proprietary and
|
|
224
|
+
requires an account.
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
|
@@ -191,6 +191,8 @@ def post_ontology_import(
|
|
|
191
191
|
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
192
192
|
if response.status_code == 400:
|
|
193
193
|
raise UploadValidationError(str(_detail_or_text(response)))
|
|
194
|
+
if response.status_code == 426: # this CLI is too old for the server's ontology format
|
|
195
|
+
raise UploadValidationError(str(_detail_or_text(response)))
|
|
194
196
|
if response.status_code in (403, 404):
|
|
195
197
|
raise _project_scope_error(response)
|
|
196
198
|
if response.status_code >= 400:
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Helpers shared by the `cassis` subcommands (tree collection, auth, exit codes)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
# Repository directory the ontology tree is exported under. Must match the
|
|
13
|
+
# project's git-sync "Path" setting in Cassis (default "cassis").
|
|
14
|
+
DEFAULT_BASE_PATH = "cassis"
|
|
15
|
+
|
|
16
|
+
# Exit codes (documented in the README; stable contract for CI scripts).
|
|
17
|
+
EXIT_OK = 0
|
|
18
|
+
EXIT_VALIDATION_FAILED = 1
|
|
19
|
+
EXIT_USAGE = 2
|
|
20
|
+
EXIT_TRANSPORT = 3
|
|
21
|
+
|
|
22
|
+
# Request ceilings of the /api/ci file-tree endpoints, mirrored so oversized
|
|
23
|
+
# trees fail fast with a clear message before any upload. Source of truth:
|
|
24
|
+
# backend/app/schemas/ci.py (the server's 422 remains the backstop).
|
|
25
|
+
MAX_FILES = 2000
|
|
26
|
+
MAX_TOTAL_BYTES = 5 * 1024 * 1024
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_ontology_file(rel_path: str) -> bool:
|
|
30
|
+
"""Whether a base-relative path is an ontology file the server reads.
|
|
31
|
+
|
|
32
|
+
YAML (``project.yml``, tables, joins, metrics, legacy domains) plus domain
|
|
33
|
+
Markdown — every domain is the ``README.md`` of its folder, root included
|
|
34
|
+
(``domains/README.md``), so ``domains/**/README.md`` covers them all. Mirrors
|
|
35
|
+
the server's ``ontology_fs.is_ontology_tree_file`` (kept in sync by hand —
|
|
36
|
+
the CLI can't import the backend). Excludes the managed ``AGENTS.md`` and any
|
|
37
|
+
stray Markdown note, so neither is uploaded nor deleted by ``pull --prune``.
|
|
38
|
+
"""
|
|
39
|
+
if rel_path.endswith((".yml", ".yaml")):
|
|
40
|
+
return True
|
|
41
|
+
return rel_path.startswith("domains/") and rel_path.endswith("/README.md")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_legacy_domain_file(rel_path: str) -> bool:
|
|
45
|
+
"""Whether a base-relative path is a legacy (pre-Markdown) domain file.
|
|
46
|
+
|
|
47
|
+
Used to report the one-time migration to the Markdown domain format, when
|
|
48
|
+
``pull``/``fmt`` remove a ``_project.yml``/``_domain.yml`` and write the
|
|
49
|
+
``domains/README.md`` (and sub-domain ``README.md``) that replaces it.
|
|
50
|
+
"""
|
|
51
|
+
return rel_path == "_project.yml" or rel_path.endswith("/_domain.yml")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def collect_files(ontology_dir: Path) -> dict[str, str]:
|
|
55
|
+
"""Read every ontology file under the ontology dir, keyed by posix relpath.
|
|
56
|
+
|
|
57
|
+
Ontology files are YAML and domain Markdown (see ``is_ontology_file``); the
|
|
58
|
+
managed ``AGENTS.md`` and stray notes are skipped. Exits 2 (usage) on an
|
|
59
|
+
unreadable or non-UTF-8 file — a local checkout problem, reported before
|
|
60
|
+
anything is sent to the API.
|
|
61
|
+
"""
|
|
62
|
+
files: dict[str, str] = {}
|
|
63
|
+
for pattern in ("**/*.yml", "**/*.yaml", "**/*.md"):
|
|
64
|
+
for file in sorted(ontology_dir.glob(pattern)):
|
|
65
|
+
if not file.is_file():
|
|
66
|
+
continue
|
|
67
|
+
rel = file.relative_to(ontology_dir).as_posix()
|
|
68
|
+
if not is_ontology_file(rel):
|
|
69
|
+
continue
|
|
70
|
+
try:
|
|
71
|
+
files[rel] = file.read_text(encoding="utf-8")
|
|
72
|
+
except (UnicodeDecodeError, OSError) as exc:
|
|
73
|
+
typer.secho(f"Cannot read {file}: {exc}", fg=typer.colors.RED, err=True)
|
|
74
|
+
raise typer.Exit(EXIT_USAGE) from exc
|
|
75
|
+
return files
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# project.yml is a two-line machine-written file (`cassis_format_version`, `project_id`);
|
|
79
|
+
# match the id line directly rather than pull in a YAML parser just for this.
|
|
80
|
+
_PROJECT_ID_LINE = re.compile(r"^project_id:\s*['\"]?([^'\"\s]+)['\"]?\s*$")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def read_project_id_from_dir(ontology_dir: Path) -> Optional[str]:
|
|
84
|
+
"""Return the ``project_id`` recorded in ``<ontology_dir>/project.yml``, or None."""
|
|
85
|
+
try:
|
|
86
|
+
text = (ontology_dir / "project.yml").read_text(encoding="utf-8")
|
|
87
|
+
except (OSError, UnicodeDecodeError) as _exc: # `as` keeps black from stripping the parens (3.14-only syntax)
|
|
88
|
+
return None
|
|
89
|
+
for line in text.splitlines():
|
|
90
|
+
match = _PROJECT_ID_LINE.match(line.strip())
|
|
91
|
+
if match:
|
|
92
|
+
return match.group(1)
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def resolve_project_id(project_id: Optional[str], ontology_dir: Path) -> str:
|
|
97
|
+
"""Resolve the target project id, defaulting to the checkout's ``project.yml``.
|
|
98
|
+
|
|
99
|
+
Precedence: an explicit ``--project`` / ``CASSIS_PROJECT_ID`` wins; otherwise
|
|
100
|
+
the ``project_id`` recorded in ``<base-path>/project.yml`` (written by
|
|
101
|
+
``pull`` / publish) is used, and where it came from is noted on stderr so a
|
|
102
|
+
stale value in a copied repo is visible. Exits 2 (usage) when neither is
|
|
103
|
+
available or the value isn't a UUID.
|
|
104
|
+
"""
|
|
105
|
+
from_file = False
|
|
106
|
+
if not project_id:
|
|
107
|
+
project_id = read_project_id_from_dir(ontology_dir)
|
|
108
|
+
from_file = project_id is not None
|
|
109
|
+
if not project_id:
|
|
110
|
+
typer.secho(
|
|
111
|
+
f"No project. Pass --project (or set CASSIS_PROJECT_ID), or run in a checkout whose "
|
|
112
|
+
f"{ontology_dir.name}/project.yml records it (written by `cassis ontology pull` or a publish).",
|
|
113
|
+
fg=typer.colors.RED,
|
|
114
|
+
err=True,
|
|
115
|
+
)
|
|
116
|
+
raise typer.Exit(EXIT_USAGE)
|
|
117
|
+
try:
|
|
118
|
+
UUID(project_id)
|
|
119
|
+
except ValueError:
|
|
120
|
+
typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
|
|
121
|
+
raise typer.Exit(EXIT_USAGE)
|
|
122
|
+
if from_file:
|
|
123
|
+
typer.secho(f"Using project {project_id} from {ontology_dir.name}/project.yml.", fg=typer.colors.CYAN, err=True)
|
|
124
|
+
return project_id
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def require_api_key(api_key: Optional[str]) -> str:
|
|
128
|
+
"""Exit 2 (usage) when no API key was provided."""
|
|
129
|
+
if not api_key:
|
|
130
|
+
typer.secho(
|
|
131
|
+
"No API key. Set CASSIS_API_KEY or pass --api-key "
|
|
132
|
+
"(create one in Cassis under Organization settings -> API keys).",
|
|
133
|
+
fg=typer.colors.RED,
|
|
134
|
+
err=True,
|
|
135
|
+
)
|
|
136
|
+
raise typer.Exit(EXIT_USAGE)
|
|
137
|
+
return api_key
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def collect_tree(path: Path, base_path: str) -> "tuple[dict[str, str], str]":
|
|
141
|
+
"""Resolve the ontology dir under the checkout and read its file tree.
|
|
142
|
+
|
|
143
|
+
Returns ``(files, normalized_base_path)``. Exits 2 (usage) on an empty or
|
|
144
|
+
missing dir, or a tree beyond the API request ceilings — all local checkout
|
|
145
|
+
problems, reported before anything is sent to the API.
|
|
146
|
+
"""
|
|
147
|
+
base_path = base_path.strip().strip("/")
|
|
148
|
+
if not base_path:
|
|
149
|
+
typer.secho("--base-path must not be empty.", fg=typer.colors.RED, err=True)
|
|
150
|
+
raise typer.Exit(EXIT_USAGE)
|
|
151
|
+
|
|
152
|
+
ontology_dir = path / Path(base_path)
|
|
153
|
+
if not ontology_dir.is_dir():
|
|
154
|
+
typer.secho(
|
|
155
|
+
f"No {base_path}/ directory found under {path}. "
|
|
156
|
+
"If the project exports to a custom path, pass it with --base-path (or CASSIS_BASE_PATH).",
|
|
157
|
+
fg=typer.colors.RED,
|
|
158
|
+
err=True,
|
|
159
|
+
)
|
|
160
|
+
raise typer.Exit(EXIT_USAGE)
|
|
161
|
+
|
|
162
|
+
files = collect_files(ontology_dir)
|
|
163
|
+
if not files:
|
|
164
|
+
typer.secho(f"No ontology files found under {ontology_dir}.", fg=typer.colors.RED, err=True)
|
|
165
|
+
raise typer.Exit(EXIT_USAGE)
|
|
166
|
+
|
|
167
|
+
total_bytes = sum(len(content.encode()) for content in files.values())
|
|
168
|
+
if len(files) > MAX_FILES or total_bytes > MAX_TOTAL_BYTES:
|
|
169
|
+
typer.secho(
|
|
170
|
+
f"Ontology tree too large: {len(files)} files / {total_bytes / (1024 * 1024):.1f} MB "
|
|
171
|
+
f"(limits: {MAX_FILES} files / {MAX_TOTAL_BYTES // (1024 * 1024)} MB). "
|
|
172
|
+
"Check that --base-path points at the ontology directory, not a larger tree.",
|
|
173
|
+
fg=typer.colors.RED,
|
|
174
|
+
err=True,
|
|
175
|
+
)
|
|
176
|
+
raise typer.Exit(EXIT_USAGE)
|
|
177
|
+
return files, base_path
|
|
@@ -8,7 +8,6 @@ import subprocess
|
|
|
8
8
|
import time
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any, Optional
|
|
11
|
-
from uuid import UUID
|
|
12
11
|
|
|
13
12
|
import typer
|
|
14
13
|
from cassis_cli.api import (
|
|
@@ -33,6 +32,7 @@ from cassis_cli.common import (
|
|
|
33
32
|
EXIT_VALIDATION_FAILED,
|
|
34
33
|
collect_tree,
|
|
35
34
|
require_api_key,
|
|
35
|
+
resolve_project_id,
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
app = typer.Typer(no_args_is_help=True, help="Eval commands.")
|
|
@@ -144,11 +144,15 @@ def _print_summary(run: dict[str, Any]) -> None:
|
|
|
144
144
|
|
|
145
145
|
@app.command(name="add-case")
|
|
146
146
|
def add_case(
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
path: Path = typer.Argument(
|
|
148
|
+
Path("."),
|
|
149
|
+
help="Repository checkout root (holds <base-path>/project.yml for the --project default).",
|
|
150
|
+
),
|
|
151
|
+
project_id: Optional[str] = typer.Option(
|
|
152
|
+
None,
|
|
149
153
|
"--project",
|
|
150
154
|
envvar="CASSIS_PROJECT_ID",
|
|
151
|
-
help="Target Cassis project ID (UUID
|
|
155
|
+
help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
|
|
152
156
|
),
|
|
153
157
|
question: str = typer.Option(
|
|
154
158
|
...,
|
|
@@ -173,6 +177,12 @@ def add_case(
|
|
|
173
177
|
envvar="CASSIS_API_URL",
|
|
174
178
|
help="Cassis API base URL.",
|
|
175
179
|
),
|
|
180
|
+
base_path: str = typer.Option(
|
|
181
|
+
DEFAULT_BASE_PATH,
|
|
182
|
+
"--base-path",
|
|
183
|
+
envvar="CASSIS_BASE_PATH",
|
|
184
|
+
help="Repository directory the ontology is exported under (holds project.yml for the --project default).",
|
|
185
|
+
),
|
|
176
186
|
json_output: bool = typer.Option(False, "--json", help="Print the created case as raw JSON."),
|
|
177
187
|
) -> None:
|
|
178
188
|
"""Add a gold test case to the project's eval suite.
|
|
@@ -185,11 +195,7 @@ def add_case(
|
|
|
185
195
|
run, 2 on usage errors, 3 on transport/API errors.
|
|
186
196
|
"""
|
|
187
197
|
api_key = require_api_key(api_key)
|
|
188
|
-
|
|
189
|
-
UUID(project_id)
|
|
190
|
-
except ValueError:
|
|
191
|
-
typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
|
|
192
|
-
raise typer.Exit(EXIT_USAGE)
|
|
198
|
+
project_id = resolve_project_id(project_id, path / Path(base_path))
|
|
193
199
|
if not question.strip() or not gold_sql.strip():
|
|
194
200
|
typer.secho("--question and --gold-sql must not be empty.", fg=typer.colors.RED, err=True)
|
|
195
201
|
raise typer.Exit(EXIT_USAGE)
|
|
@@ -221,11 +227,11 @@ def run(
|
|
|
221
227
|
Path("."),
|
|
222
228
|
help="Repository checkout root (the directory containing the ontology export path).",
|
|
223
229
|
),
|
|
224
|
-
project_id: str = typer.Option(
|
|
225
|
-
|
|
230
|
+
project_id: Optional[str] = typer.Option(
|
|
231
|
+
None,
|
|
226
232
|
"--project",
|
|
227
233
|
envvar="CASSIS_PROJECT_ID",
|
|
228
|
-
help="Target Cassis project ID (UUID
|
|
234
|
+
help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
|
|
229
235
|
),
|
|
230
236
|
api_key: Optional[str] = typer.Option(
|
|
231
237
|
None,
|
|
@@ -272,18 +278,13 @@ def run(
|
|
|
272
278
|
) -> None:
|
|
273
279
|
"""Run the project's eval suite against your local ontology files.
|
|
274
280
|
|
|
275
|
-
Uploads the local
|
|
281
|
+
Uploads the local ontology file tree and scores it in-memory — nothing is pushed or
|
|
276
282
|
persisted in Cassis besides the eval run itself. With --branch, runs against
|
|
277
283
|
an existing Cassis branch instead (no files are sent). Exits 0 when the run
|
|
278
284
|
completes with every case passed, 1 on any failed case / failed run /
|
|
279
285
|
invalid tree, 2 on usage errors, 3 on transport errors or --timeout.
|
|
280
286
|
"""
|
|
281
287
|
api_key = require_api_key(api_key)
|
|
282
|
-
try:
|
|
283
|
-
UUID(project_id)
|
|
284
|
-
except ValueError:
|
|
285
|
-
typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
|
|
286
|
-
raise typer.Exit(EXIT_USAGE)
|
|
287
288
|
if branch is not None and label is not None:
|
|
288
289
|
typer.secho(
|
|
289
290
|
"--label cannot be used with --branch: branch runs are labelled with the branch name.",
|
|
@@ -297,6 +298,7 @@ def run(
|
|
|
297
298
|
files, base_path = collect_tree(path, base_path)
|
|
298
299
|
if label is None:
|
|
299
300
|
label = _git_branch(path)
|
|
301
|
+
project_id = resolve_project_id(project_id, path / Path(base_path))
|
|
300
302
|
|
|
301
303
|
try:
|
|
302
304
|
run_record = post_eval_run_start(
|
|
@@ -6,7 +6,7 @@ identical — the backend image doesn't ship ``docs/``, so the CLI carries its o
|
|
|
6
6
|
copy). ``pull`` writes it into the checkout as ``<base_path>/AGENTS.md`` and
|
|
7
7
|
``fmt`` keeps it canonical, so a repo-aware agent loads current Cassis modeling
|
|
8
8
|
doctrine by convention. The file is managed: a banner marks it generated and the
|
|
9
|
-
CLI overwrites local edits, exactly as ``fmt`` rewrites drifted ontology
|
|
9
|
+
CLI overwrites local edits, exactly as ``fmt`` rewrites drifted ontology files.
|
|
10
10
|
|
|
11
11
|
Two writers manage the file — this CLI and the Cassis server's git export — and
|
|
12
12
|
they may run different doctrine versions (the guide ships inside each). The
|
|
@@ -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 = 4
|
|
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
|
|
@@ -5,7 +5,6 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import List, Optional
|
|
8
|
-
from uuid import UUID
|
|
9
8
|
|
|
10
9
|
import typer
|
|
11
10
|
from cassis_cli.api import (
|
|
@@ -29,7 +28,9 @@ from cassis_cli.common import (
|
|
|
29
28
|
)
|
|
30
29
|
from cassis_cli.common import collect_files as _collect_files
|
|
31
30
|
from cassis_cli.common import collect_tree as _collect_tree
|
|
31
|
+
from cassis_cli.common import is_legacy_domain_file as _is_legacy_domain_file
|
|
32
32
|
from cassis_cli.common import require_api_key as _require_api_key
|
|
33
|
+
from cassis_cli.common import resolve_project_id as _resolve_project_id
|
|
33
34
|
from cassis_cli.guide import DOCTRINE_VERSION, GUIDE_FILENAME, guide_status, refresh_guide
|
|
34
35
|
|
|
35
36
|
app = typer.Typer(no_args_is_help=True, help="Ontology commands.")
|
|
@@ -113,11 +114,11 @@ def pull(
|
|
|
113
114
|
Path("."),
|
|
114
115
|
help="Repository checkout root (the directory containing the ontology export path).",
|
|
115
116
|
),
|
|
116
|
-
project_id: str = typer.Option(
|
|
117
|
-
|
|
117
|
+
project_id: Optional[str] = typer.Option(
|
|
118
|
+
None,
|
|
118
119
|
"--project",
|
|
119
120
|
envvar="CASSIS_PROJECT_ID",
|
|
120
|
-
help="Source Cassis project ID (UUID
|
|
121
|
+
help="Source Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
|
|
121
122
|
),
|
|
122
123
|
api_key: Optional[str] = typer.Option(
|
|
123
124
|
None,
|
|
@@ -140,28 +141,24 @@ def pull(
|
|
|
140
141
|
prune: bool = typer.Option(
|
|
141
142
|
True,
|
|
142
143
|
"--prune/--no-prune",
|
|
143
|
-
help="Delete local
|
|
144
|
+
help="Delete local ontology files that no longer exist in the project's ontology (default: prune).",
|
|
144
145
|
),
|
|
145
146
|
json_output: bool = typer.Option(False, "--json", help="Print a JSON summary of written/deleted files."),
|
|
146
147
|
) -> None:
|
|
147
148
|
"""Download the project's unpublished ontology into a repository checkout.
|
|
148
149
|
|
|
149
|
-
Writes the ontology
|
|
150
|
-
overwritten and, unless --no-prune, stale local
|
|
151
|
-
the checkout ends up matching the project exactly). Review the changes with
|
|
150
|
+
Writes the ontology tree under the export path (full sync: files are
|
|
151
|
+
overwritten and, unless --no-prune, stale local ontology files are deleted,
|
|
152
|
+
so the checkout ends up matching the project exactly). Review the changes with
|
|
152
153
|
git diff before committing. Exits 0 on success, 2 on usage errors, 3 on
|
|
153
154
|
transport/API errors.
|
|
154
155
|
"""
|
|
155
156
|
api_key = _require_api_key(api_key)
|
|
156
|
-
try:
|
|
157
|
-
UUID(project_id)
|
|
158
|
-
except ValueError:
|
|
159
|
-
typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
|
|
160
|
-
raise typer.Exit(EXIT_USAGE)
|
|
161
157
|
base_path = base_path.strip().strip("/")
|
|
162
158
|
if not base_path:
|
|
163
159
|
typer.secho("--base-path must not be empty.", fg=typer.colors.RED, err=True)
|
|
164
160
|
raise typer.Exit(EXIT_USAGE)
|
|
161
|
+
project_id = _resolve_project_id(project_id, path / Path(base_path))
|
|
165
162
|
|
|
166
163
|
try:
|
|
167
164
|
files = get_ontology_export(api_url=api_url, api_key=api_key, project_id=project_id)
|
|
@@ -221,6 +218,13 @@ def pull(
|
|
|
221
218
|
if guide_written:
|
|
222
219
|
summary += f"; wrote {base_path}/{GUIDE_FILENAME}"
|
|
223
220
|
typer.secho(f"{summary}.", fg=typer.colors.GREEN)
|
|
221
|
+
migrated = sum(1 for rel in deleted if _is_legacy_domain_file(rel))
|
|
222
|
+
if migrated:
|
|
223
|
+
typer.secho(
|
|
224
|
+
f" Migrated {migrated} domain(s) to Markdown README.md files; "
|
|
225
|
+
"the old _domain.yml / _project.yml were removed. Review the diff before committing.",
|
|
226
|
+
fg=typer.colors.YELLOW,
|
|
227
|
+
)
|
|
224
228
|
raise typer.Exit(EXIT_OK)
|
|
225
229
|
|
|
226
230
|
|
|
@@ -230,11 +234,11 @@ def upload(
|
|
|
230
234
|
Path("."),
|
|
231
235
|
help="Repository checkout root (the directory containing the ontology export path).",
|
|
232
236
|
),
|
|
233
|
-
project_id: str = typer.Option(
|
|
234
|
-
|
|
237
|
+
project_id: Optional[str] = typer.Option(
|
|
238
|
+
None,
|
|
235
239
|
"--project",
|
|
236
240
|
envvar="CASSIS_PROJECT_ID",
|
|
237
|
-
help="Target Cassis project ID (UUID
|
|
241
|
+
help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
|
|
238
242
|
),
|
|
239
243
|
api_key: Optional[str] = typer.Option(
|
|
240
244
|
None,
|
|
@@ -270,12 +274,8 @@ def upload(
|
|
|
270
274
|
usage errors, 3 on transport/API errors.
|
|
271
275
|
"""
|
|
272
276
|
api_key = _require_api_key(api_key)
|
|
273
|
-
try:
|
|
274
|
-
UUID(project_id)
|
|
275
|
-
except ValueError:
|
|
276
|
-
typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
|
|
277
|
-
raise typer.Exit(EXIT_USAGE)
|
|
278
277
|
files, base_path = _collect_tree(path, base_path)
|
|
278
|
+
project_id = _resolve_project_id(project_id, path / Path(base_path))
|
|
279
279
|
|
|
280
280
|
try:
|
|
281
281
|
result = post_ontology_import(
|
|
@@ -377,7 +377,7 @@ def fmt(
|
|
|
377
377
|
|
|
378
378
|
changed = result["changed_paths"]
|
|
379
379
|
removed = result["removed_paths"]
|
|
380
|
-
# The managed AGENTS.md guide is canonicalized alongside the
|
|
380
|
+
# The managed AGENTS.md guide is canonicalized alongside the ontology tree
|
|
381
381
|
# (it isn't in the tree, so the server round-trip above never sees it).
|
|
382
382
|
# A guide stamped with a NEWER doctrine than this CLI carries is left
|
|
383
383
|
# alone and does not fail --check: the repo is fine, the CLI is old.
|
|
@@ -422,6 +422,13 @@ def fmt(
|
|
|
422
422
|
+ ". Review the diff: fields Cassis does not recognize are dropped.",
|
|
423
423
|
fg=typer.colors.YELLOW,
|
|
424
424
|
)
|
|
425
|
+
migrated = sum(1 for p in removed if _is_legacy_domain_file(p))
|
|
426
|
+
if migrated:
|
|
427
|
+
typer.secho(
|
|
428
|
+
f"Migrated {migrated} domain(s) to Markdown README.md files; "
|
|
429
|
+
"the old _domain.yml / _project.yml were removed.",
|
|
430
|
+
fg=typer.colors.YELLOW,
|
|
431
|
+
)
|
|
425
432
|
else:
|
|
426
433
|
# Only the guide was refreshed — the "rewrote ..." line above already said so.
|
|
427
434
|
typer.secho(f"✓ {len(files)} file(s) already canonical.", fg=typer.colors.GREEN)
|
|
@@ -440,11 +447,11 @@ def test(
|
|
|
440
447
|
"-q",
|
|
441
448
|
help="Natural-language question to probe (repeat for several).",
|
|
442
449
|
),
|
|
443
|
-
project_id: str = typer.Option(
|
|
444
|
-
|
|
450
|
+
project_id: Optional[str] = typer.Option(
|
|
451
|
+
None,
|
|
445
452
|
"--project",
|
|
446
453
|
envvar="CASSIS_PROJECT_ID",
|
|
447
|
-
help="Target Cassis project ID (UUID
|
|
454
|
+
help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
|
|
448
455
|
),
|
|
449
456
|
api_key: Optional[str] = typer.Option(
|
|
450
457
|
None,
|
|
@@ -478,12 +485,8 @@ def test(
|
|
|
478
485
|
invalid or a probe failed, 2 on usage errors, 3 on transport errors.
|
|
479
486
|
"""
|
|
480
487
|
api_key = _require_api_key(api_key)
|
|
481
|
-
try:
|
|
482
|
-
UUID(project_id)
|
|
483
|
-
except ValueError:
|
|
484
|
-
typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
|
|
485
|
-
raise typer.Exit(EXIT_USAGE)
|
|
486
488
|
files, base_path = _collect_tree(path, base_path)
|
|
489
|
+
project_id = _resolve_project_id(project_id, path / Path(base_path))
|
|
487
490
|
|
|
488
491
|
outcomes: "list[dict]" = []
|
|
489
492
|
failed = False
|
|
@@ -13,13 +13,13 @@ Read this before proposing an ontology change.
|
|
|
13
13
|
## 1. What a Cassis ontology is
|
|
14
14
|
|
|
15
15
|
The ontology is the curated business context the agent uses to translate a
|
|
16
|
-
natural-language question into SQL. It has five kinds of object plus
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
natural-language question into SQL. It has five kinds of object plus a project-level root context, edited as
|
|
17
|
+
files under the repository's ontology directory (`cassis/` by default) —
|
|
18
|
+
domains as Markdown, everything else as YAML:
|
|
19
19
|
|
|
20
20
|
| Layer | What it is | Key fields |
|
|
21
21
|
|---|---|---|
|
|
22
|
-
| **Root context** (`
|
|
22
|
+
| **Root context** (`domains/README.md` body) | One free-text block, always in the agent's prompt | markdown |
|
|
23
23
|
| **Domains** | A navigable tree grouping the business by subject area | `path`, `display_name`, `description`, `context_md` |
|
|
24
24
|
| **Tables** | A physical warehouse table (introspected) or a virtual one (SQL-defined) placed in a domain | `name` (`schema.TABLE`), `description`, `synonyms`, `grain`, columns |
|
|
25
25
|
| **Columns** | Enrichment on a table's columns | `description`, `unit`, `synonyms` |
|
|
@@ -31,11 +31,12 @@ On disk, the export layout is fixed — create each object in its canonical home
|
|
|
31
31
|
|
|
32
32
|
```
|
|
33
33
|
cassis/ (the git-sync base path)
|
|
34
|
-
|
|
35
|
-
domains
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
project.yml project identity: project id + Cassis format version (written by publish / pull)
|
|
35
|
+
domains/README.md root domain (path ""): frontmatter (type/title/description) + context_md body
|
|
36
|
+
domains/<path>/README.md one per sub-domain, nested by path (same Markdown format)
|
|
37
|
+
tables/<schema>/<table>.yml one per table, columns inline (YAML)
|
|
38
|
+
metrics/<name>.yml one per metric (YAML)
|
|
39
|
+
joins.yml ALL joins, one list in one file (YAML)
|
|
39
40
|
```
|
|
40
41
|
|
|
41
42
|
Joins and metrics are never embedded inside a table's file, and a table's file
|
|
@@ -45,7 +46,7 @@ time (run `cassis ontology fmt` to see what would be lost).
|
|
|
45
46
|
The **published** ontology is an immutable numbered snapshot — what production
|
|
46
47
|
answers from. The **unpublished** ontology is the editable state (the published
|
|
47
48
|
base plus unpublished changes). In a git-synced project the repository *is* the
|
|
48
|
-
edit surface: you edit the
|
|
49
|
+
edit surface: you edit the files, open a pull request, and merging syncs and
|
|
49
50
|
publishes it.
|
|
50
51
|
|
|
51
52
|
Physical tables and their columns come from schema introspection — you never
|
|
@@ -151,6 +152,12 @@ its own — an empty navigational domain is just noise. Every table and every
|
|
|
151
152
|
metric names a `domain_path` that must resolve to a domain you've declared (or the
|
|
152
153
|
root, `""`).
|
|
153
154
|
|
|
155
|
+
Each domain is a Markdown file (`domains/<path>/README.md`, or `domains/README.md`
|
|
156
|
+
for the root). Its YAML frontmatter holds the structured fields — `type: Domain`,
|
|
157
|
+
`title:` (the display name), `description:` — and the Markdown body **is** the
|
|
158
|
+
domain's `context_md`. Write the display name as `title`, not `display_name`: an
|
|
159
|
+
unrecognized frontmatter key is dropped on sync.
|
|
160
|
+
|
|
154
161
|
A useful convention inside `context_md`: a `## Terms` section for the domain's
|
|
155
162
|
vocabulary and disambiguation, and a `## Notes` section for routing hints and
|
|
156
163
|
scope caveats. Omit either if you have nothing for it.
|
|
@@ -167,7 +174,7 @@ scope caveats. Omit either if you have nothing for it.
|
|
|
167
174
|
- Markdown **links** to related domains, using the resolvable domain path
|
|
168
175
|
(`[berries](play/features/berries)`), not a bare filename.
|
|
169
176
|
|
|
170
|
-
**Root context** — the
|
|
177
|
+
**Root context** — the body of the root domain (`domains/README.md`), injected into every
|
|
171
178
|
conversation — **holds only what applies across almost every query:** corporate
|
|
172
179
|
identity, the core entity hierarchy (how the central models relate — e.g. "users
|
|
173
180
|
belong to companies via enrollments; primary enrollments are employees, partner
|
|
@@ -379,13 +386,37 @@ description, so a wrong example is worse than none.
|
|
|
379
386
|
|
|
380
387
|
---
|
|
381
388
|
|
|
382
|
-
## 12.
|
|
389
|
+
## 12. Extension passes: structure before content
|
|
390
|
+
|
|
391
|
+
Adding a source or a whole subject area inverts the scoping bullet above. §11 is
|
|
392
|
+
written for maintenance edits, where a structural change is genuinely adjacent;
|
|
393
|
+
on an extension pass the tree *is* the change — every table and metric names a
|
|
394
|
+
`domain_path`, so content filled into a hierarchy you already doubt all has to
|
|
395
|
+
move. Same human opt-in, front-loaded:
|
|
396
|
+
|
|
397
|
+
1. **Scope.** Agree what is being added, from which sources, in what order.
|
|
398
|
+
2. **Structure.** Evaluate the existing hierarchy against what is arriving and
|
|
399
|
+
propose the tree changes — new domains, splits, moves — for approval *before*
|
|
400
|
+
any content. On approval, create the new domain files frontmatter-only (empty
|
|
401
|
+
body), so every `domain_path` resolves while you fill.
|
|
402
|
+
3. **Fill bottom-up.** Column and table facts, then metrics and joins, then each
|
|
403
|
+
domain README body last, written from what is left over. That residue is by
|
|
404
|
+
construction the cross-table connective tissue domain level owns, so the order
|
|
405
|
+
enforces the one-home rule instead of leaving it to vigilance. Bottom-up is an
|
|
406
|
+
authoring order, not a holding rule: a fact learned out of order still lands at
|
|
407
|
+
its owning layer immediately (§11). If filling shows the approved tree is wrong
|
|
408
|
+
— a domain overloads, or wants to split — stop and return to 2 rather than keep
|
|
409
|
+
filling into it.
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## 13. Working in a git-synced repo
|
|
383
414
|
|
|
384
|
-
The repository is the source of truth. Edit the
|
|
415
|
+
The repository is the source of truth. Edit the files, then verify before opening
|
|
385
416
|
a pull request — the CLI runs the same checks the platform does, from your
|
|
386
417
|
checkout:
|
|
387
418
|
|
|
388
|
-
- `cassis ontology check` — validate the files (
|
|
419
|
+
- `cassis ontology check` — validate the files (parse, round-trip, semantic
|
|
389
420
|
checks); the same gate that runs on the pull request.
|
|
390
421
|
- `cassis ontology fmt` — rewrite the files in canonical form, so hand or agent
|
|
391
422
|
edits round-trip cleanly and any dropped/unknown fields become visible in the
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cassis-cli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.1.1"
|
|
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
|
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
"""Helpers shared by the `cassis` subcommands (tree collection, auth, exit codes)."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
7
|
-
|
|
8
|
-
import typer
|
|
9
|
-
|
|
10
|
-
# Repository directory the ontology tree is exported under. Must match the
|
|
11
|
-
# project's git-sync "Path" setting in Cassis (default "cassis").
|
|
12
|
-
DEFAULT_BASE_PATH = "cassis"
|
|
13
|
-
|
|
14
|
-
# Exit codes (documented in the README; stable contract for CI scripts).
|
|
15
|
-
EXIT_OK = 0
|
|
16
|
-
EXIT_VALIDATION_FAILED = 1
|
|
17
|
-
EXIT_USAGE = 2
|
|
18
|
-
EXIT_TRANSPORT = 3
|
|
19
|
-
|
|
20
|
-
# Request ceilings of the /api/ci file-tree endpoints, mirrored so oversized
|
|
21
|
-
# trees fail fast with a clear message before any upload. Source of truth:
|
|
22
|
-
# backend/app/schemas/ci.py (the server's 422 remains the backstop).
|
|
23
|
-
MAX_FILES = 2000
|
|
24
|
-
MAX_TOTAL_BYTES = 5 * 1024 * 1024
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def collect_files(ontology_dir: Path) -> dict[str, str]:
|
|
28
|
-
"""Read every YAML file under the ontology dir, keyed by posix relpath.
|
|
29
|
-
|
|
30
|
-
Exits 2 (usage) on an unreadable or non-UTF-8 file — a local checkout
|
|
31
|
-
problem, reported before anything is sent to the API.
|
|
32
|
-
"""
|
|
33
|
-
files: dict[str, str] = {}
|
|
34
|
-
for pattern in ("**/*.yml", "**/*.yaml"):
|
|
35
|
-
for file in sorted(ontology_dir.glob(pattern)):
|
|
36
|
-
if file.is_file():
|
|
37
|
-
try:
|
|
38
|
-
files[file.relative_to(ontology_dir).as_posix()] = file.read_text(encoding="utf-8")
|
|
39
|
-
except (UnicodeDecodeError, OSError) as exc:
|
|
40
|
-
typer.secho(f"Cannot read {file}: {exc}", fg=typer.colors.RED, err=True)
|
|
41
|
-
raise typer.Exit(EXIT_USAGE) from exc
|
|
42
|
-
return files
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def require_api_key(api_key: Optional[str]) -> str:
|
|
46
|
-
"""Exit 2 (usage) when no API key was provided."""
|
|
47
|
-
if not api_key:
|
|
48
|
-
typer.secho(
|
|
49
|
-
"No API key. Set CASSIS_API_KEY or pass --api-key "
|
|
50
|
-
"(create one in Cassis under Organization settings -> API keys).",
|
|
51
|
-
fg=typer.colors.RED,
|
|
52
|
-
err=True,
|
|
53
|
-
)
|
|
54
|
-
raise typer.Exit(EXIT_USAGE)
|
|
55
|
-
return api_key
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def collect_tree(path: Path, base_path: str) -> "tuple[dict[str, str], str]":
|
|
59
|
-
"""Resolve the ontology dir under the checkout and read its YAML tree.
|
|
60
|
-
|
|
61
|
-
Returns ``(files, normalized_base_path)``. Exits 2 (usage) on an empty or
|
|
62
|
-
missing dir, or a tree beyond the API request ceilings — all local checkout
|
|
63
|
-
problems, reported before anything is sent to the API.
|
|
64
|
-
"""
|
|
65
|
-
base_path = base_path.strip().strip("/")
|
|
66
|
-
if not base_path:
|
|
67
|
-
typer.secho("--base-path must not be empty.", fg=typer.colors.RED, err=True)
|
|
68
|
-
raise typer.Exit(EXIT_USAGE)
|
|
69
|
-
|
|
70
|
-
ontology_dir = path / Path(base_path)
|
|
71
|
-
if not ontology_dir.is_dir():
|
|
72
|
-
typer.secho(
|
|
73
|
-
f"No {base_path}/ directory found under {path}. "
|
|
74
|
-
"If the project exports to a custom path, pass it with --base-path (or CASSIS_BASE_PATH).",
|
|
75
|
-
fg=typer.colors.RED,
|
|
76
|
-
err=True,
|
|
77
|
-
)
|
|
78
|
-
raise typer.Exit(EXIT_USAGE)
|
|
79
|
-
|
|
80
|
-
files = collect_files(ontology_dir)
|
|
81
|
-
if not files:
|
|
82
|
-
typer.secho(f"No YAML files found under {ontology_dir}.", fg=typer.colors.RED, err=True)
|
|
83
|
-
raise typer.Exit(EXIT_USAGE)
|
|
84
|
-
|
|
85
|
-
total_bytes = sum(len(content.encode()) for content in files.values())
|
|
86
|
-
if len(files) > MAX_FILES or total_bytes > MAX_TOTAL_BYTES:
|
|
87
|
-
typer.secho(
|
|
88
|
-
f"Ontology tree too large: {len(files)} files / {total_bytes / (1024 * 1024):.1f} MB "
|
|
89
|
-
f"(limits: {MAX_FILES} files / {MAX_TOTAL_BYTES // (1024 * 1024)} MB). "
|
|
90
|
-
"Check that --base-path points at the ontology directory, not a larger tree.",
|
|
91
|
-
fg=typer.colors.RED,
|
|
92
|
-
err=True,
|
|
93
|
-
)
|
|
94
|
-
raise typer.Exit(EXIT_USAGE)
|
|
95
|
-
return files, base_path
|
|
File without changes
|