splent-cli 1.6.0__tar.gz → 1.8.0__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 (169) hide show
  1. {splent_cli-1.6.0/src/splent_cli.egg-info → splent_cli-1.8.0}/PKG-INFO +2 -1
  2. {splent_cli-1.6.0 → splent_cli-1.8.0}/pyproject.toml +2 -1
  3. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_pyproject.py +53 -14
  4. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_reset.py +4 -0
  5. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_upgrade.py +5 -0
  6. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/export/export_puml.py +100 -25
  7. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_add.py +19 -0
  8. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_attach.py +21 -0
  9. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_env.py +12 -14
  10. splent_cli-1.8.0/src/splent_cli/commands/feature/feature_impact.py +161 -0
  11. splent_cli-1.8.0/src/splent_cli/commands/feature/feature_install.py +425 -0
  12. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_release.py +22 -9
  13. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_test.py +48 -0
  14. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_build.py +153 -11
  15. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_configure.py +66 -3
  16. splent_cli-1.8.0/src/splent_cli/commands/product/product_containers.py +316 -0
  17. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_deploy.py +60 -6
  18. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_env.py +33 -0
  19. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_restart.py +36 -0
  20. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_test.py +14 -0
  21. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_up.py +12 -10
  22. splent_cli-1.8.0/src/splent_cli/services/preflight.py +207 -0
  23. {splent_cli-1.6.0 → splent_cli-1.8.0/src/splent_cli.egg-info}/PKG-INFO +2 -1
  24. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli.egg-info/SOURCES.txt +2 -0
  25. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli.egg-info/requires.txt +1 -0
  26. splent_cli-1.6.0/src/splent_cli/commands/product/product_containers.py +0 -196
  27. splent_cli-1.6.0/src/splent_cli/services/preflight.py +0 -88
  28. {splent_cli-1.6.0 → splent_cli-1.8.0}/LICENSE +0 -0
  29. {splent_cli-1.6.0 → splent_cli-1.8.0}/README.md +0 -0
  30. {splent_cli-1.6.0 → splent_cli-1.8.0}/setup.cfg +0 -0
  31. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/__init__.py +0 -0
  32. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/__main__.py +0 -0
  33. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/cli.py +0 -0
  34. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/__init__.py +0 -0
  35. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  36. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  37. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  38. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  39. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  40. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  41. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  42. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  43. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  44. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/__init__.py +0 -0
  45. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  46. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  47. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_env.py +0 -0
  48. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_features.py +0 -0
  49. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_github.py +0 -0
  50. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  51. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_product.py +0 -0
  52. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  53. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  54. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  55. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  56. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  57. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/command/__init__.py +0 -0
  58. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/command/command_create.py +0 -0
  59. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/coverage.py +0 -0
  60. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_console.py +0 -0
  61. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  62. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  63. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  64. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  65. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  66. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_status.py +0 -0
  67. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/doctor.py +0 -0
  68. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/env/env_list.py +0 -0
  69. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/env/env_set.py +0 -0
  70. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/env/env_show.py +0 -0
  71. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/export/__init__.py +0 -0
  72. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  73. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  74. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
  75. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  76. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
  77. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
  78. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  79. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
  80. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  81. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  82. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  83. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  84. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  85. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  86. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  87. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  88. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  89. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  90. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  91. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  92. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  93. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  94. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
  95. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
  96. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  97. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  98. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  99. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  100. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  101. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
  102. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  103. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  104. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  105. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/lint.py +0 -0
  106. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/locust.py +0 -0
  107. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/__init__.py +0 -0
  108. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  109. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  110. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  111. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_config.py +0 -0
  112. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_console.py +0 -0
  113. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_create.py +0 -0
  114. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  115. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  116. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_down.py +0 -0
  117. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  118. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_list.py +0 -0
  119. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  120. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  121. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_port.py +0 -0
  122. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_release.py +0 -0
  123. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  124. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  125. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_run.py +0 -0
  126. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_select.py +0 -0
  127. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  128. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  129. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  130. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_validate.py +0 -0
  131. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/release/__init__.py +0 -0
  132. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/release/release_core.py +0 -0
  133. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/selenium.py +0 -0
  134. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  135. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  136. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  137. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  138. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  139. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  140. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  141. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  142. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  143. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  144. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  145. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  146. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  147. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/version.py +0 -0
  148. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/services/__init__.py +0 -0
  149. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/services/compose.py +0 -0
  150. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/services/context.py +0 -0
  151. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/services/release.py +0 -0
  152. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/__init__.py +0 -0
  153. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/cache_utils.py +0 -0
  154. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/command_loader.py +0 -0
  155. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  156. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/db_utils.py +0 -0
  157. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/decorators.py +0 -0
  158. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  159. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/feature_installer.py +0 -0
  160. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/feature_utils.py +0 -0
  161. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/git_url.py +0 -0
  162. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/integrity.py +0 -0
  163. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/lifecycle.py +0 -0
  164. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/manifest.py +0 -0
  165. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/path_utils.py +0 -0
  166. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/template_drift.py +0 -0
  167. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  168. {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  169. {splent_cli-1.6.0 → splent_cli-1.8.0}/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.6.0
3
+ Version: 1.8.0
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
@@ -10,6 +10,7 @@ Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: click==8.1.8
12
12
  Requires-Dist: packaging>=24.0
13
+ Requires-Dist: requests>=2.31.0
13
14
  Provides-Extra: dev
14
15
  Requires-Dist: setuptools==80.3.1; extra == "dev"
15
16
  Requires-Dist: pytest==8.3.4; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.6.0"
7
+ version = "1.8.0"
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"
@@ -14,6 +14,7 @@ license-files = ["LICENSE"]
14
14
  dependencies = [
15
15
  "click==8.1.8",
16
16
  "packaging>=24.0",
17
+ "requests>=2.31.0",
17
18
  ]
18
19
 
19
20
  [project.optional-dependencies]
@@ -9,7 +9,6 @@ import click
9
9
  import tomllib
10
10
 
11
11
  from splent_cli.services import context
12
- from splent_cli.utils.feature_utils import read_features_from_data
13
12
 
14
13
 
15
14
  def _find_missing_pkgs(deps: list) -> list:
@@ -88,29 +87,69 @@ def check_pyproject():
88
87
  _ok(f"All {len(deps)} dependencies satisfied")
89
88
 
90
89
  # Features
91
- features = read_features_from_data(data)
92
- _ok(f"{len(features)} features declared in [tool.splent.features]")
93
-
94
90
  splent = data.get("tool", {}).get("splent", {})
91
+ base_feats = splent.get("features", [])
95
92
  dev_feats = splent.get("features_dev", [])
96
93
  prod_feats = splent.get("features_prod", [])
94
+
95
+ _ok(f"{len(base_feats)} features in [tool.splent].features")
97
96
  if dev_feats:
98
97
  _ok(f"{len(dev_feats)} dev-only features")
99
98
  if prod_feats:
100
99
  _ok(f"{len(prod_feats)} prod-only features")
101
100
 
102
- # UVL
103
- uvl_cfg = splent.get("uvl", {})
104
- uvl_file = uvl_cfg.get("file")
105
- if uvl_file:
106
- app_path = os.path.join(workspace, product)
107
- uvl_path = os.path.join(app_path, "uvl", uvl_file)
108
- if os.path.exists(uvl_path):
109
- _ok(f"UVL file found: uvl/{uvl_file}")
101
+ # Check for duplicates across feature lists
102
+ def _bare_name(entry):
103
+ name = entry.split("/")[-1] if "/" in entry else entry
104
+ return name.split("@")[0]
105
+
106
+ all_entries = base_feats + dev_feats + prod_feats
107
+ seen_names = {}
108
+ for entry in all_entries:
109
+ bare = _bare_name(entry)
110
+ seen_names.setdefault(bare, []).append(entry)
111
+
112
+ for bare, entries in seen_names.items():
113
+ if len(entries) > 1:
114
+ short = bare.replace("splent_feature_", "")
115
+ _fail(f"Duplicate feature '{short}': {', '.join(entries)}")
116
+
117
+ # Check for inconsistent namespaces
118
+ namespaces = set()
119
+ for entry in all_entries:
120
+ if "/" in entry:
121
+ ns = entry.split("/")[0]
122
+ namespaces.add(ns)
123
+ if len(namespaces) > 1:
124
+ _warn(
125
+ f"Inconsistent namespaces: {', '.join(sorted(namespaces))} — use one format consistently"
126
+ )
127
+
128
+ # SPL / UVL
129
+ spl_name = splent.get("spl")
130
+ if spl_name:
131
+ uvl_path = os.path.join(
132
+ workspace, "splent_catalog", spl_name, f"{spl_name}.uvl"
133
+ )
134
+ if os.path.isfile(uvl_path):
135
+ _ok(f"SPL catalog: {spl_name} (UVL found)")
110
136
  else:
111
- _fail(f"UVL file not found: uvl/{uvl_file}")
137
+ _fail(
138
+ f"SPL catalog: {spl_name} — UVL not found at splent_catalog/{spl_name}/{spl_name}.uvl"
139
+ )
112
140
  else:
113
- _warn("No UVL file configured")
141
+ # Legacy fallback
142
+ uvl_cfg = splent.get("uvl", {})
143
+ uvl_file = uvl_cfg.get("file")
144
+ if uvl_file:
145
+ app_path = os.path.join(workspace, product)
146
+ uvl_path = os.path.join(app_path, "uvl", uvl_file)
147
+ if os.path.exists(uvl_path):
148
+ _ok(f"UVL file found: uvl/{uvl_file} (legacy)")
149
+ else:
150
+ _fail(f"UVL file not found: uvl/{uvl_file}")
151
+ else:
152
+ _warn("No SPL configured — set [tool.splent].spl in pyproject.toml")
114
153
 
115
154
  click.echo()
116
155
  if fail:
@@ -123,6 +123,10 @@ def db_reset(yes):
123
123
  name=name,
124
124
  version=version,
125
125
  )
