katalyst-taxonomy-cli 2.0.1a1__py3-none-any.whl
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.
- katalyst_taxonomy_cli/__init__.py +14 -0
- katalyst_taxonomy_cli/__main__.py +12 -0
- katalyst_taxonomy_cli/_wiring.py +248 -0
- katalyst_taxonomy_cli/app.py +550 -0
- katalyst_taxonomy_cli/commands/__init__.py +1 -0
- katalyst_taxonomy_cli/commands/api.py +104 -0
- katalyst_taxonomy_cli/commands/backend.py +96 -0
- katalyst_taxonomy_cli/commands/bundle.py +106 -0
- katalyst_taxonomy_cli/commands/clone.py +68 -0
- katalyst_taxonomy_cli/commands/config.py +748 -0
- katalyst_taxonomy_cli/commands/config_filters.py +159 -0
- katalyst_taxonomy_cli/commands/config_home.py +179 -0
- katalyst_taxonomy_cli/commands/config_plugins.py +180 -0
- katalyst_taxonomy_cli/commands/config_profile.py +604 -0
- katalyst_taxonomy_cli/commands/config_schemas.py +257 -0
- katalyst_taxonomy_cli/commands/config_sources.py +141 -0
- katalyst_taxonomy_cli/commands/core.py +468 -0
- katalyst_taxonomy_cli/commands/create.py +145 -0
- katalyst_taxonomy_cli/commands/deps.py +48 -0
- katalyst_taxonomy_cli/commands/diff.py +63 -0
- katalyst_taxonomy_cli/commands/drafts.py +63 -0
- katalyst_taxonomy_cli/commands/export.py +186 -0
- katalyst_taxonomy_cli/commands/init.py +84 -0
- katalyst_taxonomy_cli/commands/kinds.py +302 -0
- katalyst_taxonomy_cli/commands/lifecycle.py +127 -0
- katalyst_taxonomy_cli/commands/lint.py +256 -0
- katalyst_taxonomy_cli/commands/plugin.py +148 -0
- katalyst_taxonomy_cli/commands/promote.py +95 -0
- katalyst_taxonomy_cli/commands/query.py +187 -0
- katalyst_taxonomy_cli/commands/relate.py +137 -0
- katalyst_taxonomy_cli/commands/relocate.py +129 -0
- katalyst_taxonomy_cli/commands/remove.py +95 -0
- katalyst_taxonomy_cli/commands/resolve.py +48 -0
- katalyst_taxonomy_cli/commands/rollback.py +51 -0
- katalyst_taxonomy_cli/commands/self_update.py +530 -0
- katalyst_taxonomy_cli/commands/show.py +33 -0
- katalyst_taxonomy_cli/commands/status.py +107 -0
- katalyst_taxonomy_cli/commands/sync.py +60 -0
- katalyst_taxonomy_cli/commands/update.py +112 -0
- katalyst_taxonomy_cli/commands/versions.py +54 -0
- katalyst_taxonomy_cli/context.py +145 -0
- katalyst_taxonomy_cli/discovery.py +157 -0
- katalyst_taxonomy_cli/fields.py +70 -0
- katalyst_taxonomy_cli/install.py +330 -0
- katalyst_taxonomy_cli/options.py +20 -0
- katalyst_taxonomy_cli/output.py +92 -0
- katalyst_taxonomy_cli/py.typed +0 -0
- katalyst_taxonomy_cli-2.0.1a1.dist-info/METADATA +12 -0
- katalyst_taxonomy_cli-2.0.1a1.dist-info/RECORD +51 -0
- katalyst_taxonomy_cli-2.0.1a1.dist-info/WHEEL +4 -0
- katalyst_taxonomy_cli-2.0.1a1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""``kata core`` — katalyst-core, the LinkML module every schema module imports.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
kata core check [--module PATH] hold a module (katalyst-core by default) to the
|
|
6
|
+
core contract: URIs under declared prefixes, a
|
|
7
|
+
delete policy on every relationship
|
|
8
|
+
kata core save RECORDS OUT write a records document to its graph form (Turtle)
|
|
9
|
+
kata core load GRAPH read a graph back as a records document (YAML)
|
|
10
|
+
kata core validate RECORDS run the local shapes (generated from the installed
|
|
11
|
+
modules); fails on a violation
|
|
12
|
+
kata core coverage RECORDS run the global shapes (gates); reports, never fails
|
|
13
|
+
kata core kinds every node kind the installed modules define
|
|
14
|
+
kata core query RECORDS [--module PATH] [--is-a CLASS] [--name NAME] [--as-of DATE]
|
|
15
|
+
[--engine in-memory|oxigraph]
|
|
16
|
+
ask the query interface; the engine is a choice
|
|
17
|
+
|
|
18
|
+
kata core schema diff [--bundle B] [--from VERSION] [--json] [--write-migration]
|
|
19
|
+
the classified diff of a schema's modules against
|
|
20
|
+
an archived version; a migration skeleton
|
|
21
|
+
kata core schema check [--bundle B]
|
|
22
|
+
the version gate: the bump and the migration match
|
|
23
|
+
the diff
|
|
24
|
+
kata core schema archive --bundle B
|
|
25
|
+
archive the current modules as the released version
|
|
26
|
+
|
|
27
|
+
Records and graphs are read against every installed module (``katalyst_core.modules``).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
import click
|
|
36
|
+
|
|
37
|
+
__all__ = ["core"]
|
|
38
|
+
|
|
39
|
+
_EXISTING = click.Path(exists=True, dir_okay=False, path_type=Path)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@click.group("core")
|
|
43
|
+
def core() -> None:
|
|
44
|
+
"""katalyst-core: the base schema every Katalyst module imports."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@core.command("check")
|
|
48
|
+
@click.option(
|
|
49
|
+
"--module",
|
|
50
|
+
"module",
|
|
51
|
+
type=_EXISTING,
|
|
52
|
+
default=None,
|
|
53
|
+
help="A LinkML module that imports katalyst_core. Default: katalyst-core itself.",
|
|
54
|
+
)
|
|
55
|
+
def check(module: Path | None) -> None:
|
|
56
|
+
"""Hold a LinkML module to the core contract."""
|
|
57
|
+
from katalyst_core import installed_modules, open_module
|
|
58
|
+
from katalyst_core.contract import check_contract
|
|
59
|
+
from katalyst_core.local import LocalModuleError, check_local_module
|
|
60
|
+
|
|
61
|
+
if module is not None:
|
|
62
|
+
try:
|
|
63
|
+
check_local_module(module, installed_modules())
|
|
64
|
+
except LocalModuleError as exc:
|
|
65
|
+
raise click.ClickException(str(exc)) from exc
|
|
66
|
+
try:
|
|
67
|
+
report = check_contract(open_module(module))
|
|
68
|
+
except Exception as exc: # a module LinkML itself cannot open
|
|
69
|
+
raise click.ClickException(f"cannot open {module}: {exc}") from exc
|
|
70
|
+
if not report.ok:
|
|
71
|
+
for problem in report.problems:
|
|
72
|
+
click.echo(problem, err=True)
|
|
73
|
+
raise click.ClickException(
|
|
74
|
+
f"{report.module}: {len(report.problems)} problem(s) with the core contract"
|
|
75
|
+
)
|
|
76
|
+
click.echo(
|
|
77
|
+
f"OK: {report.module} ({report.classes} classes, {report.slots} slots): "
|
|
78
|
+
"every class and slot has a URI under a declared prefix; "
|
|
79
|
+
"every relationship has a delete policy"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@core.command("save")
|
|
84
|
+
@click.argument("records", type=_EXISTING)
|
|
85
|
+
@click.argument("out", type=click.Path(dir_okay=False, path_type=Path))
|
|
86
|
+
def save(records: Path, out: Path) -> None:
|
|
87
|
+
"""Write the records in RECORDS (YAML) to OUT as an RDF graph (Turtle)."""
|
|
88
|
+
import yaml
|
|
89
|
+
from katalyst_core import open_installed
|
|
90
|
+
from katalyst_core.graph import CoreDataError, to_graph
|
|
91
|
+
|
|
92
|
+
document: Any = yaml.safe_load(records.read_text(encoding="utf-8")) or {}
|
|
93
|
+
if not isinstance(document, dict):
|
|
94
|
+
raise click.ClickException(f"{records}: expected a mapping with prefixes and records")
|
|
95
|
+
try:
|
|
96
|
+
graph = to_graph(open_installed(), document) # pyright: ignore[reportUnknownArgumentType]
|
|
97
|
+
except CoreDataError as exc:
|
|
98
|
+
raise click.ClickException(f"{records}: {exc}") from exc
|
|
99
|
+
out.write_text(graph.serialize(format="turtle"), encoding="utf-8")
|
|
100
|
+
click.echo(f"Saved {len(document.get('records') or [])} record(s) to {out}") # pyright: ignore[reportUnknownArgumentType, reportUnknownMemberType]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@core.command("load")
|
|
104
|
+
@click.argument("graph_file", metavar="GRAPH", type=_EXISTING)
|
|
105
|
+
def load(graph_file: Path) -> None:
|
|
106
|
+
"""Read the records in GRAPH (Turtle) and print them as YAML."""
|
|
107
|
+
import yaml
|
|
108
|
+
from katalyst_core import open_installed
|
|
109
|
+
from katalyst_core.graph import from_graph
|
|
110
|
+
from rdflib import Graph
|
|
111
|
+
|
|
112
|
+
graph = Graph(bind_namespaces="none")
|
|
113
|
+
graph.parse(graph_file, format="turtle")
|
|
114
|
+
document = from_graph(open_installed(), graph)
|
|
115
|
+
click.echo(yaml.safe_dump(document, sort_keys=False, allow_unicode=True), nl=False)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _records_graph(records: Path, modules: tuple[Path, ...] = ()) -> tuple[Any, Any]:
|
|
119
|
+
"""The installed modules (and local ``modules``), and ``records`` as an RDF graph."""
|
|
120
|
+
import yaml
|
|
121
|
+
from katalyst_core import local_modules, open_installed
|
|
122
|
+
from katalyst_core.graph import CoreDataError, to_graph
|
|
123
|
+
|
|
124
|
+
document: Any = yaml.safe_load(records.read_text(encoding="utf-8")) or {}
|
|
125
|
+
if not isinstance(document, dict):
|
|
126
|
+
raise click.ClickException(f"{records}: expected a mapping with prefixes and records")
|
|
127
|
+
from katalyst_core.local import LocalModuleError
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
sv = open_installed(local_modules(modules))
|
|
131
|
+
except LocalModuleError as exc:
|
|
132
|
+
raise click.ClickException(str(exc)) from exc
|
|
133
|
+
try:
|
|
134
|
+
return sv, to_graph(sv, document) # pyright: ignore[reportUnknownArgumentType]
|
|
135
|
+
except CoreDataError as exc:
|
|
136
|
+
raise click.ClickException(f"{records}: {exc}") from exc
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@core.command("validate")
|
|
140
|
+
@click.argument("records", type=_EXISTING)
|
|
141
|
+
@click.option(
|
|
142
|
+
"--module",
|
|
143
|
+
"modules",
|
|
144
|
+
type=_EXISTING,
|
|
145
|
+
multiple=True,
|
|
146
|
+
help="A local LinkML module to read the records against (repeatable).",
|
|
147
|
+
)
|
|
148
|
+
def validate(records: Path, modules: tuple[Path, ...]) -> None:
|
|
149
|
+
"""Run the local shapes over RECORDS; fail on any violation."""
|
|
150
|
+
from katalyst_core.shapes import local_shapes, run_shapes
|
|
151
|
+
|
|
152
|
+
sv, graph = _records_graph(records, modules)
|
|
153
|
+
findings = run_shapes(sv, graph, local_shapes(sv))
|
|
154
|
+
violations = [f for f in findings if f.severity == "violation"]
|
|
155
|
+
for finding in findings:
|
|
156
|
+
click.echo(str(finding), err=finding.severity == "violation")
|
|
157
|
+
if violations:
|
|
158
|
+
raise click.ClickException(f"{records}: {len(violations)} violation(s)")
|
|
159
|
+
click.echo(f"OK: {records} conforms to the installed modules")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@core.command("coverage")
|
|
163
|
+
@click.argument("records", type=_EXISTING)
|
|
164
|
+
@click.option(
|
|
165
|
+
"--module",
|
|
166
|
+
"modules",
|
|
167
|
+
type=_EXISTING,
|
|
168
|
+
multiple=True,
|
|
169
|
+
help="A local LinkML module to read the records against (repeatable).",
|
|
170
|
+
)
|
|
171
|
+
def coverage(records: Path, modules: tuple[Path, ...]) -> None:
|
|
172
|
+
"""Run the global shapes over RECORDS and report what they find (never fails).
|
|
173
|
+
|
|
174
|
+
The global shapes are the baseline's gates; they target the baseline classes, so they
|
|
175
|
+
also check every local subtype of them (``--module``).
|
|
176
|
+
"""
|
|
177
|
+
from katalyst_core.shapes import global_shapes, run_shapes
|
|
178
|
+
|
|
179
|
+
sv, graph = _records_graph(records, modules)
|
|
180
|
+
findings = run_shapes(sv, graph, global_shapes())
|
|
181
|
+
# A global rule is about the node as a whole, so its finding names the node alone.
|
|
182
|
+
for finding in findings:
|
|
183
|
+
click.echo(f"{finding.focus}: {finding.message}")
|
|
184
|
+
click.echo(f"{len(findings)} finding(s)")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@core.command("kinds")
|
|
188
|
+
def kinds() -> None:
|
|
189
|
+
"""Every node kind the installed modules define, one per line."""
|
|
190
|
+
from katalyst_core import kinds as module_kinds
|
|
191
|
+
from katalyst_core import open_installed
|
|
192
|
+
|
|
193
|
+
for kind in sorted(module_kinds(open_installed())):
|
|
194
|
+
click.echo(kind)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@core.command("query")
|
|
198
|
+
@click.argument("records", type=_EXISTING)
|
|
199
|
+
@click.option(
|
|
200
|
+
"--module",
|
|
201
|
+
"modules",
|
|
202
|
+
type=_EXISTING,
|
|
203
|
+
multiple=True,
|
|
204
|
+
help="A local LinkML module to read the records against (repeatable).",
|
|
205
|
+
)
|
|
206
|
+
@click.option("--is-a", "is_a", default=None, help="Records of this class or any subtype.")
|
|
207
|
+
@click.option("--name", default=None, help="Records with this name.")
|
|
208
|
+
@click.option(
|
|
209
|
+
"--as-of",
|
|
210
|
+
"as_of",
|
|
211
|
+
type=click.DateTime(formats=["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S"]),
|
|
212
|
+
default=None,
|
|
213
|
+
help="Only what was recorded as true at this date.",
|
|
214
|
+
)
|
|
215
|
+
@click.option(
|
|
216
|
+
"--engine",
|
|
217
|
+
type=click.Choice(["oxigraph", "in-memory"]),
|
|
218
|
+
default="oxigraph",
|
|
219
|
+
show_default=True,
|
|
220
|
+
help="The engine below the query interface.",
|
|
221
|
+
)
|
|
222
|
+
@click.option(
|
|
223
|
+
"--mappings",
|
|
224
|
+
"mapping_files",
|
|
225
|
+
type=_EXISTING,
|
|
226
|
+
multiple=True,
|
|
227
|
+
help=(
|
|
228
|
+
"An SSSOM mapping set (TSV, repeatable). Confirmed exact matches apply at query "
|
|
229
|
+
"time; every other row is reported and not applied."
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
def query(
|
|
233
|
+
records: Path,
|
|
234
|
+
modules: tuple[Path, ...],
|
|
235
|
+
is_a: str | None,
|
|
236
|
+
name: str | None,
|
|
237
|
+
as_of: Any,
|
|
238
|
+
engine: str,
|
|
239
|
+
mapping_files: tuple[Path, ...],
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Ask the query interface about RECORDS and print the matching records (YAML)."""
|
|
242
|
+
import yaml
|
|
243
|
+
from katalyst_core.mappings import MappingSet, read_sssom
|
|
244
|
+
from katalyst_core.query import QueryEngineUnavailable, open_engine
|
|
245
|
+
|
|
246
|
+
sv, graph = _records_graph(records, modules)
|
|
247
|
+
mappings = MappingSet.merge(read_sssom(f) for f in mapping_files) if mapping_files else None
|
|
248
|
+
for skipped in mappings.not_applied if mappings else ():
|
|
249
|
+
click.echo(f"Mapping not applied: {skipped}: {skipped.why_not()}", err=True)
|
|
250
|
+
try:
|
|
251
|
+
answer = open_engine(engine, sv, graph, mappings)
|
|
252
|
+
if is_a is not None:
|
|
253
|
+
found = answer.instances_of(is_a)
|
|
254
|
+
if name is not None or as_of is not None:
|
|
255
|
+
wanted = {str(r["id"]) for r in answer.find(name=name, as_of=as_of)}
|
|
256
|
+
found = [r for r in found if str(r["id"]) in wanted]
|
|
257
|
+
else:
|
|
258
|
+
found = answer.find(name=name, as_of=as_of)
|
|
259
|
+
except (QueryEngineUnavailable, LookupError) as exc:
|
|
260
|
+
raise click.ClickException(str(exc)) from exc
|
|
261
|
+
click.echo(yaml.safe_dump({"records": found}, sort_keys=False, allow_unicode=True), nl=False)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@core.command("migrate")
|
|
265
|
+
@click.argument("records", type=_EXISTING)
|
|
266
|
+
@click.option(
|
|
267
|
+
"--spec",
|
|
268
|
+
type=_EXISTING,
|
|
269
|
+
required=True,
|
|
270
|
+
help="The LinkML-Map transformation specification of the breaking change.",
|
|
271
|
+
)
|
|
272
|
+
@click.option(
|
|
273
|
+
"--from",
|
|
274
|
+
"source",
|
|
275
|
+
type=_EXISTING,
|
|
276
|
+
required=True,
|
|
277
|
+
help="The module the records were written against (the old baseline version).",
|
|
278
|
+
)
|
|
279
|
+
@click.option(
|
|
280
|
+
"--to",
|
|
281
|
+
"target",
|
|
282
|
+
type=_EXISTING,
|
|
283
|
+
required=True,
|
|
284
|
+
help="The module to migrate them to (the new baseline version).",
|
|
285
|
+
)
|
|
286
|
+
@click.option(
|
|
287
|
+
"--out",
|
|
288
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
289
|
+
default=None,
|
|
290
|
+
help="Where to write the migrated records. Default: print them.",
|
|
291
|
+
)
|
|
292
|
+
def migrate(records: Path, spec: Path, source: Path, target: Path, out: Path | None) -> None:
|
|
293
|
+
"""Migrate RECORDS from one baseline version to the next with a LinkML-Map spec.
|
|
294
|
+
|
|
295
|
+
Each record of a class the spec derives is transformed; every slot the spec does not
|
|
296
|
+
consume carries over unchanged. The result must conform to the new version.
|
|
297
|
+
"""
|
|
298
|
+
import yaml
|
|
299
|
+
from katalyst_core import open_module
|
|
300
|
+
from katalyst_core.graph import CoreDataError, to_graph
|
|
301
|
+
from katalyst_core.migrate import MigrationError, migrate_records
|
|
302
|
+
from katalyst_core.shapes import local_shapes, run_shapes
|
|
303
|
+
|
|
304
|
+
document: Any = yaml.safe_load(records.read_text(encoding="utf-8")) or {}
|
|
305
|
+
if not isinstance(document, dict):
|
|
306
|
+
raise click.ClickException(f"{records}: expected a mapping with prefixes and records")
|
|
307
|
+
old, new = open_module(source), open_module(target)
|
|
308
|
+
try:
|
|
309
|
+
migrated = migrate_records(document, spec=spec, source=old, target=new) # pyright: ignore[reportUnknownArgumentType]
|
|
310
|
+
graph = to_graph(new, migrated)
|
|
311
|
+
except (MigrationError, CoreDataError) as exc:
|
|
312
|
+
raise click.ClickException(f"{records}: {exc}") from exc
|
|
313
|
+
violations = [f for f in run_shapes(new, graph, local_shapes(new)) if f.severity == "violation"]
|
|
314
|
+
for finding in violations:
|
|
315
|
+
click.echo(str(finding), err=True)
|
|
316
|
+
if violations:
|
|
317
|
+
raise click.ClickException(
|
|
318
|
+
f"{records}: the migrated records break {target.name} ({len(violations)} violation(s))"
|
|
319
|
+
)
|
|
320
|
+
text = yaml.safe_dump(migrated, sort_keys=False, allow_unicode=True)
|
|
321
|
+
if out is None:
|
|
322
|
+
click.echo(text, nl=False)
|
|
323
|
+
else:
|
|
324
|
+
out.write_text(text, encoding="utf-8")
|
|
325
|
+
click.echo(f"Migrated {len(migrated['records'])} record(s) to {out}")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# --- kata core schema: versions, the archive, the diff, migrations ------------------
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@core.group("schema")
|
|
332
|
+
def schema() -> None:
|
|
333
|
+
"""Schema versions: the classified diff, the version gate, the archive.
|
|
334
|
+
|
|
335
|
+
A schema is katalyst-core or an installed bundle (by its manifest name). Its released
|
|
336
|
+
versions are archived beside its modules (``released/<version>/``), its migrations in
|
|
337
|
+
``migrations/<from>-to-<to>.yaml``.
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _releases(bundle: str | None) -> list[Any]:
|
|
342
|
+
from katalyst_engine.evolution import installed_releases
|
|
343
|
+
|
|
344
|
+
releases = installed_releases()
|
|
345
|
+
if bundle is None:
|
|
346
|
+
return releases
|
|
347
|
+
chosen = [r for r in releases if r.name == bundle]
|
|
348
|
+
if not chosen:
|
|
349
|
+
names = ", ".join(r.name for r in releases)
|
|
350
|
+
raise click.ClickException(f"no schema named {bundle!r}; installed: {names}")
|
|
351
|
+
return chosen
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
_BUNDLE = click.option(
|
|
355
|
+
"--bundle",
|
|
356
|
+
"bundle",
|
|
357
|
+
default=None,
|
|
358
|
+
help="katalyst-core or a bundle's name (default: every installed schema).",
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@schema.command("diff")
|
|
363
|
+
@_BUNDLE
|
|
364
|
+
@click.option(
|
|
365
|
+
"--from",
|
|
366
|
+
"since",
|
|
367
|
+
default=None,
|
|
368
|
+
help="The archived version to compare with (default: the latest).",
|
|
369
|
+
)
|
|
370
|
+
@click.option("--json", "as_json", is_flag=True, help="Print the changes as JSON.")
|
|
371
|
+
@click.option(
|
|
372
|
+
"--write-migration",
|
|
373
|
+
is_flag=True,
|
|
374
|
+
help="Write migrations/<from>-to-<to>.yaml with a step or an accept per breaking change.",
|
|
375
|
+
)
|
|
376
|
+
def schema_diff(
|
|
377
|
+
bundle: str | None, since: str | None, as_json: bool, write_migration: bool
|
|
378
|
+
) -> None:
|
|
379
|
+
"""Print the classified diff of the current modules against an archived version."""
|
|
380
|
+
import json
|
|
381
|
+
|
|
382
|
+
from katalyst_engine.core.version import Version
|
|
383
|
+
from katalyst_engine.evolution import migration_skeleton
|
|
384
|
+
from katalyst_engine.evolution.diff import diff_modules
|
|
385
|
+
from katalyst_engine.evolution.gate import required_bump
|
|
386
|
+
|
|
387
|
+
releases = _releases(bundle)
|
|
388
|
+
if write_migration and len(releases) != 1:
|
|
389
|
+
raise click.ClickException("--write-migration needs --bundle")
|
|
390
|
+
out: dict[str, Any] = {}
|
|
391
|
+
for release in releases:
|
|
392
|
+
try:
|
|
393
|
+
old = Version.parse(since) if since else release.latest_released()
|
|
394
|
+
except ValueError as exc:
|
|
395
|
+
raise click.ClickException(str(exc)) from exc
|
|
396
|
+
if old is None:
|
|
397
|
+
click.echo(f"{release.name} {release.version}: no released version is archived")
|
|
398
|
+
continue
|
|
399
|
+
try:
|
|
400
|
+
archived = release.released_modules(old)
|
|
401
|
+
except FileNotFoundError as exc:
|
|
402
|
+
raise click.ClickException(str(exc)) from exc
|
|
403
|
+
diff = diff_modules(archived, release.modules)
|
|
404
|
+
need = required_bump(diff.impact)
|
|
405
|
+
if as_json:
|
|
406
|
+
out[release.name] = {
|
|
407
|
+
"from": str(old),
|
|
408
|
+
"to": str(release.version),
|
|
409
|
+
"bump": need,
|
|
410
|
+
**diff.to_dict(),
|
|
411
|
+
}
|
|
412
|
+
else:
|
|
413
|
+
click.echo(diff.summary(f"{release.name} {old} -> {release.version}"))
|
|
414
|
+
if diff.changes:
|
|
415
|
+
click.echo(f"Needs a {need} bump.")
|
|
416
|
+
if write_migration:
|
|
417
|
+
new = release.version
|
|
418
|
+
if new <= old:
|
|
419
|
+
new = {"major": old.next_major(), "minor": old.next_minor()}.get(
|
|
420
|
+
need, old.next_patch()
|
|
421
|
+
)
|
|
422
|
+
path = release.migration_file(old, new)
|
|
423
|
+
if path.exists():
|
|
424
|
+
raise click.ClickException(f"{path} exists; edit it, or remove it to start again")
|
|
425
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
426
|
+
path.write_text(migration_skeleton(diff, old, new), encoding="utf-8")
|
|
427
|
+
click.echo(f"Wrote {path}", err=as_json)
|
|
428
|
+
if new != release.version:
|
|
429
|
+
click.echo(
|
|
430
|
+
f"Bump {release.name} to {new}: its manifest and every module's version",
|
|
431
|
+
err=as_json,
|
|
432
|
+
)
|
|
433
|
+
if as_json:
|
|
434
|
+
click.echo(json.dumps(out, indent=2, default=str))
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@schema.command("check")
|
|
438
|
+
@_BUNDLE
|
|
439
|
+
def schema_check(bundle: str | None) -> None:
|
|
440
|
+
"""The version gate: fail when a bump or a migration does not match the diff."""
|
|
441
|
+
from katalyst_engine.evolution import check_release
|
|
442
|
+
|
|
443
|
+
failed: list[str] = []
|
|
444
|
+
for release in _releases(bundle):
|
|
445
|
+
report = check_release(release)
|
|
446
|
+
click.echo(report.summary(), err=not report.ok)
|
|
447
|
+
if not report.ok:
|
|
448
|
+
failed.append(release.name)
|
|
449
|
+
if failed:
|
|
450
|
+
raise click.ClickException(f"the schema version gate failed for {', '.join(failed)}")
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@schema.command("archive")
|
|
454
|
+
@click.option("--bundle", "bundle", required=True, help="katalyst-core or a bundle's name.")
|
|
455
|
+
def schema_archive(bundle: str) -> None:
|
|
456
|
+
"""Archive the current modules as the released version (do this when you release it)."""
|
|
457
|
+
from katalyst_engine.evolution import SchemaVersionMismatchError, check_release
|
|
458
|
+
|
|
459
|
+
(release,) = _releases(bundle)
|
|
460
|
+
report = check_release(release)
|
|
461
|
+
if not report.ok:
|
|
462
|
+
click.echo(report.summary(), err=True)
|
|
463
|
+
raise click.ClickException(f"{bundle} does not pass the gate; fix it before archiving")
|
|
464
|
+
try:
|
|
465
|
+
target = release.archive()
|
|
466
|
+
except (FileExistsError, SchemaVersionMismatchError) as exc:
|
|
467
|
+
raise click.ClickException(str(exc)) from exc
|
|
468
|
+
click.echo(f"Archived {release.name} {release.version} in {target}")
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""kata tax create — Create a new taxonomy node."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from katalyst_taxonomy_cli.context import CliContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command("create")
|
|
11
|
+
@click.argument("node_type")
|
|
12
|
+
@click.argument("name")
|
|
13
|
+
@click.option("-d", "--description", default=None, help="Node description.")
|
|
14
|
+
@click.option("-O", "--owner", "owners", multiple=True, help="Owner email(s).")
|
|
15
|
+
@click.option(
|
|
16
|
+
"--under",
|
|
17
|
+
"parent",
|
|
18
|
+
default=None,
|
|
19
|
+
help="The parent's qualified name (shop.backend), or backend:fqtn.",
|
|
20
|
+
)
|
|
21
|
+
@click.option("--parent", "parent_alt", default=None, help="Alias for --under.")
|
|
22
|
+
@click.option(
|
|
23
|
+
"--scaffold-variant",
|
|
24
|
+
"scaffold_variant",
|
|
25
|
+
default=None,
|
|
26
|
+
help="Scaffold variant to scaffold the node's directory from; any scaffoldable kind.",
|
|
27
|
+
)
|
|
28
|
+
@click.option(
|
|
29
|
+
"--set",
|
|
30
|
+
"set_fields",
|
|
31
|
+
multiple=True,
|
|
32
|
+
metavar="KEY=VALUE",
|
|
33
|
+
help=(
|
|
34
|
+
"Set a field the kind declares, typed by its declaration. Repeat a key for a "
|
|
35
|
+
"list; a dotted key (scope.regions=x) sets a key of a map field; a JSON "
|
|
36
|
+
"value sets a list or map whole."
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
@click.option(
|
|
40
|
+
"--from-file",
|
|
41
|
+
"from_file",
|
|
42
|
+
default=None,
|
|
43
|
+
metavar="PATH",
|
|
44
|
+
help="Spec fields from a YAML or JSON file (- for stdin); --set applies over them.",
|
|
45
|
+
)
|
|
46
|
+
@click.option("--target-dir", default=None, help="Override output directory.")
|
|
47
|
+
@click.option(
|
|
48
|
+
"--backend",
|
|
49
|
+
"backend_target",
|
|
50
|
+
default=None,
|
|
51
|
+
help=(
|
|
52
|
+
"Backend to create the node in (or write NAME as backend:name). Defaults to the "
|
|
53
|
+
"filesystem backend the current directory is inside; from any other directory "
|
|
54
|
+
"it must be named."
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
@click.option(
|
|
58
|
+
"--dry-run",
|
|
59
|
+
is_flag=True,
|
|
60
|
+
default=False,
|
|
61
|
+
help=(
|
|
62
|
+
"Check it and show what it would write — its document and every file its "
|
|
63
|
+
"scaffold and create hooks add or change — without writing anything."
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
@click.pass_obj
|
|
67
|
+
def create(
|
|
68
|
+
ctx: CliContext,
|
|
69
|
+
node_type: str,
|
|
70
|
+
name: str,
|
|
71
|
+
description: str | None,
|
|
72
|
+
owners: tuple[str, ...],
|
|
73
|
+
parent: str | None,
|
|
74
|
+
parent_alt: str | None,
|
|
75
|
+
scaffold_variant: str | None,
|
|
76
|
+
set_fields: tuple[str, ...],
|
|
77
|
+
from_file: str | None,
|
|
78
|
+
target_dir: str | None,
|
|
79
|
+
backend_target: str | None,
|
|
80
|
+
dry_run: bool,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Create a NODE_TYPE node called NAME, in the backend --backend names.
|
|
83
|
+
|
|
84
|
+
Every backend takes the same path: the node is checked against its kind and written
|
|
85
|
+
through the backend; a backend that keeps documents as files (a repository) writes
|
|
86
|
+
it as katalyst.taxonomy/v2alpha at the kind's storage path (or --target-dir), and
|
|
87
|
+
--scaffold-variant scaffolds the node's directory from an
|
|
88
|
+
installed template (see kata tax add) — a variant not installed is refused, with the
|
|
89
|
+
command that installs it. --under/--parent names the parent by its qualified name
|
|
90
|
+
(a layer under stack backend of system shop: --under shop.backend); a bare name of
|
|
91
|
+
a nested parent is refused with the qualified names it may mean. --dry-run checks
|
|
92
|
+
the same and writes nothing: it prints the document and the files it would write.
|
|
93
|
+
|
|
94
|
+
--set KEY=VALUE and --from-file give the node's fields, typed and checked as
|
|
95
|
+
kata tax update checks them: a field the kind declares (or description / owners),
|
|
96
|
+
a value of its type (boolean, integer, list, map) and allowed values, a reference
|
|
97
|
+
naming a node of the referenced kind.
|
|
98
|
+
|
|
99
|
+
Examples:
|
|
100
|
+
|
|
101
|
+
kata tax create stage verify --set halt_on_failure=false --set concern_order=maintainability
|
|
102
|
+
|
|
103
|
+
kata tax create operation probe --set order=10 --set scope.regions=eu-west
|
|
104
|
+
|
|
105
|
+
kata tax create venue ci --set 'triggers=[{"on":"push","environments":["dev"]}]'
|
|
106
|
+
|
|
107
|
+
kata tax create environment qa --set template_replacements.log_level=debug
|
|
108
|
+
"""
|
|
109
|
+
from katalyst_taxonomy.adapters.orchestration import run_create
|
|
110
|
+
from katalyst_taxonomy.config.routing import split_backend_prefix
|
|
111
|
+
|
|
112
|
+
from katalyst_taxonomy_cli.fields import parse_set, read_spec_file
|
|
113
|
+
|
|
114
|
+
from .._wiring import resolve_backend
|
|
115
|
+
|
|
116
|
+
resolved_parent = parent or parent_alt
|
|
117
|
+
|
|
118
|
+
# The backend is `--backend`, or the `backend:` prefix of NAME. Without either, the
|
|
119
|
+
# repository is the one the current directory is inside — even with one repository,
|
|
120
|
+
# a directory none holds is an error naming the candidates, not a write elsewhere.
|
|
121
|
+
prefix, name = split_backend_prefix(name)
|
|
122
|
+
if prefix is not None and backend_target is not None and prefix != backend_target:
|
|
123
|
+
raise click.ClickException(
|
|
124
|
+
f"{prefix}:{name} names backend {prefix!r} but --backend is {backend_target!r}."
|
|
125
|
+
)
|
|
126
|
+
chosen = prefix or backend_target or ctx.repository(None, from_cwd=True).name
|
|
127
|
+
resolved = resolve_backend(ctx, chosen)
|
|
128
|
+
|
|
129
|
+
exit_code = run_create(
|
|
130
|
+
ctx.cwd,
|
|
131
|
+
node_type,
|
|
132
|
+
name,
|
|
133
|
+
backend_name=resolved.name,
|
|
134
|
+
description=description,
|
|
135
|
+
owners=owners or (),
|
|
136
|
+
fields=parse_set(set_fields),
|
|
137
|
+
spec=read_spec_file(from_file),
|
|
138
|
+
parent=resolved_parent,
|
|
139
|
+
scaffold_variant=scaffold_variant,
|
|
140
|
+
target_dir=target_dir,
|
|
141
|
+
profile=ctx.profile,
|
|
142
|
+
dry_run=dry_run,
|
|
143
|
+
)
|
|
144
|
+
if exit_code != 0:
|
|
145
|
+
raise SystemExit(exit_code)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""kata tax deps — Show dependency graph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from katalyst_taxonomy_cli.context import CliContext
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command("deps")
|
|
13
|
+
@click.option(
|
|
14
|
+
"--format",
|
|
15
|
+
"fmt",
|
|
16
|
+
type=click.Choice(["tree", "list", "json", "mermaid"]),
|
|
17
|
+
default=None,
|
|
18
|
+
help="Output format.",
|
|
19
|
+
)
|
|
20
|
+
@click.option("--node", default=None, help="Filter to specific node.")
|
|
21
|
+
@click.option("--reverse", is_flag=True, help="Show reverse dependencies (dependents).")
|
|
22
|
+
@click.pass_obj
|
|
23
|
+
def deps(ctx: CliContext, fmt: str | None, node: str | None, reverse: bool) -> None:
|
|
24
|
+
"""Show dependency graph."""
|
|
25
|
+
from katalyst_taxonomy.services.deps import build_dependency_graph
|
|
26
|
+
from katalyst_taxonomy.services.deps_formatting import (
|
|
27
|
+
output_json,
|
|
28
|
+
output_list,
|
|
29
|
+
output_mermaid,
|
|
30
|
+
output_tree,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
fmt = fmt or ctx.output_format or "tree"
|
|
34
|
+
service = ctx.taxonomy_service
|
|
35
|
+
graph = build_dependency_graph(service)
|
|
36
|
+
|
|
37
|
+
buf = io.StringIO()
|
|
38
|
+
|
|
39
|
+
if fmt == "json":
|
|
40
|
+
output_json(graph, service, buf)
|
|
41
|
+
elif fmt == "list":
|
|
42
|
+
output_list(graph, service, buf)
|
|
43
|
+
elif fmt == "mermaid":
|
|
44
|
+
output_mermaid(graph, service, node, buf)
|
|
45
|
+
else:
|
|
46
|
+
output_tree(graph, service, node, reverse, buf)
|
|
47
|
+
|
|
48
|
+
ctx.output.echo(buf.getvalue().rstrip())
|