katalyst-taxonomy-cli 2.0.1a2__tar.gz → 2.0.1a3__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.
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/PKG-INFO +8 -1
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/pyproject.toml +13 -1
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/_wiring.py +4 -2
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/app.py +55 -1
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/create.py +4 -4
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/init.py +1 -1
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/lint.py +45 -18
- katalyst_taxonomy_cli-2.0.1a3/src/katalyst_taxonomy_cli/commands/migrate.py +210 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/relate.py +31 -5
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/relocate.py +16 -5
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/status.py +45 -0
- katalyst_taxonomy_cli-2.0.1a3/tests/test_backend_option.py +135 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_config_schemas_command.py +3 -3
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_core_schema.py +6 -6
- katalyst_taxonomy_cli-2.0.1a3/tests/test_lint_profile_graph.py +155 -0
- katalyst_taxonomy_cli-2.0.1a3/tests/test_migrate.py +142 -0
- katalyst_taxonomy_cli-2.0.1a3/tests/test_optional_extras.py +69 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/.gitignore +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/__init__.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/__main__.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/__init__.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/api.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/backend.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/bundle.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/clone.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/config.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/config_filters.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/config_home.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/config_plugins.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/config_profile.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/config_schemas.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/config_sources.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/core.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/deps.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/diff.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/drafts.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/export.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/kinds.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/lifecycle.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/plugin.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/promote.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/query.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/remove.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/resolve.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/rollback.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/self_update.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/show.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/sync.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/update.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/commands/versions.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/context.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/discovery.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/fields.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/install.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/options.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/output.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/py.typed +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/conftest.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_add.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_add_bundle.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_api_command.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_cli_version.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_clone.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_command_signatures.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_config_command.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_config_explain.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_config_home.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_config_plugins_command.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_config_profiles.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_config_sources.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_create.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_create_typed_and_variants.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_deps.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_git_backend_cli.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_github_backend_cli.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_install.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_kinds.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_kinds_plugin_settings.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_lint_output.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_lint_strict.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_output.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_plugin_command.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_plugin_gating.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_relate_update.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_relocate.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_remove.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_repository_targets.py +0 -0
- {katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/tests/test_self_update.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: katalyst-taxonomy-cli
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1a3
|
|
4
4
|
Summary: CLI for katalyst-taxonomy — the kata command
|
|
5
5
|
Author: eSimplicity
|
|
6
6
|
License: Proprietary
|
|
@@ -8,5 +8,12 @@ Requires-Python: >=3.12
|
|
|
8
8
|
Requires-Dist: click>=8.1
|
|
9
9
|
Requires-Dist: katalyst-core<1,>=0.1
|
|
10
10
|
Requires-Dist: katalyst-engine<6,>=5
|
|
11
|
+
Requires-Dist: katalyst-plugin-context<1,>=0.2
|
|
12
|
+
Requires-Dist: katalyst-plugin-harness<1,>=0.1
|
|
13
|
+
Requires-Dist: katalyst-plugin-scaffolding<2,>=1
|
|
11
14
|
Requires-Dist: katalyst-taxonomy-mcp<5,>=4
|
|
12
15
|
Requires-Dist: katalyst-taxonomy<6,>=5
|
|
16
|
+
Provides-Extra: bard
|
|
17
|
+
Requires-Dist: katalyst-plugin-bard<1,>=0.1; extra == 'bard'
|
|
18
|
+
Provides-Extra: tui
|
|
19
|
+
Requires-Dist: katalyst-taxonomy-tui<3,>=2; extra == 'tui'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "katalyst-taxonomy-cli"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.1a3"
|
|
4
4
|
description = "CLI for katalyst-taxonomy — the kata command"
|
|
5
5
|
license = { text = "Proprietary" }
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -15,8 +15,20 @@ dependencies = [
|
|
|
15
15
|
"katalyst-engine>=5,<6",
|
|
16
16
|
"click>=8.1",
|
|
17
17
|
"katalyst-core>=0.1,<1",
|
|
18
|
+
# The plugins that ship with kata: `kata tax context|harness|add`. Bounded to the
|
|
19
|
+
# config-rooted runtime, like the plugins' own floors on this package (a cycle pip
|
|
20
|
+
# and uv both resolve: a plugin's command imports this package's context and options).
|
|
21
|
+
"katalyst-plugin-context>=0.2,<1",
|
|
22
|
+
"katalyst-plugin-harness>=0.1,<1",
|
|
23
|
+
"katalyst-plugin-scaffolding>=1,<2",
|
|
18
24
|
]
|
|
19
25
|
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
# `kata tax bard`: the web UI plugin.
|
|
28
|
+
bard = ["katalyst-plugin-bard>=0.1,<1"]
|
|
29
|
+
# `kata tui`: the terminal UI.
|
|
30
|
+
tui = ["katalyst-taxonomy-tui>=2,<3"]
|
|
31
|
+
|
|
20
32
|
[project.scripts]
|
|
21
33
|
kata = "katalyst_taxonomy_cli.__main__:main"
|
|
22
34
|
|
{katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/_wiring.py
RENAMED
|
@@ -123,7 +123,9 @@ def _named(registry: BackendRegistry, target: str | None, address: str) -> tuple
|
|
|
123
123
|
A prefix and ``--backend`` that disagree is an error rather than one silently
|
|
124
124
|
winning: the command was told two different stores.
|
|
125
125
|
"""
|
|
126
|
-
|
|
126
|
+
from katalyst_taxonomy.config.routing import split_backend_prefix
|
|
127
|
+
|
|
128
|
+
if address and (target is None or split_backend_prefix(address)[0] is not None):
|
|
127
129
|
parsed = registry.address(address)
|
|
128
130
|
if target and target != parsed.backend:
|
|
129
131
|
raise click.ClickException(
|
|
@@ -135,7 +137,7 @@ def _named(registry: BackendRegistry, target: str | None, address: str) -> tuple
|
|
|
135
137
|
from katalyst_taxonomy.config.routing import UnknownBackendError
|
|
136
138
|
|
|
137
139
|
raise UnknownBackendError(target, registry.profile_backends())
|
|
138
|
-
return target,
|
|
140
|
+
return target, address
|
|
139
141
|
return None, ""
|
|
140
142
|
|
|
141
143
|
|
{katalyst_taxonomy_cli-2.0.1a2 → katalyst_taxonomy_cli-2.0.1a3}/src/katalyst_taxonomy_cli/app.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import importlib.util
|
|
5
6
|
import subprocess
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
@@ -77,6 +78,42 @@ class KataGroup(click.Group):
|
|
|
77
78
|
raise click.ClickException(str(exc)) from exc
|
|
78
79
|
|
|
79
80
|
|
|
81
|
+
def _missing_extra(name: str) -> str | None:
|
|
82
|
+
"""The extra of this package named *name* whose requirements are not installed.
|
|
83
|
+
|
|
84
|
+
A command group a plugin contributes is offered as an extra of the same name
|
|
85
|
+
(``katalyst-taxonomy-cli[<name>]``). Read from this distribution's own metadata, so
|
|
86
|
+
the CLI names no plugin: ``None`` when there is no such extra, or it is installed.
|
|
87
|
+
"""
|
|
88
|
+
import importlib.metadata as metadata
|
|
89
|
+
import re
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
requirements = metadata.requires("katalyst-taxonomy-cli") or []
|
|
93
|
+
except metadata.PackageNotFoundError:
|
|
94
|
+
return None
|
|
95
|
+
marker = re.compile(rf"""extra\s*==\s*['"]{re.escape(name)}['"]""")
|
|
96
|
+
wanted = [r for r in requirements if marker.search(r)]
|
|
97
|
+
if not wanted:
|
|
98
|
+
return None
|
|
99
|
+
for requirement in wanted:
|
|
100
|
+
distribution = re.split(r"[\s;<>=!~\[(]", requirement, maxsplit=1)[0]
|
|
101
|
+
try:
|
|
102
|
+
metadata.distribution(distribution)
|
|
103
|
+
except metadata.PackageNotFoundError:
|
|
104
|
+
return name
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _not_installed_message(extra: str, label: str) -> str:
|
|
109
|
+
return (
|
|
110
|
+
f"{label} is not installed. Install it with:\n"
|
|
111
|
+
f" pip install 'katalyst-taxonomy-cli[{extra}]'\n"
|
|
112
|
+
f"or, for a uv tool install:\n"
|
|
113
|
+
f" uv tool install 'katalyst-taxonomy-cli[{extra}]'"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
80
117
|
class TaxGroup(click.Group):
|
|
81
118
|
"""The ``tax`` group, which resolves plugin-contributed commands **lazily**.
|
|
82
119
|
|
|
@@ -114,6 +151,16 @@ class TaxGroup(click.Group):
|
|
|
114
151
|
self._ensure_plugin_commands(ctx)
|
|
115
152
|
return super().get_command(ctx, cmd_name)
|
|
116
153
|
|
|
154
|
+
def resolve_command(
|
|
155
|
+
self, ctx: click.Context, args: list[str]
|
|
156
|
+
) -> tuple[str | None, click.Command | None, list[str]]:
|
|
157
|
+
name = args[0] if args else None
|
|
158
|
+
if name and self.get_command(ctx, name) is None:
|
|
159
|
+
extra = _missing_extra(name)
|
|
160
|
+
if extra is not None:
|
|
161
|
+
raise click.ClickException(_not_installed_message(extra, f"`kata tax {name}`"))
|
|
162
|
+
return super().resolve_command(ctx, args)
|
|
163
|
+
|
|
117
164
|
def _ensure_plugin_commands(self, ctx: click.Context) -> None:
|
|
118
165
|
if self._plugin_commands_resolved:
|
|
119
166
|
return
|
|
@@ -295,7 +342,12 @@ def _configure_logging() -> None:
|
|
|
295
342
|
|
|
296
343
|
@cli.command("tui")
|
|
297
344
|
def tui_cmd() -> None:
|
|
298
|
-
"""Launch the interactive terminal UI.
|
|
345
|
+
"""Launch the interactive terminal UI.
|
|
346
|
+
|
|
347
|
+
The UI is an extra: ``pip install 'katalyst-taxonomy-cli[tui]'``.
|
|
348
|
+
"""
|
|
349
|
+
if importlib.util.find_spec("katalyst_taxonomy_tui") is None:
|
|
350
|
+
raise click.ClickException(_not_installed_message("tui", "the terminal UI"))
|
|
299
351
|
try:
|
|
300
352
|
subprocess.run(
|
|
301
353
|
[sys.executable, "-m", "katalyst_taxonomy_tui"],
|
|
@@ -466,6 +518,7 @@ def _register_core_commands() -> None:
|
|
|
466
518
|
kinds,
|
|
467
519
|
lifecycle,
|
|
468
520
|
lint,
|
|
521
|
+
migrate,
|
|
469
522
|
plugin,
|
|
470
523
|
promote,
|
|
471
524
|
query,
|
|
@@ -505,6 +558,7 @@ def _register_core_commands() -> None:
|
|
|
505
558
|
tax.add_command(show.show)
|
|
506
559
|
tax.add_command(kinds.kinds)
|
|
507
560
|
tax.add_command(lint.lint)
|
|
561
|
+
tax.add_command(migrate.migrate)
|
|
508
562
|
tax.add_command(deps.deps)
|
|
509
563
|
tax.add_command(status.status)
|
|
510
564
|
tax.add_command(rollback.rollback)
|
|
@@ -98,13 +98,13 @@ def create(
|
|
|
98
98
|
|
|
99
99
|
Examples:
|
|
100
100
|
|
|
101
|
-
kata tax create
|
|
101
|
+
kata tax create layer api --under shop.backend --set audited=false --set tier=gold
|
|
102
102
|
|
|
103
|
-
kata tax create
|
|
103
|
+
kata tax create layer worker --under shop.backend --set priority=10 --set limits.region=eu-west
|
|
104
104
|
|
|
105
|
-
kata tax create
|
|
105
|
+
kata tax create capability checkout --under shop --set 'hooks=[{"on":"push","branches":["main"]}]'
|
|
106
106
|
|
|
107
|
-
kata tax create
|
|
107
|
+
kata tax create stack web --under shop --set limits.log_level=debug
|
|
108
108
|
"""
|
|
109
109
|
from katalyst_taxonomy.adapters.orchestration import run_create
|
|
110
110
|
from katalyst_taxonomy.config.routing import split_backend_prefix
|
|
@@ -66,7 +66,7 @@ def init(
|
|
|
66
66
|
|
|
67
67
|
Writes the taxonomy root (.global/taxonomy/) and version.yaml — together the
|
|
68
68
|
"initialized" marker — and registers the repository as a filesystem backend of a
|
|
69
|
-
profile in the
|
|
69
|
+
profile in the active home's config (`kata config path` prints it): the --profile
|
|
70
70
|
profile (created when it is not declared), else the active one; a file with no
|
|
71
71
|
profiles gets a new `local`, made active. The backend is named --backend, else for
|
|
72
72
|
the directory; a name the profile already declares for another directory is refused
|
|
@@ -16,21 +16,39 @@ from katalyst_taxonomy_cli.context import CliContext
|
|
|
16
16
|
is_flag=True,
|
|
17
17
|
help=(
|
|
18
18
|
"Treat advisory findings (governance id collisions, duplicate names, undeclared "
|
|
19
|
-
"spec keys, misplaced documents) as errors."
|
|
19
|
+
"spec keys, misplaced documents, shape findings and shape warnings) as errors."
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
@click.option(
|
|
23
|
+
"--backend",
|
|
24
|
+
default=None,
|
|
25
|
+
help=(
|
|
26
|
+
"Report only this backend's findings. Its documents are still checked against "
|
|
27
|
+
"the graph of every backend the profile reads."
|
|
20
28
|
),
|
|
21
29
|
)
|
|
22
30
|
@click.pass_obj
|
|
23
|
-
def lint(ctx: CliContext, verbose: bool, strict: bool) -> None:
|
|
31
|
+
def lint(ctx: CliContext, verbose: bool, strict: bool, backend: str | None) -> None:
|
|
24
32
|
"""Validate taxonomy against schema — everything the active profile reads.
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
Every check runs over one graph: the documents of every backend the profile reads,
|
|
35
|
+
so a reference into another backend (other:shop.storefront.checkout) is checked
|
|
36
|
+
against the node it names. Each finding is labelled [backend] when the profile
|
|
37
|
+
reads more than one. Repository-local checks (plugin rules, plugin-document schemas,
|
|
38
|
+
version constraints) run once per filesystem backend. A backend that cannot be read
|
|
39
|
+
is an error naming it.
|
|
29
40
|
"""
|
|
30
41
|
from katalyst_taxonomy.adapters.orchestration import run_lint
|
|
42
|
+
from katalyst_taxonomy.config.routing import BackendUnavailableError
|
|
31
43
|
|
|
32
44
|
try:
|
|
33
|
-
result = run_lint(profile=ctx.profile)
|
|
45
|
+
result = run_lint(profile=ctx.profile, backend=backend)
|
|
46
|
+
except BackendUnavailableError as exc:
|
|
47
|
+
raise click.ClickException(
|
|
48
|
+
f"{exc}\nLint checks the graph of every backend the profile reads, so each "
|
|
49
|
+
"must be readable: make it reachable, or lint with a profile that does not "
|
|
50
|
+
"read it."
|
|
51
|
+
) from exc
|
|
34
52
|
except ValueError as exc:
|
|
35
53
|
# A `filters` block that cannot be honoured — `include: []`, a clause
|
|
36
54
|
# declaring `match.backend` — is a user error (#217), and lint does not go
|
|
@@ -39,6 +57,17 @@ def lint(ctx: CliContext, verbose: bool, strict: bool) -> None:
|
|
|
39
57
|
# it still reaches the root group and still exits 7.
|
|
40
58
|
raise click.ClickException(str(exc)) from exc
|
|
41
59
|
|
|
60
|
+
labelled = len(result.graph_backends) > 1
|
|
61
|
+
|
|
62
|
+
def _where(path: object) -> str:
|
|
63
|
+
"""``[backend] path: `` for a finding about a file, as much as is known."""
|
|
64
|
+
label = ""
|
|
65
|
+
if labelled and isinstance(path, Path):
|
|
66
|
+
owner = result.backend_of(path)
|
|
67
|
+
label = f"[{owner}] " if owner else ""
|
|
68
|
+
shown = f"{path}: " if path is not None and str(path) not in ("", ".") else ""
|
|
69
|
+
return f"{label}{shown}"
|
|
70
|
+
|
|
42
71
|
# Version constraint warnings are advisory (they do not make the taxonomy
|
|
43
72
|
# invalid) but must still be surfaced — previously they were unreachable
|
|
44
73
|
# when the taxonomy was otherwise valid, and only shown under --verbose.
|
|
@@ -99,21 +128,15 @@ def lint(ctx: CliContext, verbose: bool, strict: bool) -> None:
|
|
|
99
128
|
("Misplaced documents", result.misplaced_documents),
|
|
100
129
|
("Relationships no kind carries", result.uncarried_relationships),
|
|
101
130
|
("Shape findings", result.shape_findings),
|
|
131
|
+
("Shape warnings", result.shape_warnings),
|
|
102
132
|
):
|
|
103
133
|
if not found:
|
|
104
134
|
continue
|
|
105
135
|
ctx.output.echo(f"\n{title} ({len(found)}):")
|
|
106
136
|
for violation in found:
|
|
107
|
-
|
|
108
|
-
ctx.output.echo(f" {severity}: {where}{violation.message}")
|
|
137
|
+
ctx.output.echo(f" {severity}: {_where(violation.path)}{violation.message}")
|
|
109
138
|
if not strict:
|
|
110
139
|
ctx.output.echo(" Re-run with --strict to fail on these.")
|
|
111
|
-
# A shape's warnings (e.g. coverage) are a report, strict or not.
|
|
112
|
-
if result.shape_warnings:
|
|
113
|
-
ctx.output.echo(f"\nShape warnings ({len(result.shape_warnings)}):")
|
|
114
|
-
for violation in result.shape_warnings:
|
|
115
|
-
where = f"{violation.path}: " if str(violation.path) not in ("", ".") else ""
|
|
116
|
-
ctx.output.echo(f" WARNING: {where}{violation.message}")
|
|
117
140
|
|
|
118
141
|
def _print_history_notes() -> None:
|
|
119
142
|
found = result.removed_history_targets
|
|
@@ -121,8 +144,7 @@ def lint(ctx: CliContext, verbose: bool, strict: bool) -> None:
|
|
|
121
144
|
return
|
|
122
145
|
ctx.output.echo(f"\nHistory references to removed nodes ({len(found)}):")
|
|
123
146
|
for violation in found:
|
|
124
|
-
|
|
125
|
-
ctx.output.echo(f" INFO: {where}{violation.message}")
|
|
147
|
+
ctx.output.echo(f" INFO: {_where(violation.path)}{violation.message}")
|
|
126
148
|
|
|
127
149
|
strict_findings = (
|
|
128
150
|
len(result.governance_id_collisions)
|
|
@@ -131,6 +153,7 @@ def lint(ctx: CliContext, verbose: bool, strict: bool) -> None:
|
|
|
131
153
|
+ len(result.misplaced_documents)
|
|
132
154
|
+ len(result.uncarried_relationships)
|
|
133
155
|
+ len(result.shape_findings)
|
|
156
|
+
+ len(result.shape_warnings)
|
|
134
157
|
)
|
|
135
158
|
strict_failure = strict and strict_findings > 0
|
|
136
159
|
|
|
@@ -142,6 +165,10 @@ def lint(ctx: CliContext, verbose: bool, strict: bool) -> None:
|
|
|
142
165
|
for repo in result.repositories
|
|
143
166
|
)
|
|
144
167
|
ctx.output.echo(f" Repositories checked: {repositories or '(none)'}")
|
|
168
|
+
if result.focus is not None:
|
|
169
|
+
ctx.output.echo(
|
|
170
|
+
f" Findings for: {result.focus}; graph: {', '.join(result.graph_backends)}"
|
|
171
|
+
)
|
|
145
172
|
# Show the receipts: a gate that verified nothing must not look identical
|
|
146
173
|
# to one that verified everything.
|
|
147
174
|
ctx.output.echo(
|
|
@@ -179,14 +206,14 @@ def lint(ctx: CliContext, verbose: bool, strict: bool) -> None:
|
|
|
179
206
|
if result.document_errors:
|
|
180
207
|
ctx.output.echo(f"\nDocument errors ({len(result.document_errors)}):")
|
|
181
208
|
for err in result.document_errors:
|
|
182
|
-
ctx.output.echo(f" {err.path}
|
|
209
|
+
ctx.output.echo(f" {_where(err.path)}{err.message}")
|
|
183
210
|
|
|
184
211
|
# Positioned between document and plugin-schema errors so block order is
|
|
185
212
|
# fixed across runs and corpora.
|
|
186
213
|
if result.reference_errors:
|
|
187
214
|
ctx.output.echo(f"\nReference errors ({len(result.reference_errors)}):")
|
|
188
215
|
for ref_err in result.reference_errors:
|
|
189
|
-
ctx.output.echo(f" {ref_err.path}
|
|
216
|
+
ctx.output.echo(f" {_where(ref_err.path)}{ref_err.message}")
|
|
190
217
|
|
|
191
218
|
if result.plugin_schema_errors:
|
|
192
219
|
ctx.output.echo(f"\nSchema errors ({len(result.plugin_schema_errors)}):")
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""kata tax migrate — move a repository's documents to the loaded schema versions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from katalyst_taxonomy_cli.context import CliContext
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from katalyst_taxonomy.services.schema_migration import MigrationReport
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command("migrate")
|
|
16
|
+
@click.option(
|
|
17
|
+
"--backend",
|
|
18
|
+
"backend",
|
|
19
|
+
default=None,
|
|
20
|
+
help=(
|
|
21
|
+
"The repository (a filesystem backend of the active profile) to migrate. "
|
|
22
|
+
"Default: every repository the profile reads."
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
@click.option("--dry-run", is_flag=True, help="Show what would change, as a diff; write nothing.")
|
|
26
|
+
@click.option(
|
|
27
|
+
"--from",
|
|
28
|
+
"starts",
|
|
29
|
+
multiple=True,
|
|
30
|
+
metavar="SCHEMA=VERSION",
|
|
31
|
+
help=(
|
|
32
|
+
"The version a schema's documents are at, instead of what the repository "
|
|
33
|
+
"records (or, when it records nothing, the schema's first release). Repeatable."
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
@click.option(
|
|
37
|
+
"--allow-dirty",
|
|
38
|
+
is_flag=True,
|
|
39
|
+
help="Migrate even when files the migration writes have uncommitted changes.",
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"--allow-lint-errors",
|
|
43
|
+
is_flag=True,
|
|
44
|
+
help="Write even when the migrated documents do not lint clean (strict).",
|
|
45
|
+
)
|
|
46
|
+
@click.option("--json", "as_json", is_flag=True, help="Print the report as JSON.")
|
|
47
|
+
@click.pass_obj
|
|
48
|
+
def migrate(
|
|
49
|
+
ctx: CliContext,
|
|
50
|
+
backend: str | None,
|
|
51
|
+
dry_run: bool,
|
|
52
|
+
starts: tuple[str, ...],
|
|
53
|
+
allow_dirty: bool,
|
|
54
|
+
allow_lint_errors: bool,
|
|
55
|
+
as_json: bool,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Move a repository's documents to the schema versions this kata loads.
|
|
58
|
+
|
|
59
|
+
A repository records the version of each schema its documents were written for
|
|
60
|
+
(.global/taxonomy/schema.lock). When a schema's major is behind the loaded one, the
|
|
61
|
+
repository is read-only until it is migrated: this runs the schema's migrations,
|
|
62
|
+
each major in turn, then lints the result strictly and records the new versions.
|
|
63
|
+
|
|
64
|
+
Nothing is committed: review the change with `git diff`, then commit it. A
|
|
65
|
+
repository already at the loaded majors is not changed (its lock is written when it
|
|
66
|
+
has none, or records an older minor), so running it twice is safe.
|
|
67
|
+
"""
|
|
68
|
+
from katalyst_engine.evolution.chain import MigrationChainError
|
|
69
|
+
from katalyst_engine.evolution.lock import SchemaLockError
|
|
70
|
+
from katalyst_engine.evolution.migration import MigrationFileError, MigrationStepError
|
|
71
|
+
from katalyst_taxonomy.adapters.filesystem.schema_migration import MigrationWriteError
|
|
72
|
+
from katalyst_taxonomy.adapters.permission_gate import unwrap_backend
|
|
73
|
+
from katalyst_taxonomy.adapters.schema_migrate import MigrationRefused, migrate_repository
|
|
74
|
+
from katalyst_taxonomy.services.schema_migration import parse_starts
|
|
75
|
+
|
|
76
|
+
from katalyst_taxonomy_cli._wiring import load_registry
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
start_versions = parse_starts(starts)
|
|
80
|
+
except ValueError as exc:
|
|
81
|
+
raise click.BadParameter(str(exc), param_hint="--from") from exc
|
|
82
|
+
|
|
83
|
+
registry = load_registry(ctx)
|
|
84
|
+
if backend is not None:
|
|
85
|
+
names = [backend]
|
|
86
|
+
if backend not in registry.profile_backends():
|
|
87
|
+
reads = ", ".join(registry.profile_backends()) or "none"
|
|
88
|
+
raise click.ClickException(
|
|
89
|
+
f"backend {backend!r} is not one the active profile reads ({reads})"
|
|
90
|
+
)
|
|
91
|
+
else:
|
|
92
|
+
names = list(registry.profile_backends())
|
|
93
|
+
repositories: list[tuple[str, Any]] = []
|
|
94
|
+
for name in names:
|
|
95
|
+
inner = unwrap_backend(registry.get(name))
|
|
96
|
+
if getattr(inner, "repository_root", None) is None or not callable(
|
|
97
|
+
getattr(inner, "read_schema_lock", None)
|
|
98
|
+
):
|
|
99
|
+
if backend is not None:
|
|
100
|
+
raise click.ClickException(
|
|
101
|
+
f"backend {name!r} is not a repository; `kata tax migrate` moves "
|
|
102
|
+
"repositories (filesystem backends) forward"
|
|
103
|
+
)
|
|
104
|
+
continue
|
|
105
|
+
repositories.append((name, inner))
|
|
106
|
+
if not repositories:
|
|
107
|
+
raise click.ClickException("the active profile reads no repository to migrate")
|
|
108
|
+
|
|
109
|
+
payloads: list[dict[str, object]] = []
|
|
110
|
+
failed = False
|
|
111
|
+
for name, inner in repositories:
|
|
112
|
+
root = inner.repository_root
|
|
113
|
+
try:
|
|
114
|
+
report = migrate_repository(
|
|
115
|
+
name,
|
|
116
|
+
root,
|
|
117
|
+
dry_run=dry_run,
|
|
118
|
+
allow_dirty=allow_dirty,
|
|
119
|
+
allow_lint_errors=allow_lint_errors,
|
|
120
|
+
starts=start_versions,
|
|
121
|
+
excluded_subtrees=tuple(getattr(inner, "excluded_subtrees", ())),
|
|
122
|
+
)
|
|
123
|
+
except (
|
|
124
|
+
MigrationRefused,
|
|
125
|
+
MigrationChainError,
|
|
126
|
+
MigrationFileError,
|
|
127
|
+
MigrationStepError,
|
|
128
|
+
MigrationWriteError,
|
|
129
|
+
SchemaLockError,
|
|
130
|
+
ValueError,
|
|
131
|
+
) as exc:
|
|
132
|
+
message = str(exc) if str(exc).startswith(f"{name}:") else f"{name}: {exc}"
|
|
133
|
+
payloads.append({"backend": name, "error": message})
|
|
134
|
+
if not as_json:
|
|
135
|
+
ctx.output.error(message)
|
|
136
|
+
failed = True
|
|
137
|
+
continue
|
|
138
|
+
payloads.append(report.to_dict())
|
|
139
|
+
if not as_json:
|
|
140
|
+
_print(ctx, report, dry_run=dry_run)
|
|
141
|
+
if as_json:
|
|
142
|
+
ctx.output.json_out(payloads[0] if len(payloads) == 1 else payloads)
|
|
143
|
+
if failed:
|
|
144
|
+
raise SystemExit(1)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _versions(report: MigrationReport) -> str:
|
|
148
|
+
moving = report.moving
|
|
149
|
+
if moving:
|
|
150
|
+
return ", ".join(f"{c.name} {c.start} → {c.target}" for c in moving)
|
|
151
|
+
return ", ".join(f"{c.name} {c.target}" for c in report.chains)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _print(ctx: CliContext, report: MigrationReport, *, dry_run: bool) -> None:
|
|
155
|
+
out = ctx.output
|
|
156
|
+
if report.rolled_back:
|
|
157
|
+
out.echo(
|
|
158
|
+
f"{report.backend}: an interrupted migration was rolled back "
|
|
159
|
+
f"({len(report.rolled_back)} file(s) put back)."
|
|
160
|
+
)
|
|
161
|
+
if report.nothing_to_do:
|
|
162
|
+
out.echo(f"{report.backend}: current ({_versions(report)}). Nothing to migrate.")
|
|
163
|
+
return
|
|
164
|
+
count = sum(len(c.migrations) for c in report.moving)
|
|
165
|
+
if report.moving:
|
|
166
|
+
taken = (
|
|
167
|
+
"" if report.status.recorded else " (no schema.lock: taken to be at the first release)"
|
|
168
|
+
)
|
|
169
|
+
out.echo(f"{report.backend}: {_versions(report)} ({count} migration(s)){taken}")
|
|
170
|
+
else:
|
|
171
|
+
out.echo(f"{report.backend}: current; recording its schema versions")
|
|
172
|
+
for chain in report.moving:
|
|
173
|
+
for migration in chain.migrations:
|
|
174
|
+
out.echo(f" {chain.name} {migration.from_}-to-{migration.to}")
|
|
175
|
+
for outcome in report.outcomes:
|
|
176
|
+
if outcome.migration is migration:
|
|
177
|
+
counts = f"{outcome.written} document(s)"
|
|
178
|
+
if outcome.removed:
|
|
179
|
+
counts += f", {outcome.removed} removed"
|
|
180
|
+
out.echo(f" {outcome.label:<52} {counts}")
|
|
181
|
+
actions: dict[str, int] = {}
|
|
182
|
+
for planned in report.files:
|
|
183
|
+
actions[planned.action] = actions.get(planned.action, 0) + 1
|
|
184
|
+
summary = ", ".join(f"{n} {a}" for a, n in sorted(actions.items()))
|
|
185
|
+
out.echo(f"Files ({len(report.files)}): {summary}")
|
|
186
|
+
for planned in report.files:
|
|
187
|
+
where = f"{planned.moved_from} → {planned.path}" if planned.moved_from else planned.path
|
|
188
|
+
out.echo(f" {planned.action:<7} {where}")
|
|
189
|
+
after = ", ".join(f"{k} {v}" for k, v in report.lock_after.as_strings().items())
|
|
190
|
+
out.echo(f"schema.lock: {after}")
|
|
191
|
+
if report.lint_clean:
|
|
192
|
+
out.echo(f"lint after: clean (strict, {report.lint_documents} document(s))")
|
|
193
|
+
else:
|
|
194
|
+
out.echo(f"lint after: {len(report.lint_problems)} problem(s) (strict) in files it writes")
|
|
195
|
+
for problem in report.lint_problems[:20]:
|
|
196
|
+
out.echo(f" {problem}")
|
|
197
|
+
if report.lint_other:
|
|
198
|
+
out.echo(
|
|
199
|
+
f"lint after, elsewhere: {len(report.lint_other)} problem(s) the migration "
|
|
200
|
+
"does not touch (not stopping it)"
|
|
201
|
+
)
|
|
202
|
+
for problem in report.lint_other[:20]:
|
|
203
|
+
out.echo(f" {problem}")
|
|
204
|
+
if report.dirty:
|
|
205
|
+
out.echo(f"uncommitted changes to: {', '.join(report.dirty)}")
|
|
206
|
+
if dry_run:
|
|
207
|
+
out.echo("Nothing written (dry run). The changes, as a diff:")
|
|
208
|
+
out.echo(report.diff.rstrip("\n"))
|
|
209
|
+
elif report.applied:
|
|
210
|
+
out.echo("Written. Review the change with `git diff`, then commit it.")
|
|
@@ -6,14 +6,31 @@ import click
|
|
|
6
6
|
|
|
7
7
|
from katalyst_taxonomy_cli.context import CliContext
|
|
8
8
|
|
|
9
|
+
_BACKEND = click.option(
|
|
10
|
+
"--backend",
|
|
11
|
+
"backend_target",
|
|
12
|
+
default=None,
|
|
13
|
+
help="The source's backend, when SOURCE_FQTN has no prefix.",
|
|
14
|
+
)
|
|
9
15
|
|
|
10
|
-
|
|
16
|
+
|
|
17
|
+
def _endpoints( # noqa: ANN202
|
|
18
|
+
ctx: CliContext, source_address: str, target_address: str, backend_target: str | None = None
|
|
19
|
+
):
|
|
11
20
|
"""The source and target ends; an unprefixed target is in the source's backend."""
|
|
12
|
-
from katalyst_taxonomy.config.routing import BackendAddressError
|
|
21
|
+
from katalyst_taxonomy.config.routing import BackendAddressError, split_backend_prefix
|
|
13
22
|
from katalyst_taxonomy.services.relationships import endpoints
|
|
14
23
|
|
|
15
24
|
from .._wiring import load_registry
|
|
16
25
|
|
|
26
|
+
prefix, bare = split_backend_prefix(source_address)
|
|
27
|
+
if backend_target is not None:
|
|
28
|
+
if prefix is not None and prefix != backend_target:
|
|
29
|
+
raise click.ClickException(
|
|
30
|
+
f"{source_address!r} names backend {prefix!r} but --backend is {backend_target!r}."
|
|
31
|
+
)
|
|
32
|
+
if prefix is None:
|
|
33
|
+
source_address = f"{backend_target}:{bare}"
|
|
17
34
|
try:
|
|
18
35
|
return endpoints(load_registry(ctx), source_address, target_address)
|
|
19
36
|
except BackendAddressError as exc:
|
|
@@ -51,6 +68,7 @@ def _service(): # noqa: ANN202
|
|
|
51
68
|
"--before", default=None, metavar="TARGET", help="In an ordered field, before TARGET."
|
|
52
69
|
)
|
|
53
70
|
@click.option("--after", default=None, metavar="TARGET", help="In an ordered field, after TARGET.")
|
|
71
|
+
@_BACKEND
|
|
54
72
|
@click.pass_obj
|
|
55
73
|
def relate(
|
|
56
74
|
ctx: CliContext,
|
|
@@ -61,6 +79,7 @@ def relate(
|
|
|
61
79
|
index: int | None,
|
|
62
80
|
before: str | None,
|
|
63
81
|
after: str | None,
|
|
82
|
+
backend_target: str | None,
|
|
64
83
|
) -> None:
|
|
65
84
|
"""Relate two nodes, in the source's backend.
|
|
66
85
|
|
|
@@ -102,7 +121,7 @@ def relate(
|
|
|
102
121
|
position = Position(index=index, before=before, after=after)
|
|
103
122
|
except ValueError as exc:
|
|
104
123
|
raise click.ClickException("give one of --index, --before and --after") from exc
|
|
105
|
-
source, target = _endpoints(ctx, source_fqtn, target_fqtn)
|
|
124
|
+
source, target = _endpoints(ctx, source_fqtn, target_fqtn, backend_target)
|
|
106
125
|
outcome = _service().relate(source, rel_type, target, properties=props, position=position)
|
|
107
126
|
if not outcome.success or outcome.relationship is None:
|
|
108
127
|
ctx.output.echo(f"Error: {outcome.error}")
|
|
@@ -118,15 +137,22 @@ def relate(
|
|
|
118
137
|
@click.argument("source_fqtn")
|
|
119
138
|
@click.argument("rel_type")
|
|
120
139
|
@click.argument("target_fqtn")
|
|
140
|
+
@_BACKEND
|
|
121
141
|
@click.pass_obj
|
|
122
|
-
def unrelate(
|
|
142
|
+
def unrelate(
|
|
143
|
+
ctx: CliContext,
|
|
144
|
+
source_fqtn: str,
|
|
145
|
+
rel_type: str,
|
|
146
|
+
target_fqtn: str,
|
|
147
|
+
backend_target: str | None,
|
|
148
|
+
) -> None:
|
|
123
149
|
"""Remove a relationship ``kata tax relate`` made, from the source's backend.
|
|
124
150
|
|
|
125
151
|
Examples:
|
|
126
152
|
|
|
127
153
|
kata tax unrelate local:payments.checkout DEPENDS_ON payments.billing
|
|
128
154
|
"""
|
|
129
|
-
source, target = _endpoints(ctx, source_fqtn, target_fqtn)
|
|
155
|
+
source, target = _endpoints(ctx, source_fqtn, target_fqtn, backend_target)
|
|
130
156
|
outcome = _service().unrelate(source, rel_type, target)
|
|
131
157
|
if not outcome.success or outcome.relationship is None:
|
|
132
158
|
ctx.output.echo(f"Error: {outcome.error}")
|