126
+ except ImportError as e:
127
+ if "models" in str(e):
128
+ continue
129
+ click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
126
130
  except Exception as e:
127
131
  click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
128
132
 
@@ -84,6 +84,11 @@ def db_upgrade(feature):
84
84
  name=name,
85
85
  version=version,
86
86
  )
87
+ except ImportError as e:
88
+ if "models" in str(e):
89
+ # Feature has migrations/ dir but no models module — skip silently
90
+ continue
91
+ click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
87
92
  except Exception as e:
88
93
  click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
89
94
 
@@ -149,8 +149,8 @@ def _parse_models(models_path: str) -> list[dict]:
149
149
  text = "\n".join(joined_lines)
150
150
 
151
151
  classes = []
152
- # Split by class definition
153
- class_blocks = re.split(r"^(class \w+\(.*?\):)", text, flags=re.MULTILINE)
152
+ # Split by class definition (with or without parentheses for mixins)
153
+ class_blocks = re.split(r"^(class \w+(?:\(.*?\))?:)", text, flags=re.MULTILINE)
154
154
 
155
155
  i = 1
156
156
  while i < len(class_blocks):
@@ -158,7 +158,7 @@ def _parse_models(models_path: str) -> list[dict]:
158
158
  body = class_blocks[i + 1] if i + 1 < len(class_blocks) else ""
159
159
  i += 2
160
160
 
161
- class_m = re.match(r"class (\w+)\(", header)
161
+ class_m = re.match(r"class (\w+)", header)
162
162
  if not class_m:
163
163
  continue
164
164
  class_name = class_m.group(1)
@@ -213,8 +213,11 @@ def _parse_models(models_path: str) -> list[dict]:
213
213
  if rel_m:
214
214
  rel_name = rel_m.group(1)
215
215
  rel_args = rel_m.group(2)
216
- target_m = re.match(r"(\w+)", rel_args)
217
- target = target_m.group(1) if target_m else "?"
216
+ # Target can be unquoted (User) or quoted ("User" / 'User')
217
+ target_m = re.match(r"""[\"']?(\w+)[\"']?""", rel_args)
218
+ target = target_m.group(1) if target_m else None
219
+ if not target:
220
+ continue
218
221
  uselist = "uselist=False" not in rel_args
219
222
  relationships.append(
220
223
  {
@@ -668,11 +671,16 @@ def _render_exports(
668
671
  )
669
672
  except subprocess.CalledProcessError as e:
670
673
  click.secho("❌ PlantUML failed to render diagram.", fg="red")
671
- if os.getenv("SPLENT_DEBUG"):
672
- stderr = e.stderr
673
- if isinstance(stderr, bytes):
674
- stderr = stderr.decode(errors="replace")
675
- click.secho(stderr[:500], fg="bright_black")
674
+ stderr = e.stderr
675
+ if isinstance(stderr, bytes):
676
+ stderr = stderr.decode(errors="replace")
677
+ if stderr:
678
+ click.secho(stderr.strip(), fg="bright_black")
679
+ stdout = e.stdout
680
+ if isinstance(stdout, bytes):
681
+ stdout = stdout.decode(errors="replace")
682
+ if stdout:
683
+ click.secho(stdout.strip(), fg="bright_black")
676
684
  return
677
685
 
678
686
  try:
@@ -780,21 +788,41 @@ def export_puml(
780
788
  product = context.require_app()
781
789
  product_dir = os.path.join(workspace, product)
782
790
 
783
- # Read UVL
784
- try:
785
- uvl_cfg = PyprojectReader.for_product(product_dir).uvl_config
786
- except (FileNotFoundError, RuntimeError) as e:
787
- click.secho(f"❌ Cannot read pyproject.toml: {e}", fg="red")
788
- raise SystemExit(1)
789
-
790
- uvl_file = uvl_cfg.get("file")
791
- if not uvl_file:
792
- click.secho("❌ No UVL file configured in [tool.splent.uvl].", fg="red")
793
- raise SystemExit(1)
794
-
795
- uvl_path = os.path.join(product_dir, "uvl", uvl_file)
796
- if not os.path.isfile(uvl_path):
797
- click.secho(f"❌ UVL file not found: {uvl_path}", fg="red")
791
+ # Read UVL — try SPL catalog first, fall back to legacy [tool.splent.uvl]
792
+ import tomllib
793
+
794
+ uvl_path = None
795
+ pyproject_path = os.path.join(product_dir, "pyproject.toml")
796
+ if os.path.isfile(pyproject_path):
797
+ with open(pyproject_path, "rb") as f:
798
+ pydata = tomllib.load(f)
799
+ spl_name = pydata.get("tool", {}).get("splent", {}).get("spl")
800
+ if spl_name:
801
+ candidate = os.path.join(
802
+ workspace, "splent_catalog", spl_name, f"{spl_name}.uvl"
803
+ )
804
+ if os.path.isfile(candidate):
805
+ uvl_path = candidate
806
+
807
+ # Legacy fallback: [tool.splent.uvl].file
808
+ if not uvl_path:
809
+ try:
810
+ uvl_cfg = PyprojectReader.for_product(product_dir).uvl_config
811
+ uvl_file = uvl_cfg.get("file")
812
+ if uvl_file:
813
+ candidate = os.path.join(product_dir, "uvl", uvl_file)
814
+ if os.path.isfile(candidate):
815
+ uvl_path = candidate
816
+ except (FileNotFoundError, RuntimeError):
817
+ pass
818
+
819
+ if not uvl_path:
820
+ click.secho(
821
+ "❌ No UVL file found.\n"
822
+ " Set [tool.splent].spl in pyproject.toml (SPL catalog)\n"
823
+ " or [tool.splent.uvl].file (legacy).",
824
+ fg="red",
825
+ )
798
826
  raise SystemExit(1)
799
827
 
800
828
  click.echo(f"📖 Reading UVL model: {uvl_path}")
@@ -848,6 +876,53 @@ def export_puml(
848
876
  if parsed:
849
877
  all_models[package] = parsed
850
878
  break
879
+ # Apply refinement mixins: merge mixin attributes into target models
880
+ for package, fpath in feature_paths.items():
881
+ pyproject_path = os.path.join(fpath, "pyproject.toml")
882
+ if not os.path.isfile(pyproject_path):
883
+ continue
884
+ import tomllib as _tomllib
885
+
886
+ with open(pyproject_path, "rb") as f:
887
+ feat_data = _tomllib.load(f)
888
+ extends = (
889
+ feat_data.get("tool", {})
890
+ .get("splent", {})
891
+ .get("refinement", {})
892
+ .get("extends", {})
893
+ .get("models", [])
894
+ )
895
+ if not extends:
896
+ continue
897
+ # Parse the mixin's models.py
898
+ mixin_models = []
899
+ src_root = os.path.join(fpath, "src")
900
+ if os.path.isdir(src_root):
901
+ for org_dir in os.listdir(src_root):
902
+ mpath = os.path.join(src_root, org_dir, package, "models.py")
903
+ if os.path.isfile(mpath):
904
+ mixin_models = _parse_models(mpath)
905
+ break
906
+ # Merge each mixin into its target model
907
+ for ext in extends:
908
+ target_name = ext.get("target")
909
+ mixin_name = ext.get("mixin")
910
+ if not target_name or not mixin_name:
911
+ continue
912
+ # Find the mixin class in parsed mixin_models
913
+ mixin_cls = next(
914
+ (m for m in mixin_models if m["name"] == mixin_name), None
915
+ )
916
+ if not mixin_cls:
917
+ continue
918
+ # Find the target model across all parsed features
919
+ for feat_models in all_models.values():
920
+ for model in feat_models:
921
+ if model["name"] == target_name:
922
+ model["attributes"].extend(mixin_cls["attributes"])
923
+ model["methods"].extend(mixin_cls["methods"])
924
+ break
925
+
851
926
  click.echo(
852
927
  f"📋 Parsed models from {len(all_models)}/{len(feature_paths)} features."
853
928
  )
@@ -59,6 +59,25 @@ def feature_add(full_name, env_scope):
59
59
  )
60
60
  raise SystemExit(1)
61
61
 
62
+ # ── Auto-detect env scope from feature contract ───────────────────
63
+ if not env_scope:
64
+ feat_pyproject = os.path.join(feature_dir, "pyproject.toml")
65
+ if os.path.isfile(feat_pyproject):
66
+ with open(feat_pyproject, "rb") as f:
67
+ feat_data = tomllib.load(f)
68
+ contract_env = (
69
+ feat_data.get("tool", {})
70
+ .get("splent", {})
71
+ .get("contract", {})
72
+ .get("env")
73
+ )
74
+ if contract_env:
75
+ env_scope = contract_env
76
+ click.echo(
77
+ click.style(" scope ", dim=True)
78
+ + f"contract declares env={contract_env} → features_{contract_env}"
79
+ )
80
+
62
81
  # ── Update pyproject.toml ─────────────────────────────────────────
63
82
  pyproject_path = os.path.join(workspace, product, "pyproject.toml")
64
83
  if not os.path.exists(pyproject_path):
@@ -64,6 +64,27 @@ def feature_attach(feature_identifier, version, env_scope):
64
64
 
65
65
  short = feature_name.replace("splent_feature_", "")
66
66
 
67
+ # ── Auto-detect env scope from feature contract ───────────────────
68
+ if not env_scope:
69
+ feat_pyproject = os.path.join(versioned_dir, "pyproject.toml")
70
+ if os.path.isfile(feat_pyproject):
71
+ import tomllib as _tomllib
72
+
73
+ with open(feat_pyproject, "rb") as f:
74
+ feat_data = _tomllib.load(f)
75
+ contract_env = (
76
+ feat_data.get("tool", {})
77
+ .get("splent", {})
78
+ .get("contract", {})
79
+ .get("env")
80
+ )
81
+ if contract_env:
82
+ env_scope = contract_env
83
+ click.echo(
84
+ click.style(" scope ", dim=True)
85
+ + f"contract declares env={contract_env} → features_{contract_env}"
86
+ )
87
+
67
88
  # ── Update pyproject.toml ─────────────────────────────────────────
68
89
  full_name = f"{namespace}/{feature_name}@{version}"
69
90
  bare_name = f"{namespace}/{feature_name}"
@@ -45,19 +45,26 @@ def feature_env(feature_name, generate, env_name):
45
45
  with open(pyproject_path, "rb") as f:
46
46
  data = tomllib.load(f)
47
47
 
48
- feature_entries = read_features_from_data(data)
48
+ # 2️⃣ Determinar entorno (antes de leer features, para incluir features_dev/prod)
49
+ env_from_var = os.getenv("SPLENT_ENV")
50
+ if not env_name and env_from_var:
51
+ env_name = env_from_var
52
+ click.echo(f"🌍 Using SPLENT_ENV={env_name}")
53
+ elif not env_name:
54
+ click.echo("❌ You must specify --dev, --prod, or define SPLENT_ENV=dev|prod.")
55
+ raise SystemExit(1)
56
+
57
+ feature_entries = read_features_from_data(data, env_name)
49
58
  if not feature_entries:
50
59
  click.echo("❌ No features declared in pyproject.toml.")
51
60
  raise SystemExit(1)
52
61
 
53
- # 2️⃣ Buscar la feature pedida con su versión
62
+ # 3️⃣ Buscar la feature pedida con su versión
54
63
  feature_entry = next(
55
64
  (f for f in feature_entries if f.startswith(feature_name)), None
56
65
  )
57
66
  if not feature_entry:
58
- click.echo(
59
- f"❌ Feature '{feature_name}' not found in [features] section of {pyproject_path}"
60
- )
67
+ click.echo(f"❌ Feature '{feature_name}' not found in pyproject.toml")
61
68
  raise SystemExit(1)
62
69
 
63
70
  # Derive org_safe from the entry itself (e.g. "splent_io/splent_feature_auth@v1")
@@ -68,15 +75,6 @@ def feature_env(feature_name, generate, env_name):
68
75
  org_safe = "splent_io"
69
76
  entry_basename = feature_entry
70
77
 
71
- # 3️⃣ Determinar entorno
72
- env_from_var = os.getenv("SPLENT_ENV")
73
- if not env_name and env_from_var:
74
- env_name = env_from_var
75
- click.echo(f"🌍 Using SPLENT_ENV={env_name}")
76
- elif not env_name:
77
- click.echo("❌ You must specify --dev, --prod, or define SPLENT_ENV=dev|prod.")
78
- raise SystemExit(1)
79
-
80
78
  # 4️⃣ Ruta del symlink del producto (con versión incluida)
81
79
  symlink_path = os.path.join(
82
80
  workspace, product, "features", org_safe, entry_basename
@@ -0,0 +1,161 @@
1
+ """
2
+ splent feature:impact
3
+
4
+ Shows the full dependency impact of a feature within the active product:
5
+ what it requires, what depends on it, and what would break if removed.
6
+
7
+ All dependency information is resolved from the product's UVL constraints.
8
+ """
9
+
10
+ import os
11
+
12
+ import click
13
+
14
+ from splent_cli.services import context
15
+ from splent_cli.commands.feature.feature_order import (
16
+ _uvl_path,
17
+ _build_requires_map,
18
+ _build_reverse_map,
19
+ _closure,
20
+ _parse_entry,
21
+ )
22
+ from splent_framework.utils.pyproject_reader import PyprojectReader
23
+
24
+
25
+ def _short_name(pkg: str) -> str:
26
+ """splent_feature_auth → auth"""
27
+ return pkg.removeprefix("splent_feature_")
28
+
29
+
30
+ @click.command(
31
+ "feature:impact",
32
+ short_help="Show the dependency impact of adding or removing a feature.",
33
+ )
34
+ @click.argument("feature_ref", required=True)
35
+ def feature_impact(feature_ref):
36
+ """
37
+ Show what FEATURE_REF depends on and what depends on it.
38
+
39
+ Reads the product's UVL constraints to build a bidirectional dependency
40
+ graph and shows:
41
+
42
+ \b
43
+ 1. Dependencies — features this one requires to work.
44
+ 2. Dependents — features that require this one.
45
+ 3. Impact — transitive set of features that would break
46
+ if this feature were removed.
47
+
48
+ FEATURE_REF can be a short name (auth) or full package name
49
+ (splent_feature_auth).
50
+
51
+ \b
52
+ Examples:
53
+ splent feature:impact auth
54
+ splent feature:impact splent_feature_mail
55
+ """
56
+ product = context.require_app()
57
+ workspace = str(context.workspace())
58
+ product_dir = os.path.join(workspace, product)
59
+
60
+ try:
61
+ reader = PyprojectReader.for_product(product_dir)
62
+ env = os.getenv("SPLENT_ENV")
63
+ features_raw = reader.features_for_env(env)
64
+ except FileNotFoundError:
65
+ click.secho(" pyproject.toml not found.", fg="red")
66
+ raise SystemExit(1)
67
+
68
+ uvl = _uvl_path(product_dir)
69
+ if not uvl or not os.path.isfile(uvl):
70
+ click.secho(" No UVL file found — cannot compute impact.", fg="red")
71
+ raise SystemExit(1)
72
+
73
+ # Normalize input: accept "auth" or "splent_feature_auth"
74
+ target = feature_ref
75
+ if target.startswith("splent_feature_"):
76
+ target = target[len("splent_feature_") :]
77
+ pkg_target = f"splent_feature_{target}"
78
+
79
+ # Build declared feature set
80
+ declared_pkgs = set()
81
+ for entry in features_raw:
82
+ _, name, _ = _parse_entry(entry)
83
+ declared_pkgs.add(name)
84
+
85
+ if pkg_target not in declared_pkgs:
86
+ click.secho(f" '{feature_ref}' is not declared in this product.", fg="red")
87
+ raise SystemExit(1)
88
+
89
+ # Build dependency graphs
90
+ requires_map = _build_requires_map(uvl)
91
+ reverse_map = _build_reverse_map(requires_map)
92
+
93
+ # Direct dependencies (what I need)
94
+ direct_deps = requires_map.get(pkg_target, [])
95
+
96
+ # Full dependency closure (transitive: what I need, and what they need, etc.)
97
+ all_deps = _closure(pkg_target, requires_map, declared_pkgs)
98
+
99
+ # Direct dependents (who needs me)
100
+ direct_dependents = reverse_map.get(pkg_target, [])
101
+
102
+ # Full reverse closure (transitive: who breaks if I'm removed)
103
+ all_affected = _closure(pkg_target, reverse_map, declared_pkgs)
104
+
105
+ # ── Output ────────────────────────────────────────────────────────
106
+
107
+ click.echo()
108
+ click.secho(f" feature:impact — {target}", bold=True)
109
+ click.echo(click.style(f" Product: {product}", fg="bright_black"))
110
+ click.echo(click.style(f" UVL: {os.path.basename(uvl)}", fg="bright_black"))
111
+
112
+ # Section 1: What I depend on
113
+ click.echo()
114
+ click.secho(" Depends on", bold=True)
115
+
116
+ if not all_deps:
117
+ click.echo(click.style(" (none) — this feature is independent", fg="green"))
118
+ else:
119
+ direct_set = set(direct_deps)
120
+ for dep in all_deps:
121
+ marker = "direct" if dep in direct_set else "transitive"
122
+ icon = "→" if marker == "direct" else " →"
123
+ color = "cyan" if marker == "direct" else "bright_black"
124
+ click.echo(
125
+ f" {icon} {click.style(_short_name(dep), fg=color)} ({marker})"
126
+ )
127
+
128
+ # Section 2: Who depends on me + impact
129
+ click.echo()
130
+ click.secho(" Depended on by", bold=True)
131
+
132
+ if not all_affected:
133
+ click.echo(click.style(" (none) — safe to remove", fg="green"))
134
+ else:
135
+ direct_set = set(direct_dependents)
136
+ for dep in all_affected:
137
+ marker = "direct" if dep in direct_set else "transitive"
138
+ icon = "←" if marker == "direct" else " ←"
139
+ color = "red" if marker == "direct" else "yellow"
140
+ click.echo(
141
+ f" {icon} {click.style(_short_name(dep), fg=color)} ({marker})"
142
+ )
143
+
144
+ # Section 3: Summary
145
+ click.echo()
146
+ if all_affected:
147
+ click.secho(
148
+ f" Removing {target} would break {len(all_affected)} feature(s).",
149
+ fg="red",
150
+ bold=True,
151
+ )
152
+ else:
153
+ click.secho(
154
+ f" Removing {target} has no impact on other features.",
155
+ fg="green",
156
+ bold=True,
157
+ )
158
+ click.echo()
159
+
160
+
161
+ cli_command = feature_impact