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.
Files changed (51) hide show
  1. katalyst_taxonomy_cli/__init__.py +14 -0
  2. katalyst_taxonomy_cli/__main__.py +12 -0
  3. katalyst_taxonomy_cli/_wiring.py +248 -0
  4. katalyst_taxonomy_cli/app.py +550 -0
  5. katalyst_taxonomy_cli/commands/__init__.py +1 -0
  6. katalyst_taxonomy_cli/commands/api.py +104 -0
  7. katalyst_taxonomy_cli/commands/backend.py +96 -0
  8. katalyst_taxonomy_cli/commands/bundle.py +106 -0
  9. katalyst_taxonomy_cli/commands/clone.py +68 -0
  10. katalyst_taxonomy_cli/commands/config.py +748 -0
  11. katalyst_taxonomy_cli/commands/config_filters.py +159 -0
  12. katalyst_taxonomy_cli/commands/config_home.py +179 -0
  13. katalyst_taxonomy_cli/commands/config_plugins.py +180 -0
  14. katalyst_taxonomy_cli/commands/config_profile.py +604 -0
  15. katalyst_taxonomy_cli/commands/config_schemas.py +257 -0
  16. katalyst_taxonomy_cli/commands/config_sources.py +141 -0
  17. katalyst_taxonomy_cli/commands/core.py +468 -0
  18. katalyst_taxonomy_cli/commands/create.py +145 -0
  19. katalyst_taxonomy_cli/commands/deps.py +48 -0
  20. katalyst_taxonomy_cli/commands/diff.py +63 -0
  21. katalyst_taxonomy_cli/commands/drafts.py +63 -0
  22. katalyst_taxonomy_cli/commands/export.py +186 -0
  23. katalyst_taxonomy_cli/commands/init.py +84 -0
  24. katalyst_taxonomy_cli/commands/kinds.py +302 -0
  25. katalyst_taxonomy_cli/commands/lifecycle.py +127 -0
  26. katalyst_taxonomy_cli/commands/lint.py +256 -0
  27. katalyst_taxonomy_cli/commands/plugin.py +148 -0
  28. katalyst_taxonomy_cli/commands/promote.py +95 -0
  29. katalyst_taxonomy_cli/commands/query.py +187 -0
  30. katalyst_taxonomy_cli/commands/relate.py +137 -0
  31. katalyst_taxonomy_cli/commands/relocate.py +129 -0
  32. katalyst_taxonomy_cli/commands/remove.py +95 -0
  33. katalyst_taxonomy_cli/commands/resolve.py +48 -0
  34. katalyst_taxonomy_cli/commands/rollback.py +51 -0
  35. katalyst_taxonomy_cli/commands/self_update.py +530 -0
  36. katalyst_taxonomy_cli/commands/show.py +33 -0
  37. katalyst_taxonomy_cli/commands/status.py +107 -0
  38. katalyst_taxonomy_cli/commands/sync.py +60 -0
  39. katalyst_taxonomy_cli/commands/update.py +112 -0
  40. katalyst_taxonomy_cli/commands/versions.py +54 -0
  41. katalyst_taxonomy_cli/context.py +145 -0
  42. katalyst_taxonomy_cli/discovery.py +157 -0
  43. katalyst_taxonomy_cli/fields.py +70 -0
  44. katalyst_taxonomy_cli/install.py +330 -0
  45. katalyst_taxonomy_cli/options.py +20 -0
  46. katalyst_taxonomy_cli/output.py +92 -0
  47. katalyst_taxonomy_cli/py.typed +0 -0
  48. katalyst_taxonomy_cli-2.0.1a1.dist-info/METADATA +12 -0
  49. katalyst_taxonomy_cli-2.0.1a1.dist-info/RECORD +51 -0
  50. katalyst_taxonomy_cli-2.0.1a1.dist-info/WHEEL +4 -0
  51. 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())