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.
Files changed (139) hide show
  1. {splent_cli-1.2.6/src/splent_cli.egg-info → splent_cli-1.2.8}/PKG-INFO +1 -1
  2. {splent_cli-1.2.6 → splent_cli-1.2.8}/pyproject.toml +1 -1
  3. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_orphans.py +2 -1
  4. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_status.py +18 -7
  5. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_deps.py +57 -24
  6. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_env.py +13 -5
  7. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_features.py +15 -7
  8. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_pyproject.py +11 -4
  9. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/coverage.py +5 -2
  10. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_console.py +8 -3
  11. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_dump.py +21 -1
  12. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_migrate.py +32 -12
  13. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_reset.py +12 -3
  14. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_restore.py +3 -1
  15. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_rollback.py +7 -2
  16. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_seed.py +13 -5
  17. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_status.py +13 -4
  18. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/database/db_upgrade.py +9 -2
  19. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/doctor.py +17 -13
  20. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/env/env_set.py +15 -2
  21. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/export_puml.py +234 -123
  22. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_add.py +17 -7
  23. splent_cli-1.2.8/src/splent_cli/commands/feature/feature_attach.py +105 -0
  24. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_clone.py +55 -13
  25. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_contract.py +24 -18
  26. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_create.py +39 -9
  27. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_delete.py +2 -2
  28. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_detach.py +2 -1
  29. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_diff.py +231 -142
  30. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_discard.py +2 -2
  31. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_drift.py +4 -7
  32. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_edit.py +66 -9
  33. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_fork.py +3 -1
  34. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_hook_add.py +3 -1
  35. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_hook_remove.py +2 -0
  36. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_hooks.py +5 -2
  37. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_list.py +5 -1
  38. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_order.py +40 -15
  39. splent_cli-1.2.8/src/splent_cli/commands/feature/feature_pip_install.py +96 -0
  40. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_release.py +54 -18
  41. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_remove.py +6 -2
  42. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_rename.py +4 -1
  43. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_status.py +25 -13
  44. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_sync_template.py +3 -9
  45. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_test.py +11 -4
  46. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_upgrade.py +37 -8
  47. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_versions.py +91 -29
  48. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature_compile.py +47 -18
  49. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/linter.py +10 -10
  50. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/locust.py +10 -4
  51. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_build.py +54 -15
  52. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_clean.py +4 -0
  53. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_create.py +1 -0
  54. splent_cli-1.2.8/src/splent_cli/commands/product/product_derive.py +269 -0
  55. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_drift.py +2 -1
  56. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_release.py +45 -15
  57. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_select.py +3 -3
  58. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_status.py +28 -8
  59. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_sync.py +20 -8
  60. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_sync_template.py +2 -6
  61. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_up.py +33 -1
  62. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/selenium.py +5 -1
  63. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/tokens.py +1 -3
  64. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_check.py +10 -4
  65. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_fetch.py +1 -2
  66. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_fix.py +35 -10
  67. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_sync.py +5 -3
  68. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_utils.py +3 -1
  69. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/version.py +6 -4
  70. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/services/compose.py +10 -2
  71. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/services/context.py +11 -2
  72. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/services/release.py +74 -25
  73. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/command_loader.py +3 -1
  74. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/db_utils.py +4 -1
  75. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/feature_installer.py +3 -1
  76. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/feature_utils.py +3 -1
  77. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/lifecycle.py +23 -21
  78. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/manifest.py +10 -3
  79. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/template_drift.py +43 -51
  80. {splent_cli-1.2.6 → splent_cli-1.2.8/src/splent_cli.egg-info}/PKG-INFO +1 -1
  81. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli.egg-info/SOURCES.txt +1 -0
  82. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_attach.py +0 -165
  83. splent_cli-1.2.6/src/splent_cli/commands/product/product_derive.py +0 -136
  84. {splent_cli-1.2.6 → splent_cli-1.2.8}/LICENSE +0 -0
  85. {splent_cli-1.2.6 → splent_cli-1.2.8}/README.md +0 -0
  86. {splent_cli-1.2.6 → splent_cli-1.2.8}/setup.cfg +0 -0
  87. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/__init__.py +0 -0
  88. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/__main__.py +0 -0
  89. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/cli.py +0 -0
  90. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/__init__.py +0 -0
  91. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/__init__.py +0 -0
  92. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  93. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  94. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  95. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_size.py +0 -0
  96. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  97. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  98. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/__init__.py +0 -0
  99. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_docker.py +0 -0
  100. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_github.py +0 -0
  101. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/check/check_pypi.py +0 -0
  102. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/clear_cache.py +0 -0
  103. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/clear_log.py +0 -0
  104. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/clear_uploads.py +0 -0
  105. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/command_create.py +0 -0
  106. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/env/env_list.py +0 -0
  107. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/env/env_show.py +0 -0
  108. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_env.py +0 -0
  109. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_git.py +0 -0
  110. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  111. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/feature/feature_search.py +0 -0
  112. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/__init__.py +0 -0
  113. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_deploy.py +0 -0
  114. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_down.py +0 -0
  115. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_env.py +0 -0
  116. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_list.py +0 -0
  117. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_logs.py +0 -0
  118. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_port.py +0 -0
  119. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_run.py +0 -0
  120. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/product/product_shell.py +0 -0
  121. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/release/__init__.py +0 -0
  122. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/release/release_core.py +0 -0
  123. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/route_list.py +0 -0
  124. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_configs.py +0 -0
  125. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_deps.py +0 -0
  126. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_features.py +0 -0
  127. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_info.py +0 -0
  128. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_missing.py +0 -0
  129. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/commands/uvl/uvl_valid.py +0 -0
  130. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/services/__init__.py +0 -0
  131. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/__init__.py +0 -0
  132. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/cache_utils.py +0 -0
  133. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/decorators.py +0 -0
  134. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/dynamic_imports.py +0 -0
  135. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli/utils/path_utils.py +0 -0
  136. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  137. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli.egg-info/entry_points.txt +0 -0
  138. {splent_cli-1.2.6 → splent_cli-1.2.8}/src/splent_cli.egg-info/requires.txt +0 -0
  139. {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.6
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.2.6"
7
+ version = "1.2.8"
8
8
  description = "SPLENT-CLI is a CLI to be able to work on your development more easily."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -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 Exception:
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 (ns_dir.is_dir()
41
- and not ns_dir.name.startswith(("_", "."))
42
- and "." not in ns_dir.name
43
- and (ns_dir / entry.name).is_dir()):
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(f" {connector} " + click.style("editable (workspace root)", fg="magenta"))
80
+ click.echo(
81
+ f" {connector} "
82
+ + click.style("editable (workspace root)", fg="magenta")
83
+ )
79
84
  elif v is None:
80
- click.echo(f" {connector} " + click.style("editable (cache)", fg="blue"))
85
+ click.echo(
86
+ f" {connector} " + click.style("editable (cache)", fg="blue")
87
+ )
81
88
  else:
82
- click.echo(f" {connector} " + click.style(f"@{v}", fg="green") + click.style(" (pinned)", fg="bright_black"))
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
- def _scan_feature_imports(feature_path: str, feature_name: str, all_packages: set[str]) -> set[str]:
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 Exception:
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(r"(?:from|import)\s+splent_io\.(splent_feature_\w+)", content):
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
- def _resolve_feature_paths(workspace: str, product: str, features: list[str]) -> dict[str, str]:
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(click.style(f" {short}", bold=True) +
206
- click.style(" (not in cache, skipped)", fg="bright_black"))
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(click.style(" [✔] ", fg="green") +
214
- click.style(f"{short}", bold=True) +
215
- " — no cross-feature imports")
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(click.style(" [✔] ", fg="green") +
225
- click.style(f"{short}", bold=True) +
226
- f" imports {imp_short}" +
227
- click.style(f" (allowed: {short} => {imp_short})", fg="bright_black"))
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(click.style(" [✖] ", fg="red") +
234
- click.style(f"{short}", bold=True) +
235
- f" imports {imp_short}" +
236
- click.style(f" INVERTED UVL says {imp_short} => {short}, not the reverse", fg="red"))
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(click.style(" [✖] ", fg="red") +
239
- click.style(f"{short}", bold=True) +
240
- f" imports {imp_short}" +
241
- click.style(f" UNDECLARED no UVL constraint between {short} and {imp_short}", fg="red"))
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(f" {violations} violation(s) found. Fix the code or update the UVL.", fg="red")
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(f" ✅ All cross-feature imports are consistent with UVL ({ok} checks passed).", fg="green")
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("check:env", short_help="Validate workspace environment and tool versions.")
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; ok += 1
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; fail += 1
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; warn += 1
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(f"CLI={'?' if not cli_v else cli_v} / Framework={'?' if not fw_v else fw_v}")
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("check:features", short_help="Validate feature cache, symlinks, and install state.")
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; ok += 1
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; fail += 1
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; warn += 1
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(f"Symlink uses absolute path (should be relative)")
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, text=True, timeout=5,
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("check:pyproject", short_help="Validate pyproject.toml and dependencies.")
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; ok += 1
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; fail += 1
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; warn += 1
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 as e:
40
- click.echo(click.style(f"Error running coverage: {e}", fg="red"))
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 for name, val in {
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() if not val
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("\n Make sure the product .env is loaded: splent product:env --merge --dev", fg="yellow")
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 advance_state, resolve_feature_key_from_entry, require_editable
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
- l.strip()
34
- for l in body.splitlines()
35
- if l.strip()
36
- and not l.strip().startswith("#")
37
- and not l.strip().startswith("def ")
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
- pass
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
- (os.path.join(versions_dir, f) for f in os.listdir(versions_dir) if f.endswith(".py")),
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(click.style(f" 📝 {feat}: new migration generated", fg="cyan"))
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, product_name, key,
143
- to="migrated", namespace=ns, name=name, version=version,
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(click.style(f"⬆️ Applying migrations for {len(dirs)} features...", fg="cyan"))
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, product_name, key,
113
- to="migrated", namespace=ns, name=name, version=version,
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}", f"-p{password}", database],
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, product_name, key,
53
- to=target, namespace=ns, name=name, version=version,
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: