splent-cli 1.2.6__tar.gz → 1.2.8__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.
- {splent_cli-1.2.6/src/splent_cli.egg-info → splent_cli-1.2.8}/PKG-INFO +1 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/pyproject.toml +1 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_orphans.py +2 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_status.py +18 -7
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_deps.py +57 -24
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_env.py +13 -5
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_features.py +15 -7
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_pyproject.py +11 -4
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/coverage.py +5 -2
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_console.py +8 -3
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_dump.py +21 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_migrate.py +32 -12
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_reset.py +12 -3
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_restore.py +3 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_rollback.py +7 -2
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_seed.py +13 -5
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_status.py +13 -4
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_upgrade.py +9 -2
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/doctor.py +17 -13
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/env/env_set.py +15 -2
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/export_puml.py +234 -123
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_add.py +17 -7
- splent_cli-1.2.8/src/splent_cli/commands/feature/feature_attach.py +105 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_clone.py +55 -13
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_contract.py +24 -18
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_create.py +39 -9
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_delete.py +2 -2
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_detach.py +2 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_diff.py +231 -142
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_discard.py +2 -2
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_drift.py +4 -7
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_edit.py +66 -9
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_fork.py +3 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_hook_add.py +3 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_hook_remove.py +2 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_hooks.py +5 -2
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_list.py +5 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_order.py +40 -15
- splent_cli-1.2.8/src/splent_cli/commands/feature/feature_pip_install.py +96 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_release.py +54 -18
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_remove.py +6 -2
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_rename.py +4 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_status.py +25 -13
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_sync_template.py +3 -9
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_test.py +11 -4
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_upgrade.py +37 -8
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_versions.py +91 -29
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature_compile.py +47 -18
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/linter.py +10 -10
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/locust.py +10 -4
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_build.py +54 -15
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_clean.py +4 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_create.py +1 -0
- splent_cli-1.2.8/src/splent_cli/commands/product/product_derive.py +269 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_drift.py +2 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_release.py +45 -15
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_select.py +3 -3
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_status.py +28 -8
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_sync.py +20 -8
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_sync_template.py +2 -6
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_up.py +33 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/selenium.py +5 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/tokens.py +1 -3
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_check.py +10 -4
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_fetch.py +1 -2
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_fix.py +35 -10
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_sync.py +5 -3
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_utils.py +3 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/version.py +6 -4
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/services/compose.py +10 -2
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/services/context.py +11 -2
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/services/release.py +74 -25
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/command_loader.py +3 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/db_utils.py +4 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/feature_installer.py +3 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/feature_utils.py +3 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/lifecycle.py +23 -21
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/manifest.py +10 -3
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/template_drift.py +43 -51
- {splent_cli-1.2.6 → splent_cli-1.2.8/src/splent_cli.egg-info}/PKG-INFO +1 -1
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli.egg-info/SOURCES.txt +1 -0
- splent_cli-1.2.6/src/splent_cli/commands/feature/feature_attach.py +0 -165
- splent_cli-1.2.6/src/splent_cli/commands/product/product_derive.py +0 -136
- {splent_cli-1.2.6 → splent_cli-1.2.8}/LICENSE +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/README.md +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/setup.cfg +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/cli.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_versions.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/clear_cache.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/clear_log.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/clear_uploads.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/command_create.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_env.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_pull.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_search.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_deploy.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_down.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_env.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_port.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_run.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/release/release_core.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/route_list.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_configs.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_deps.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_features.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_info.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_missing.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_valid.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/services/__init__.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/__init__.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/cache_utils.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/dynamic_imports.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli.egg-info/requires.txt +0 -0
- {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: splent_cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.8
|
|
4
4
|
Summary: SPLENT-CLI is a CLI to be able to work on your development more easily.
|
|
5
5
|
Author-email: DiversoLab <diversolab@us.es>
|
|
6
6
|
Project-URL: Homepage, https://github.com/diverso-lab/splent_cli
|
|
@@ -40,6 +40,7 @@ def _get_cache_entries(cache_root: Path) -> list:
|
|
|
40
40
|
def _get_all_product_refs(workspace: Path) -> set:
|
|
41
41
|
"""Returns set of 'name' and 'name@version' (no namespace) from all products' pyproject.toml."""
|
|
42
42
|
import tomllib
|
|
43
|
+
|
|
43
44
|
refs = set()
|
|
44
45
|
for product_dir in sorted(workspace.iterdir()):
|
|
45
46
|
if not product_dir.is_dir() or product_dir.name.startswith("."):
|
|
@@ -57,7 +58,7 @@ def _get_all_product_refs(workspace: Path) -> set:
|
|
|
57
58
|
for entry in feats:
|
|
58
59
|
ref = entry.split("/", 1)[1] if "/" in entry else entry
|
|
59
60
|
refs.add(ref)
|
|
60
|
-
except
|
|
61
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
61
62
|
continue
|
|
62
63
|
return refs
|
|
63
64
|
|
|
@@ -37,10 +37,12 @@ def _get_workspace_root_features(workspace: Path) -> dict:
|
|
|
37
37
|
if not src.is_dir():
|
|
38
38
|
continue
|
|
39
39
|
for ns_dir in src.iterdir():
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
if (
|
|
41
|
+
ns_dir.is_dir()
|
|
42
|
+
and not ns_dir.name.startswith(("_", "."))
|
|
43
|
+
and "." not in ns_dir.name
|
|
44
|
+
and (ns_dir / entry.name).is_dir()
|
|
45
|
+
):
|
|
44
46
|
grouped[f"{ns_dir.name}/{entry.name}"].append("workspace")
|
|
45
47
|
break
|
|
46
48
|
return grouped
|
|
@@ -75,11 +77,20 @@ def cache_status():
|
|
|
75
77
|
for i, v in enumerate(sorted_versions):
|
|
76
78
|
connector = "└──" if i == len(sorted_versions) - 1 else "├──"
|
|
77
79
|
if v == "workspace":
|
|
78
|
-
click.echo(
|
|
80
|
+
click.echo(
|
|
81
|
+
f" {connector} "
|
|
82
|
+
+ click.style("editable (workspace root)", fg="magenta")
|
|
83
|
+
)
|
|
79
84
|
elif v is None:
|
|
80
|
-
click.echo(
|
|
85
|
+
click.echo(
|
|
86
|
+
f" {connector} " + click.style("editable (cache)", fg="blue")
|
|
87
|
+
)
|
|
81
88
|
else:
|
|
82
|
-
click.echo(
|
|
89
|
+
click.echo(
|
|
90
|
+
f" {connector} "
|
|
91
|
+
+ click.style(f"@{v}", fg="green")
|
|
92
|
+
+ click.style(" (pinned)", fg="bright_black")
|
|
93
|
+
)
|
|
83
94
|
click.echo()
|
|
84
95
|
|
|
85
96
|
|
|
@@ -16,7 +16,6 @@ import re
|
|
|
16
16
|
import click
|
|
17
17
|
|
|
18
18
|
from splent_cli.services import context
|
|
19
|
-
from splent_cli.utils.feature_utils import read_features_from_data
|
|
20
19
|
from splent_framework.utils.pyproject_reader import PyprojectReader
|
|
21
20
|
|
|
22
21
|
|
|
@@ -24,6 +23,7 @@ from splent_framework.utils.pyproject_reader import PyprojectReader
|
|
|
24
23
|
# UVL parser — extract dependency graph
|
|
25
24
|
# ---------------------------------------------------------------------------
|
|
26
25
|
|
|
26
|
+
|
|
27
27
|
def _parse_uvl_deps(uvl_path: str) -> tuple[dict[str, str], dict[str, set[str]]]:
|
|
28
28
|
"""Parse UVL file and return (package_map, allowed_deps).
|
|
29
29
|
|
|
@@ -82,7 +82,10 @@ def _parse_uvl_deps(uvl_path: str) -> tuple[dict[str, str], dict[str, set[str]]]
|
|
|
82
82
|
# Source code scanner — find actual imports of other features
|
|
83
83
|
# ---------------------------------------------------------------------------
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
|
|
86
|
+
def _scan_feature_imports(
|
|
87
|
+
feature_path: str, feature_name: str, all_packages: set[str]
|
|
88
|
+
) -> set[str]:
|
|
86
89
|
"""Scan all .py files in a feature and return set of other feature packages imported."""
|
|
87
90
|
imported: set[str] = set()
|
|
88
91
|
|
|
@@ -105,11 +108,13 @@ def _scan_feature_imports(feature_path: str, feature_name: str, all_packages: se
|
|
|
105
108
|
try:
|
|
106
109
|
with open(filepath, "r", encoding="utf-8") as fh:
|
|
107
110
|
content = fh.read()
|
|
108
|
-
except
|
|
111
|
+
except (OSError, PermissionError):
|
|
109
112
|
continue
|
|
110
113
|
|
|
111
114
|
# Find imports: from splent_io.splent_feature_X... or import splent_io.splent_feature_X
|
|
112
|
-
for match in re.findall(
|
|
115
|
+
for match in re.findall(
|
|
116
|
+
r"(?:from|import)\s+splent_io\.(splent_feature_\w+)", content
|
|
117
|
+
):
|
|
113
118
|
if match != feature_name and match in all_packages:
|
|
114
119
|
imported.add(match)
|
|
115
120
|
|
|
@@ -120,7 +125,10 @@ def _scan_feature_imports(feature_path: str, feature_name: str, all_packages: se
|
|
|
120
125
|
# Feature path resolver
|
|
121
126
|
# ---------------------------------------------------------------------------
|
|
122
127
|
|
|
123
|
-
|
|
128
|
+
|
|
129
|
+
def _resolve_feature_paths(
|
|
130
|
+
workspace: str, product: str, features: list[str]
|
|
131
|
+
) -> dict[str, str]:
|
|
124
132
|
"""Return {package_name: feature_path} for each declared feature."""
|
|
125
133
|
result = {}
|
|
126
134
|
features_dir = os.path.join(workspace, product, "features")
|
|
@@ -144,6 +152,7 @@ def _resolve_feature_paths(workspace: str, product: str, features: list[str]) ->
|
|
|
144
152
|
# Command
|
|
145
153
|
# ---------------------------------------------------------------------------
|
|
146
154
|
|
|
155
|
+
|
|
147
156
|
@click.command(
|
|
148
157
|
"check:deps",
|
|
149
158
|
short_help="Validate that feature imports respect UVL dependency constraints.",
|
|
@@ -202,17 +211,21 @@ def check_deps():
|
|
|
202
211
|
fpath = feature_paths.get(pkg_name)
|
|
203
212
|
|
|
204
213
|
if not fpath:
|
|
205
|
-
click.echo(
|
|
206
|
-
|
|
214
|
+
click.echo(
|
|
215
|
+
click.style(f" {short}", bold=True)
|
|
216
|
+
+ click.style(" (not in cache, skipped)", fg="bright_black")
|
|
217
|
+
)
|
|
207
218
|
continue
|
|
208
219
|
|
|
209
220
|
actual_imports = _scan_feature_imports(fpath, pkg_name, all_packages)
|
|
210
221
|
allowed = allowed_deps.get(pkg_name, set())
|
|
211
222
|
|
|
212
223
|
if not actual_imports:
|
|
213
|
-
click.echo(
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
click.echo(
|
|
225
|
+
click.style(" [✔] ", fg="green")
|
|
226
|
+
+ click.style(f"{short}", bold=True)
|
|
227
|
+
+ " — no cross-feature imports"
|
|
228
|
+
)
|
|
216
229
|
ok += 1
|
|
217
230
|
continue
|
|
218
231
|
|
|
@@ -221,24 +234,38 @@ def check_deps():
|
|
|
221
234
|
imp_short = pkg_to_short.get(imp, imp)
|
|
222
235
|
|
|
223
236
|
if imp in allowed:
|
|
224
|
-
click.echo(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
237
|
+
click.echo(
|
|
238
|
+
click.style(" [✔] ", fg="green")
|
|
239
|
+
+ click.style(f"{short}", bold=True)
|
|
240
|
+
+ f" imports {imp_short}"
|
|
241
|
+
+ click.style(
|
|
242
|
+
f" (allowed: {short} => {imp_short})", fg="bright_black"
|
|
243
|
+
)
|
|
244
|
+
)
|
|
228
245
|
ok += 1
|
|
229
246
|
else:
|
|
230
247
|
# Check if the reverse is declared (inverted dependency)
|
|
231
248
|
reverse_allowed = allowed_deps.get(imp, set())
|
|
232
249
|
if pkg_name in reverse_allowed:
|
|
233
|
-
click.echo(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
250
|
+
click.echo(
|
|
251
|
+
click.style(" [✖] ", fg="red")
|
|
252
|
+
+ click.style(f"{short}", bold=True)
|
|
253
|
+
+ f" imports {imp_short}"
|
|
254
|
+
+ click.style(
|
|
255
|
+
f" INVERTED — UVL says {imp_short} => {short}, not the reverse",
|
|
256
|
+
fg="red",
|
|
257
|
+
)
|
|
258
|
+
)
|
|
237
259
|
else:
|
|
238
|
-
click.echo(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
260
|
+
click.echo(
|
|
261
|
+
click.style(" [✖] ", fg="red")
|
|
262
|
+
+ click.style(f"{short}", bold=True)
|
|
263
|
+
+ f" imports {imp_short}"
|
|
264
|
+
+ click.style(
|
|
265
|
+
f" UNDECLARED — no UVL constraint between {short} and {imp_short}",
|
|
266
|
+
fg="red",
|
|
267
|
+
)
|
|
268
|
+
)
|
|
242
269
|
violations += 1
|
|
243
270
|
has_violation = True
|
|
244
271
|
|
|
@@ -247,10 +274,16 @@ def check_deps():
|
|
|
247
274
|
|
|
248
275
|
click.echo()
|
|
249
276
|
if violations:
|
|
250
|
-
click.secho(
|
|
277
|
+
click.secho(
|
|
278
|
+
f" {violations} violation(s) found. Fix the code or update the UVL.",
|
|
279
|
+
fg="red",
|
|
280
|
+
)
|
|
251
281
|
raise SystemExit(1)
|
|
252
282
|
else:
|
|
253
|
-
click.secho(
|
|
283
|
+
click.secho(
|
|
284
|
+
f" ✅ All cross-feature imports are consistent with UVL ({ok} checks passed).",
|
|
285
|
+
fg="green",
|
|
286
|
+
)
|
|
254
287
|
click.echo()
|
|
255
288
|
|
|
256
289
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
check:env — Validate workspace environment variables and tool versions.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import os
|
|
5
6
|
import sys
|
|
6
7
|
|
|
@@ -15,21 +16,26 @@ def _pkg_version(name: str) -> str | None:
|
|
|
15
16
|
return None
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
@click.command(
|
|
19
|
+
@click.command(
|
|
20
|
+
"check:env", short_help="Validate workspace environment and tool versions."
|
|
21
|
+
)
|
|
19
22
|
def check_env():
|
|
20
23
|
"""Check Python version, SPLENT env vars, CLI/framework compatibility."""
|
|
21
24
|
ok = fail = warn = 0
|
|
22
25
|
|
|
23
26
|
def _ok(msg):
|
|
24
|
-
nonlocal ok
|
|
27
|
+
nonlocal ok
|
|
28
|
+
ok += 1
|
|
25
29
|
click.echo(click.style(" [✔] ", fg="green") + msg)
|
|
26
30
|
|
|
27
31
|
def _fail(msg):
|
|
28
|
-
nonlocal fail
|
|
32
|
+
nonlocal fail
|
|
33
|
+
fail += 1
|
|
29
34
|
click.echo(click.style(" [✖] ", fg="red") + msg)
|
|
30
35
|
|
|
31
36
|
def _warn(msg):
|
|
32
|
-
nonlocal warn
|
|
37
|
+
nonlocal warn
|
|
38
|
+
warn += 1
|
|
33
39
|
click.echo(click.style(" [⚠] ", fg="yellow") + msg)
|
|
34
40
|
|
|
35
41
|
click.echo()
|
|
@@ -78,7 +84,9 @@ def check_env():
|
|
|
78
84
|
else:
|
|
79
85
|
_fail(f"CLI {cli_v} / Framework {fw_v} — major version mismatch")
|
|
80
86
|
else:
|
|
81
|
-
_fail(
|
|
87
|
+
_fail(
|
|
88
|
+
f"CLI={'?' if not cli_v else cli_v} / Framework={'?' if not fw_v else fw_v}"
|
|
89
|
+
)
|
|
82
90
|
|
|
83
91
|
# Credentials
|
|
84
92
|
if os.getenv("GITHUB_TOKEN"):
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
check:features — Validate feature cache, symlinks, pip install, and git state.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import os
|
|
5
6
|
import subprocess
|
|
6
7
|
import importlib.metadata
|
|
@@ -20,7 +21,9 @@ def _pkg_installed(name: str) -> bool:
|
|
|
20
21
|
return False
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
@click.command(
|
|
24
|
+
@click.command(
|
|
25
|
+
"check:features", short_help="Validate feature cache, symlinks, and install state."
|
|
26
|
+
)
|
|
24
27
|
def check_features():
|
|
25
28
|
"""Check every declared feature: cache entry, symlink, pip install, git state."""
|
|
26
29
|
workspace = str(context.workspace())
|
|
@@ -31,15 +34,18 @@ def check_features():
|
|
|
31
34
|
ok = fail = warn = 0
|
|
32
35
|
|
|
33
36
|
def _ok(msg):
|
|
34
|
-
nonlocal ok
|
|
37
|
+
nonlocal ok
|
|
38
|
+
ok += 1
|
|
35
39
|
click.echo(click.style(" [✔] ", fg="green") + msg)
|
|
36
40
|
|
|
37
41
|
def _fail(msg):
|
|
38
|
-
nonlocal fail
|
|
42
|
+
nonlocal fail
|
|
43
|
+
fail += 1
|
|
39
44
|
click.echo(click.style(" [✖] ", fg="red") + msg)
|
|
40
45
|
|
|
41
46
|
def _warn(msg):
|
|
42
|
-
nonlocal warn
|
|
47
|
+
nonlocal warn
|
|
48
|
+
warn += 1
|
|
43
49
|
click.echo(click.style(" [⚠] ", fg="yellow") + msg)
|
|
44
50
|
|
|
45
51
|
click.echo()
|
|
@@ -102,7 +108,7 @@ def check_features():
|
|
|
102
108
|
if os.path.exists(link_path):
|
|
103
109
|
target = os.readlink(link_path)
|
|
104
110
|
if os.path.isabs(target):
|
|
105
|
-
_warn(
|
|
111
|
+
_warn("Symlink uses absolute path (should be relative)")
|
|
106
112
|
else:
|
|
107
113
|
_ok("Symlink OK (relative)")
|
|
108
114
|
else:
|
|
@@ -123,7 +129,9 @@ def check_features():
|
|
|
123
129
|
try:
|
|
124
130
|
r = subprocess.run(
|
|
125
131
|
["git", "-C", feature_dir, "status", "--porcelain"],
|
|
126
|
-
capture_output=True,
|
|
132
|
+
capture_output=True,
|
|
133
|
+
text=True,
|
|
134
|
+
timeout=5,
|
|
127
135
|
)
|
|
128
136
|
if r.returncode != 0:
|
|
129
137
|
_warn("Not a git repo")
|
|
@@ -131,7 +139,7 @@ def check_features():
|
|
|
131
139
|
_warn("Uncommitted changes")
|
|
132
140
|
else:
|
|
133
141
|
_ok("Git clean")
|
|
134
|
-
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
142
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
135
143
|
_warn("Could not check git status")
|
|
136
144
|
|
|
137
145
|
click.echo()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
check:pyproject — Validate the active product's pyproject.toml.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
6
7
|
|
|
@@ -13,6 +14,7 @@ from splent_cli.utils.feature_utils import read_features_from_data
|
|
|
13
14
|
|
|
14
15
|
def _find_missing_pkgs(deps: list) -> list:
|
|
15
16
|
import importlib.metadata
|
|
17
|
+
|
|
16
18
|
missing = []
|
|
17
19
|
for dep in deps:
|
|
18
20
|
pkg = re.split(r"[=<>!~\[]", dep)[0].strip()
|
|
@@ -24,7 +26,9 @@ def _find_missing_pkgs(deps: list) -> list:
|
|
|
24
26
|
return missing
|
|
25
27
|
|
|
26
28
|
|
|
27
|
-
@click.command(
|
|
29
|
+
@click.command(
|
|
30
|
+
"check:pyproject", short_help="Validate pyproject.toml and dependencies."
|
|
31
|
+
)
|
|
28
32
|
def check_pyproject():
|
|
29
33
|
"""Parse pyproject.toml, check dependencies, and validate feature declarations."""
|
|
30
34
|
workspace = str(context.workspace())
|
|
@@ -34,15 +38,18 @@ def check_pyproject():
|
|
|
34
38
|
ok = fail = warn = 0
|
|
35
39
|
|
|
36
40
|
def _ok(msg):
|
|
37
|
-
nonlocal ok
|
|
41
|
+
nonlocal ok
|
|
42
|
+
ok += 1
|
|
38
43
|
click.echo(click.style(" [✔] ", fg="green") + msg)
|
|
39
44
|
|
|
40
45
|
def _fail(msg):
|
|
41
|
-
nonlocal fail
|
|
46
|
+
nonlocal fail
|
|
47
|
+
fail += 1
|
|
42
48
|
click.echo(click.style(" [✖] ", fg="red") + msg)
|
|
43
49
|
|
|
44
50
|
def _warn(msg):
|
|
45
|
-
nonlocal warn
|
|
51
|
+
nonlocal warn
|
|
52
|
+
warn += 1
|
|
46
53
|
click.echo(click.style(" [⚠] ", fg="yellow") + msg)
|
|
47
54
|
|
|
48
55
|
click.echo()
|
|
@@ -36,5 +36,8 @@ def coverage(module_name, html):
|
|
|
36
36
|
|
|
37
37
|
try:
|
|
38
38
|
subprocess.run(coverage_cmd, check=True)
|
|
39
|
-
except subprocess.CalledProcessError
|
|
40
|
-
click.echo(
|
|
39
|
+
except subprocess.CalledProcessError:
|
|
40
|
+
click.echo(
|
|
41
|
+
click.style("❌ Coverage run failed (tests may be failing).", fg="red")
|
|
42
|
+
)
|
|
43
|
+
raise SystemExit(1)
|
|
@@ -16,18 +16,23 @@ def db_console():
|
|
|
16
16
|
mariadb_database = os.getenv("MARIADB_DATABASE")
|
|
17
17
|
|
|
18
18
|
missing = [
|
|
19
|
-
name
|
|
19
|
+
name
|
|
20
|
+
for name, val in {
|
|
20
21
|
"MARIADB_HOSTNAME": mariadb_hostname,
|
|
21
22
|
"MARIADB_USER": mariadb_user,
|
|
22
23
|
"MARIADB_PASSWORD": mariadb_password,
|
|
23
24
|
"MARIADB_DATABASE": mariadb_database,
|
|
24
|
-
}.items()
|
|
25
|
+
}.items()
|
|
26
|
+
if not val
|
|
25
27
|
]
|
|
26
28
|
if missing:
|
|
27
29
|
click.secho("❌ Missing required environment variables:", fg="red")
|
|
28
30
|
for var in missing:
|
|
29
31
|
click.secho(f" - {var}", fg="red")
|
|
30
|
-
click.secho(
|
|
32
|
+
click.secho(
|
|
33
|
+
"\n Make sure the product .env is loaded: splent product:env --merge --dev",
|
|
34
|
+
fg="yellow",
|
|
35
|
+
)
|
|
31
36
|
raise SystemExit(1)
|
|
32
37
|
|
|
33
38
|
try:
|
|
@@ -18,6 +18,20 @@ def db_dump(filename):
|
|
|
18
18
|
mariadb_password = os.getenv("MARIADB_PASSWORD")
|
|
19
19
|
mariadb_database = os.getenv("MARIADB_DATABASE")
|
|
20
20
|
|
|
21
|
+
missing = [
|
|
22
|
+
k
|
|
23
|
+
for k, v in {
|
|
24
|
+
"MARIADB_HOSTNAME": mariadb_hostname,
|
|
25
|
+
"MARIADB_USER": mariadb_user,
|
|
26
|
+
"MARIADB_PASSWORD": mariadb_password,
|
|
27
|
+
"MARIADB_DATABASE": mariadb_database,
|
|
28
|
+
}.items()
|
|
29
|
+
if not v
|
|
30
|
+
]
|
|
31
|
+
if missing:
|
|
32
|
+
click.secho(f"❌ Missing env vars: {', '.join(missing)}", fg="red")
|
|
33
|
+
raise SystemExit(1)
|
|
34
|
+
|
|
21
35
|
# Generate default filename if not provided
|
|
22
36
|
if not filename:
|
|
23
37
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
@@ -28,23 +42,29 @@ def db_dump(filename):
|
|
|
28
42
|
filename += ".sql"
|
|
29
43
|
|
|
30
44
|
try:
|
|
45
|
+
env = {**os.environ, "MYSQL_PWD": mariadb_password or ""}
|
|
31
46
|
with open(filename, "wb") as out:
|
|
32
47
|
subprocess.run(
|
|
33
48
|
[
|
|
34
49
|
"mysqldump",
|
|
35
50
|
f"-h{mariadb_hostname}",
|
|
36
51
|
f"-u{mariadb_user}",
|
|
37
|
-
f"-p{mariadb_password}",
|
|
38
52
|
mariadb_database,
|
|
39
53
|
],
|
|
40
54
|
stdout=out,
|
|
41
55
|
check=True,
|
|
56
|
+
env=env,
|
|
42
57
|
)
|
|
43
58
|
click.echo(
|
|
44
59
|
click.style(f"Database dump created successfully: {filename}", fg="green")
|
|
45
60
|
)
|
|
46
61
|
except subprocess.CalledProcessError as e:
|
|
47
62
|
click.echo(click.style(f"Error creating database dump: {e}", fg="red"))
|
|
63
|
+
if os.path.exists(filename):
|
|
64
|
+
os.remove(filename)
|
|
65
|
+
click.echo(
|
|
66
|
+
click.style(f"Partial file removed: {filename}", fg="yellow")
|
|
67
|
+
)
|
|
48
68
|
|
|
49
69
|
|
|
50
70
|
cli_command = db_dump
|
|
@@ -5,7 +5,11 @@ from flask import current_app
|
|
|
5
5
|
from flask_migrate import migrate as alembic_migrate, upgrade as alembic_upgrade
|
|
6
6
|
|
|
7
7
|
from splent_cli.utils.decorators import requires_db
|
|
8
|
-
from splent_cli.utils.lifecycle import
|
|
8
|
+
from splent_cli.utils.lifecycle import (
|
|
9
|
+
advance_state,
|
|
10
|
+
resolve_feature_key_from_entry,
|
|
11
|
+
require_editable,
|
|
12
|
+
)
|
|
9
13
|
from splent_framework.managers.migration_manager import MigrationManager
|
|
10
14
|
from splent_framework.utils.feature_utils import get_features_from_pyproject
|
|
11
15
|
from splent_framework.utils.path_utils import PathUtils
|
|
@@ -25,16 +29,17 @@ def _is_empty_migration(path: str) -> bool:
|
|
|
25
29
|
content = f.read()
|
|
26
30
|
# Strip upgrade() and downgrade() bodies — if both are just pass, it's empty
|
|
27
31
|
import re
|
|
32
|
+
|
|
28
33
|
upgrades = re.findall(r"def upgrade\(\).*?(?=\ndef |\Z)", content, re.DOTALL)
|
|
29
34
|
downgrades = re.findall(r"def downgrade\(\).*?(?=\ndef |\Z)", content, re.DOTALL)
|
|
30
35
|
for body in upgrades + downgrades:
|
|
31
36
|
# Remove comments, docstrings, and whitespace — if only 'pass' remains, it's empty
|
|
32
37
|
lines = [
|
|
33
|
-
|
|
34
|
-
for
|
|
35
|
-
if
|
|
36
|
-
and not
|
|
37
|
-
and not
|
|
38
|
+
line.strip()
|
|
39
|
+
for line in body.splitlines()
|
|
40
|
+
if line.strip()
|
|
41
|
+
and not line.strip().startswith("#")
|
|
42
|
+
and not line.strip().startswith("def ")
|
|
38
43
|
]
|
|
39
44
|
if any(line != "pass" for line in lines):
|
|
40
45
|
return False
|
|
@@ -97,14 +102,18 @@ def db_migrate(feature):
|
|
|
97
102
|
|
|
98
103
|
# Suppress Alembic's verbose output during generation
|
|
99
104
|
import logging
|
|
105
|
+
|
|
100
106
|
alembic_logger = logging.getLogger("alembic")
|
|
101
107
|
prev_level = alembic_logger.level
|
|
102
108
|
alembic_logger.setLevel(logging.WARNING)
|
|
103
109
|
|
|
104
110
|
try:
|
|
105
111
|
alembic_migrate(directory=mdir, message=feat)
|
|
106
|
-
except Exception:
|
|
107
|
-
|
|
112
|
+
except Exception as e:
|
|
113
|
+
if os.getenv("SPLENT_DEBUG"):
|
|
114
|
+
click.secho(
|
|
115
|
+
f" ⚠️ {feat}: migration generation skipped ({e})", fg="yellow"
|
|
116
|
+
)
|
|
108
117
|
finally:
|
|
109
118
|
alembic_logger.setLevel(prev_level)
|
|
110
119
|
|
|
@@ -114,14 +123,20 @@ def db_migrate(feature):
|
|
|
114
123
|
if after > before:
|
|
115
124
|
versions_dir = os.path.join(mdir, "versions")
|
|
116
125
|
newest = max(
|
|
117
|
-
(
|
|
126
|
+
(
|
|
127
|
+
os.path.join(versions_dir, f)
|
|
128
|
+
for f in os.listdir(versions_dir)
|
|
129
|
+
if f.endswith(".py")
|
|
130
|
+
),
|
|
118
131
|
key=os.path.getmtime,
|
|
119
132
|
)
|
|
120
133
|
if _is_empty_migration(newest):
|
|
121
134
|
os.remove(newest)
|
|
122
135
|
click.echo(click.style(f" ✔ {feat}: up to date", fg="green"))
|
|
123
136
|
else:
|
|
124
|
-
click.echo(
|
|
137
|
+
click.echo(
|
|
138
|
+
click.style(f" 📝 {feat}: new migration generated", fg="cyan")
|
|
139
|
+
)
|
|
125
140
|
else:
|
|
126
141
|
click.echo(click.style(f" ✔ {feat}: up to date", fg="green"))
|
|
127
142
|
|
|
@@ -139,8 +154,13 @@ def db_migrate(feature):
|
|
|
139
154
|
if info:
|
|
140
155
|
key, ns, name, version = info
|
|
141
156
|
advance_state(
|
|
142
|
-
product_path,
|
|
143
|
-
|
|
157
|
+
product_path,
|
|
158
|
+
product_name,
|
|
159
|
+
key,
|
|
160
|
+
to="migrated",
|
|
161
|
+
namespace=ns,
|
|
162
|
+
name=name,
|
|
163
|
+
version=version,
|
|
144
164
|
)
|
|
145
165
|
except Exception as e:
|
|
146
166
|
click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
|
|
@@ -92,7 +92,11 @@ def db_reset(yes):
|
|
|
92
92
|
if not dirs:
|
|
93
93
|
click.echo(click.style("⚠️ No feature migrations found.", fg="yellow"))
|
|
94
94
|
else:
|
|
95
|
-
click.echo(
|
|
95
|
+
click.echo(
|
|
96
|
+
click.style(
|
|
97
|
+
f"⬆️ Applying migrations for {len(dirs)} features...", fg="cyan"
|
|
98
|
+
)
|
|
99
|
+
)
|
|
96
100
|
for feat, mdir in dirs.items():
|
|
97
101
|
try:
|
|
98
102
|
alembic_upgrade(directory=mdir)
|
|
@@ -109,8 +113,13 @@ def db_reset(yes):
|
|
|
109
113
|
if info:
|
|
110
114
|
key, ns, name, version = info
|
|
111
115
|
advance_state(
|
|
112
|
-
product_path,
|
|
113
|
-
|
|
116
|
+
product_path,
|
|
117
|
+
product_name,
|
|
118
|
+
key,
|
|
119
|
+
to="migrated",
|
|
120
|
+
namespace=ns,
|
|
121
|
+
name=name,
|
|
122
|
+
version=version,
|
|
114
123
|
)
|
|
115
124
|
except Exception as e:
|
|
116
125
|
click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
|
|
@@ -57,11 +57,13 @@ def db_restore(filename, yes):
|
|
|
57
57
|
raise SystemExit(0)
|
|
58
58
|
|
|
59
59
|
try:
|
|
60
|
+
env = {**os.environ, "MYSQL_PWD": password or ""}
|
|
60
61
|
with open(filename, "rb") as sql_file:
|
|
61
62
|
subprocess.run(
|
|
62
|
-
["mysql", f"-h{host}", f"-u{user}",
|
|
63
|
+
["mysql", f"-h{host}", f"-u{user}", database],
|
|
63
64
|
stdin=sql_file,
|
|
64
65
|
check=True,
|
|
66
|
+
env=env,
|
|
65
67
|
)
|
|
66
68
|
click.secho(f"✅ Database restored from: {filename}", fg="green")
|
|
67
69
|
except subprocess.CalledProcessError as e:
|
|
@@ -49,8 +49,13 @@ def db_rollback(feature, steps):
|
|
|
49
49
|
if name == feature:
|
|
50
50
|
target = "installed" if revision is None else "migrated"
|
|
51
51
|
advance_state(
|
|
52
|
-
product_path,
|
|
53
|
-
|
|
52
|
+
product_path,
|
|
53
|
+
product_name,
|
|
54
|
+
key,
|
|
55
|
+
to=target,
|
|
56
|
+
namespace=ns,
|
|
57
|
+
name=name,
|
|
58
|
+
version=version,
|
|
54
59
|
)
|
|
55
60
|
break
|
|
56
61
|
except Exception as e:
|