splent-cli 1.40.0__tar.gz → 1.41.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 (296) hide show
  1. {splent_cli-1.40.0/src/splent_cli.egg-info → splent_cli-1.41.0}/PKG-INFO +1 -1
  2. {splent_cli-1.40.0 → splent_cli-1.41.0}/pyproject.toml +1 -1
  3. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_status.py +18 -3
  4. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_create.py +2 -1
  5. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_test.py +115 -21
  6. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_auto_require.py +24 -1
  7. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_build.py +35 -10
  8. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_configure.py +118 -6
  9. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_deploy.py +8 -6
  10. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_missing.py +26 -5
  11. splent_cli-1.41.0/src/splent_cli/commands/product/product_params.py +231 -0
  12. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_validate.py +3 -11
  13. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/uvl/uvl_utils.py +27 -0
  14. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/neutral_app.py +107 -9
  15. splent_cli-1.41.0/src/splent_cli/templates/feature/feature_services_service.py.j2 +8 -0
  16. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +7 -0
  17. splent_cli-1.41.0/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +78 -0
  18. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +7 -3
  19. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/feature_params.py +89 -0
  20. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/uvl_params.py +31 -0
  21. {splent_cli-1.40.0 → splent_cli-1.41.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  22. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli.egg-info/SOURCES.txt +2 -0
  23. splent_cli-1.40.0/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -38
  24. {splent_cli-1.40.0 → splent_cli-1.41.0}/COPYING +0 -0
  25. {splent_cli-1.40.0 → splent_cli-1.41.0}/COPYING.LESSER +0 -0
  26. {splent_cli-1.40.0 → splent_cli-1.41.0}/README.md +0 -0
  27. {splent_cli-1.40.0 → splent_cli-1.41.0}/setup.cfg +0 -0
  28. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/__init__.py +0 -0
  29. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/__main__.py +0 -0
  30. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/cli.py +0 -0
  31. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/__init__.py +0 -0
  32. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  33. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  34. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  35. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  36. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  37. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  38. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  39. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  40. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  41. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/__init__.py +0 -0
  42. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  43. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  44. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  45. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_env.py +0 -0
  46. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_features.py +0 -0
  47. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_github.py +0 -0
  48. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  49. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_product.py +0 -0
  50. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  51. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  52. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_releases.py +0 -0
  53. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_vendor.py +0 -0
  54. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  55. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  56. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  57. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  58. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/command/__init__.py +0 -0
  59. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/command/command_create.py +0 -0
  60. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/coverage.py +0 -0
  61. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_console.py +0 -0
  62. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  63. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  64. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  65. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  66. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  67. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  68. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  69. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/doctor.py +0 -0
  70. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/env/env_list.py +0 -0
  71. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/env/env_set.py +0 -0
  72. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/env/env_show.py +0 -0
  73. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/export/__init__.py +0 -0
  74. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  75. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  76. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
  77. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  78. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  79. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
  80. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  81. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
  82. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  83. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
  84. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  85. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  86. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  87. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  88. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  89. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  90. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  91. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  92. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  93. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
  94. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  95. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  96. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  97. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  98. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  99. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  100. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  101. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  102. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
  103. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_release.py +0 -0
  104. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
  105. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  106. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  107. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  108. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  109. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  110. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
  111. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  112. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  113. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  114. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/lint.py +0 -0
  115. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/locust.py +0 -0
  116. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  117. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  118. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  119. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  120. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  121. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/__init__.py +0 -0
  122. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  123. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  124. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_config.py +0 -0
  125. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_console.py +0 -0
  126. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  127. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_create.py +0 -0
  128. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  129. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  130. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_down.py +0 -0
  131. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  132. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_env.py +0 -0
  133. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_list.py +0 -0
  134. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  135. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_port.py +0 -0
  136. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_release.py +0 -0
  137. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  138. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  139. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  140. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_run.py +0 -0
  141. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_select.py +0 -0
  142. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  143. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  144. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  145. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_test.py +0 -0
  146. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_up.py +0 -0
  147. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/release/__init__.py +0 -0
  148. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/release/release_core.py +0 -0
  149. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/release/release_resume.py +0 -0
  150. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/selenium.py +0 -0
  151. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  152. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  153. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  154. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  155. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  156. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  157. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  158. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  159. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  160. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  161. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  162. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  163. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  164. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
  165. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
  166. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  167. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  168. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/version.py +0 -0
  169. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/__init__.py +0 -0
  170. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/compose.py +0 -0
  171. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/context.py +0 -0
  172. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/credentials.py +0 -0
  173. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/deploy_bundle.py +0 -0
  174. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/image_registry.py +0 -0
  175. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/index_refresh.py +0 -0
  176. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/marketplace.py +0 -0
  177. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/marketplace_api.py +0 -0
  178. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/marketplace_url.py +0 -0
  179. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/preflight.py +0 -0
  180. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/pypi_quota.py +0 -0
  181. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/registry.py +0 -0
  182. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/release.py +0 -0
  183. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/release_dry_run.py +0 -0
  184. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/release_gate.py +0 -0
  185. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/spl_store.py +0 -0
  186. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/web_container.py +0 -0
  187. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
  188. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
  189. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
  190. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
  191. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
  192. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
  193. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
  194. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
  195. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
  196. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
  197. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
  198. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
  199. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
  200. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
  201. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
  202. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
  203. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
  204. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
  205. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
  206. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
  207. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
  208. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
  209. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
  210. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
  211. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
  212. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
  213. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
  214. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
  215. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
  216. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
  217. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
  218. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
  219. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
  220. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
  221. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/licenses/COPYING +0 -0
  222. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
  223. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
  224. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
  225. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
  226. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
  227. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
  228. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
  229. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
  230. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +0 -0
  231. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
  232. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
  233. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
  234. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_400.html +0 -0
  235. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_401.html +0 -0
  236. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_404.html +0 -0
  237. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_500.html +0 -0
  238. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
  239. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
  240. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
  241. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
  242. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
  243. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_app.css +0 -0
  244. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_app.js +0 -0
  245. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
  246. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +0 -0
  247. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +0 -0
  248. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
  249. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
  250. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
  251. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
  252. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
  253. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
  254. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
  255. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
  256. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
  257. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_own.css +0 -0
  258. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
  259. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
  260. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
  261. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/__init__.py +0 -0
  262. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/archetype.py +0 -0
  263. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/cache_utils.py +0 -0
  264. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/ci_services.py +0 -0
  265. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/ci_workflow.py +0 -0
  266. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/command_loader.py +0 -0
  267. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  268. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/db_client.py +0 -0
  269. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/db_utils.py +0 -0
  270. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/decorators.py +0 -0
  271. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  272. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/feature_installer.py +0 -0
  273. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/feature_naming.py +0 -0
  274. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/feature_requirements.py +0 -0
  275. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/feature_resolution.py +0 -0
  276. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/feature_utils.py +0 -0
  277. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/forge.py +0 -0
  278. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/git_url.py +0 -0
  279. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/integrity.py +0 -0
  280. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/io_utils.py +0 -0
  281. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/lifecycle.py +0 -0
  282. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/manifest.py +0 -0
  283. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/menu_keys.py +0 -0
  284. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/migration_lock.py +0 -0
  285. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/path_utils.py +0 -0
  286. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/proc.py +0 -0
  287. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/redact.py +0 -0
  288. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/refinement_facts.py +0 -0
  289. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/running_version.py +0 -0
  290. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/template_drift.py +0 -0
  291. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/toml_edit.py +0 -0
  292. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
  293. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  294. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  295. {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli.egg-info/requires.txt +0 -0
  296. {splent_cli-1.40.0 → splent_cli-1.41.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.40.0
3
+ Version: 1.41.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
  License-Expression: LGPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.40.0"
7
+ version = "1.41.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"
@@ -58,9 +58,22 @@ def _get_filesystem_head(mdir: str) -> str | None:
58
58
  "db:status",
59
59
  short_help="Show migration status for all features.",
60
60
  )
61
+ @click.option(
62
+ "--check",
63
+ is_flag=True,
64
+ help="Exit 1 when a feature's schema is not at the head of its migrations "
65
+ "(pending, not applied, or applied from scripts that are gone).",
66
+ )
61
67
  @context.requires_product
62
- def db_status():
63
- """Show migration status: DB revision vs filesystem head for each feature."""
68
+ def db_status(check):
69
+ """Show migration status: DB revision vs filesystem head for each feature.
70
+
71
+ \b
72
+ --check is for a start that must not serve on a schema older than its
73
+ code: the product's entrypoint runs it with SPLENT_MIGRATE_ON_START=check,
74
+ when a deploy agent migrates in a step of its own. An orphan (a feature
75
+ no longer declared) does not count.
76
+ """
64
77
  app = current_app
65
78
 
66
79
  click.echo(click.style("\n📊 Migration Status\n", fg="cyan", bold=True))
@@ -151,7 +164,7 @@ def db_status():
151
164
  if issues:
152
165
  click.secho(
153
166
  f" {issues} feature(s) out of sync. Run 'splent db:upgrade' to apply pending migrations.",
154
- fg="yellow",
167
+ fg="red" if check else "yellow",
155
168
  )
156
169
  click.echo()
157
170
  if orphans:
@@ -164,6 +177,8 @@ def db_status():
164
177
  fg="red",
165
178
  )
166
179
  click.echo()
180
+ if check and issues:
181
+ raise SystemExit(1)
167
182
 
168
183
 
169
184
  cli_command = db_status
@@ -276,7 +276,8 @@ def _build_file_map(feature_type, short_name, docker=False, js=False):
276
276
  src = {
277
277
  "__init__.py": f"{T}feature_init_service.py.j2",
278
278
  "config.py": f"{T}feature_config.py.j2",
279
- "services.py": f"{T}feature_services.py.j2",
279
+ # No models, so no repository for the service to wrap.
280
+ "services.py": f"{T}feature_services_service.py.j2",
280
281
  "signals.py": f"{T}feature_signals.py.j2",
281
282
  "commands.py": f"{T}feature_commands.py.j2",
282
283
  }
@@ -46,12 +46,15 @@ def _tied_to_the_cli(command: str, lock: str = TEST_LOCK) -> str:
46
46
  The CLI holds the exec's stdin open while it runs (see
47
47
  _delegate_to_web_container). When the CLI goes, stdin closes, and the
48
48
  watcher stops the whole run. A run that finds another one holding the
49
- lock waits for it instead of running beside it.
49
+ lock waits for it instead of running beside it. The lock is opened for
50
+ reading, which flock needs no more than: a run as the workspace's user
51
+ shares the file a run as root created.
50
52
  """
51
53
  return f"""set -m
52
54
  exec 3<&0
53
55
  if command -v flock >/dev/null 2>&1; then
54
- exec 9>{lock}
56
+ [ -e {lock} ] || : > {lock}
57
+ exec 9<{lock}
55
58
  if ! flock -n 9; then
56
59
  echo " Another test run is using this product's test database. Waiting for it to finish..."
57
60
  flock 9
@@ -84,13 +87,78 @@ def _delegate_to_web_container(argv: list[str]) -> None:
84
87
  )
85
88
 
86
89
 
90
+ def workspace_user(container: str) -> list[str]:
91
+ """The docker exec options that run a command in CONTAINER as the user
92
+ who owns the workspace.
93
+
94
+ An exec runs as the image's user, root in a development image, and a
95
+ test run writes into the working copies: bytecode, .pytest_cache, what
96
+ the tests leave. As root they stayed root's, and the next release of
97
+ the feature could not rewrite them. The owner is read from the
98
+ workspace's files, whoever this process runs as. A development image
99
+ has no account for that uid as a rule; Docker runs a bare uid all the
100
+ same, and the exec then names it in USER and LOGNAME, which
101
+ getpass.getuser() reads before the account it does not find (pymysql
102
+ asks it), with HOME a directory of its own under /tmp. A workspace root
103
+ owns stays root's, and so does a container where the account cannot be
104
+ looked up.
105
+ """
106
+ owner = _workspace_owner()
107
+ if owner is None or owner[0] == 0:
108
+ return []
109
+ uid, gid = owner
110
+ found = run(
111
+ ["docker", "exec", container, "getent", "passwd", str(uid)],
112
+ check=False,
113
+ capture=True,
114
+ )
115
+ if found.returncode not in (0, 2):
116
+ # 2 is getent's "no such entry"; anything else is a container that
117
+ # cannot answer, where the exec keeps the image's user.
118
+ return []
119
+ options = ["-u", f"{uid}:{gid}"]
120
+ fields = (found.stdout or "").strip().split(":")
121
+ if found.returncode != 0 or len(fields) < 6 or not fields[5]:
122
+ user = _account_name(uid)
123
+ options += [
124
+ "-e",
125
+ f"HOME=/tmp/splent-home-{uid}",
126
+ "-e",
127
+ f"USER={user}",
128
+ "-e",
129
+ f"LOGNAME={user}",
130
+ ]
131
+ return options
132
+
133
+
134
+ def _account_name(uid: int) -> str:
135
+ """The name this machine gives UID, ``splent`` when it gives none."""
136
+ import pwd
137
+
138
+ try:
139
+ return pwd.getpwuid(uid).pw_name
140
+ except KeyError:
141
+ return "splent"
142
+
143
+
144
+ def _workspace_owner() -> tuple[int, int] | None:
145
+ """``(uid, gid)`` of the workspace's root directory, or None."""
146
+ try:
147
+ owner = context.workspace().stat()
148
+ except OSError:
149
+ return None
150
+ return owner.st_uid, owner.st_gid
151
+
152
+
87
153
  def run_tests_in_web_container(product: str, argv: list[str]) -> int:
88
154
  """Run ``splent <argv>`` in PRODUCT's web container, as a test run.
89
155
 
90
156
  Returns its exit code, or 1 when the container is not running. The run
91
157
  waits for any other test run in that container and stops when this
92
- process does (see :func:`_tied_to_the_cli`). feature:test runs there
93
- this way, and so do the product's own tests (product:test).
158
+ process does (see :func:`_tied_to_the_cli`), and runs as the workspace's
159
+ owner (:func:`workspace_user`). feature:test runs there this way, and so
160
+ do the product's own tests (product:test) and the release gate, which
161
+ runs feature:test.
94
162
  """
95
163
  name = web_container.name(product)
96
164
  if not _container_running(name):
@@ -121,6 +189,7 @@ def run_tests_in_web_container(product: str, argv: list[str]) -> int:
121
189
  "docker",
122
190
  "exec",
123
191
  "-i",
192
+ *workspace_user(name),
124
193
  "-e",
125
194
  f"SPLENT_APP={product}",
126
195
  *passthrough,
@@ -694,15 +763,19 @@ def _run_neutral(
694
763
  problem = neutral_app.database_problem(dict(os.environ))
695
764
  if problem:
696
765
  _neutral_refusal(problem)
697
- missing = neutral_app.missing_distributions(members)
698
- if missing:
699
- product = context.require_app()
766
+ product = context.require_app()
767
+ unmet = neutral_app.unmet_requirements(members)
768
+ from_workspace = [str(u) for u in unmet if not u.installable]
769
+ if from_workspace:
770
+ # The framework, the CLI and the features are the workspace's
771
+ # copies, which a run never replaces with a release.
700
772
  _neutral_refusal(
701
- f"The neutral app needs packages that {product}'s image does not\n"
702
- " have at the version asked for:\n"
703
- + "\n".join(f" {line}" for line in missing)
704
- + "\n Install them in the container, or run the tests in a product\n"
705
- " that declares the feature."
773
+ f"The neutral app needs SPLENT packages that {product}'s container\n"
774
+ " does not have at the version asked for:\n"
775
+ + "\n".join(f" {line}" for line in from_workspace)
776
+ + "\n The container has the workspace's copies of them. Update the\n"
777
+ " working copy, or run the tests in a product that declares the\n"
778
+ " feature."
706
779
  )
707
780
 
708
781
  test_paths = _collect_test_paths([(feature.name, feature.root)])
@@ -710,15 +783,33 @@ def _run_neutral(
710
783
  click.secho("⚠️ No test directories found.", fg="yellow")
711
784
  return
712
785
 
713
- click.secho(
714
- f"\n🧪 Running tests for {feature_ref} in a neutral app "
715
- f"[{', '.join(levels)}]...",
716
- fg="cyan",
717
- )
718
786
  with tempfile.TemporaryDirectory(prefix="splent-neutral-") as tmp:
787
+ venv_dir = None
788
+ if unmet:
789
+ click.echo(
790
+ f" {product}'s image lacks what these need, so the run installs "
791
+ "it in a\n virtual environment of its own, removed after it:"
792
+ )
793
+ for line in unmet:
794
+ click.echo(f" {line}")
795
+ try:
796
+ venv_dir = neutral_app.install_requirements(Path(tmp), unmet)
797
+ except neutral_app.NeutralAppError as e:
798
+ _neutral_refusal(
799
+ f"{e}\n Install them in the container, or run the tests "
800
+ "in a product\n that declares the feature."
801
+ )
802
+ click.secho(
803
+ f"\n🧪 Running tests for {feature_ref} in a neutral app "
804
+ f"[{', '.join(levels)}]...",
805
+ fg="cyan",
806
+ )
719
807
  neutral_app.write_app(Path(tmp), members)
720
- env = neutral_app.environment(Path(tmp), members)
721
- _run_pytest(test_paths, levels, keyword, verbose, environment=env)
808
+ env = neutral_app.environment(Path(tmp), members, venv_dir=venv_dir)
809
+ python = str(venv_dir / "bin" / "python") if venv_dir else None
810
+ _run_pytest(
811
+ test_paths, levels, keyword, verbose, environment=env, python=python
812
+ )
722
813
 
723
814
 
724
815
  def _resolve_levels(unit, integration, functional, e2e, load) -> tuple[str, ...]:
@@ -744,12 +835,15 @@ def _run_pytest(
744
835
  verbose: bool,
745
836
  extra_sources: list[tuple[str, Path]] = (),
746
837
  environment: dict | None = None,
838
+ python: str | None = None,
747
839
  ):
748
840
  """Run each feature's tests, level by level, and exit 1 if any failed.
749
841
 
750
842
  ENVIRONMENT, when given, is the whole environment of every run (the
751
843
  neutral app's). Otherwise each run gets this process's, with the import
752
- path and the feature sources of the active product.
844
+ path and the feature sources of the active product. PYTHON, when given,
845
+ is the interpreter that runs pytest (the neutral app's virtual
846
+ environment).
753
847
  """
754
848
  passed = failed = skipped = 0
755
849
  for pkg, test_dir, src_dir in test_paths:
@@ -790,7 +884,7 @@ def _run_pytest(
790
884
  )
791
885
 
792
886
  cmd = [
793
- "pytest",
887
+ *([python, "-m", "pytest"] if python else ["pytest"]),
794
888
  str(level_dir),
795
889
  "--rootdir=.",
796
890
  "-W",
@@ -15,6 +15,8 @@ from splent_cli.commands.uvl.uvl_utils import (
15
15
  list_all_features_from_uvl as _list_all_features_from_uvl,
16
16
  extract_implications_from_uvl_text as _extract_implications_from_uvl_text,
17
17
  print_uvl_header as _print_uvl_header,
18
+ parameter_nodes_of as _parameter_nodes_of,
19
+ parameters_to_set as _parameters_to_set,
18
20
  )
19
21
  from splent_cli.utils.ci_workflow import keeps_ci_workflows
20
22
 
@@ -172,10 +174,17 @@ def product_complete(pyproject, default_org, yes, dry_run):
172
174
 
173
175
  # current selection from pyproject: the one product:validate checks, the
174
176
  # base list plus the list of the current environment (dev when unset)
175
- deps = read_features_from_data(data, context.resolve_env())
177
+ env = context.resolve_env()
178
+ deps = read_features_from_data(data, env)
176
179
  selected_names = {_normalize_feature_name(d) for d in deps}
177
180
  selected_names.add(root_name) # root always selected
178
181
 
182
+ # The nodes the product's values select count as selected (P7), and a
183
+ # parameter node a constraint asks for is a value to set in
184
+ # [tool.splent.config]: it has no package, and is never added.
185
+ nodes, chosen = _parameter_nodes_of(workspace, data, local_uvl, selected_names, env)
186
+ selected_names |= set(chosen)
187
+
179
188
  # compute missing via implications
180
189
  pairs = _extract_implications_from_uvl_text(uvl_text)
181
190
  req_graph = _build_req_graph(pairs)
@@ -185,6 +194,18 @@ def product_complete(pyproject, default_org, yes, dry_run):
185
194
 
186
195
  # filter out root just in case
187
196
  missing_names = [f for f in missing_names if f != root_name]
197
+ missing_values = [f for f in missing_names if f in nodes]
198
+ missing_names = [f for f in missing_names if f not in nodes]
199
+
200
+ def say_values():
201
+ if missing_values:
202
+ to_set = _parameters_to_set(missing_values, nodes, workspace, data, env)
203
+ click.secho(
204
+ "Not added, values to set in [tool.splent.config]: "
205
+ + ", ".join(to_set),
206
+ fg="yellow",
207
+ )
208
+ click.echo()
188
209
 
189
210
  # sanity
190
211
  unknown_missing = sorted([f for f in missing_names if f not in universe_set])
@@ -195,6 +216,7 @@ def product_complete(pyproject, default_org, yes, dry_run):
195
216
 
196
217
  if not missing_names:
197
218
  _print_uvl_header("complete", app_name, local_uvl, len(universe))
219
+ say_values()
198
220
  click.echo("OK: nothing to add (no missing dependencies).")
199
221
  click.echo()
200
222
  return
@@ -213,6 +235,7 @@ def product_complete(pyproject, default_org, yes, dry_run):
213
235
  to_add_specs.append(spec)
214
236
 
215
237
  _print_uvl_header("complete", app_name, local_uvl, len(universe))
238
+ say_values()
216
239
  click.echo("Missing features (by name):")
217
240
  for f in missing_names:
218
241
  click.echo(f"- {f}")
@@ -824,16 +824,34 @@ def _reset_features_dir(docker_path: str) -> None:
824
824
  # with a one-shot service must declare restart: "no", quoted.
825
825
  RESTART_POLICY = "unless-stopped"
826
826
 
827
- # The web container's health probe. Python is the one interpreter every
828
- # production image has, so the probe needs no extra package (curl is not
829
- # guaranteed). Any answer below 500 counts as healthy: a product without a
830
- # public home answers 302 or 404 at "/", and that still proves gunicorn and
831
- # the app are serving. A refused connection, a timeout or a 5xx does not.
827
+ # The web container's health probe, which product:deploy waits on too.
828
+ # Python is the one interpreter every production image has, so the probe
829
+ # needs no extra package (curl is not guaranteed). It asks the framework's
830
+ # /readyz (splent_framework 1.21 and later): 200 when the database and the
831
+ # services the product declares answer, 503 when they do not. The framework
832
+ # answers it before the app's own code runs, so TRUSTED_HOSTS never sees
833
+ # it. "/" did not do: with TRUSTED_HOSTS set it answers 400 to a request
834
+ # for 127.0.0.1, and a broken database could still serve a cached page.
835
+ # Any other answer at /readyz comes from the app's own routes: a framework
836
+ # older than 1.21, or a product that turned the probe off. That app is
837
+ # healthy when "/" answers below 500, the probe it had before.
832
838
  WEB_HEALTH_PROBE = (
839
+ "import http.client as h\n"
840
+ "def status(path):\n"
841
+ " c = h.HTTPConnection('127.0.0.1', 5000, timeout=4)\n"
842
+ " c.request('GET', path)\n"
843
+ " return c.getresponse().status\n"
844
+ "ready = status('/readyz')\n"
845
+ "ok = ready == 200 if ready in (200, 503) else status('/') < 500\n"
846
+ "raise SystemExit(not ok)\n"
847
+ )
848
+ # The probe CLIs before 1.41 wrote, which product:build replaces with the
849
+ # one above: a product created with them has it in its prod compose file.
850
+ LEGACY_WEB_HEALTH_PROBES = (
833
851
  "import http.client as h; "
834
852
  "c = h.HTTPConnection('127.0.0.1', 5000, timeout=4); "
835
853
  "c.request('GET', '/'); "
836
- "raise SystemExit(c.getresponse().status >= 500)"
854
+ "raise SystemExit(c.getresponse().status >= 500)",
837
855
  )
838
856
  WEB_HEALTHCHECK = {
839
857
  "test": ["CMD", "python3", "-c", WEB_HEALTH_PROBE],
@@ -906,9 +924,11 @@ def local_or_registry_image(product: str) -> str:
906
924
 
907
925
  def apply_production_policies(compose_data: dict, product: str) -> dict:
908
926
  """Give every deployed service a restart policy, and the web and database
909
- services a healthcheck, unless the compose file already set one. The web
910
- image becomes ``${SPLENT_IMAGE:-<product>:latest}``, so a host can run the
911
- image a release published instead of building its own.
927
+ services a healthcheck, unless the compose file already set one. A web
928
+ healthcheck that is the probe an older CLI wrote (it asked "/") is
929
+ brought up to WEB_HEALTH_PROBE. The web image becomes
930
+ ``${SPLENT_IMAGE:-<product>:latest}``, so a host can run the image a
931
+ release published instead of building its own.
912
932
 
913
933
  Applied by product:build to the merged deploy compose, so every product
914
934
  gets it on its next build, whatever its docker-compose.prod.yml says and
@@ -924,7 +944,12 @@ def apply_production_policies(compose_data: dict, product: str) -> dict:
924
944
  None,
925
945
  )
926
946
  if web_name and isinstance(services[web_name], dict):
927
- services[web_name].setdefault("healthcheck", dict(WEB_HEALTHCHECK))
947
+ healthcheck = services[web_name].setdefault(
948
+ "healthcheck", dict(WEB_HEALTHCHECK)
949
+ )
950
+ test = healthcheck.get("test") if isinstance(healthcheck, dict) else None
951
+ if isinstance(test, list) and test and test[-1] in LEGACY_WEB_HEALTH_PROBES:
952
+ healthcheck["test"] = list(WEB_HEALTHCHECK["test"])
928
953
  # The local build, unless deploy/.env names a registry image. Only the
929
954
  # plain local name is rewritten: an image the product chose on purpose
930
955
  # is left alone.
@@ -26,9 +26,11 @@ from dataclasses import dataclass, field
26
26
 
27
27
  import click
28
28
 
29
+ from splent_cli.commands.uvl.uvl_utils import ModelTree
29
30
  from splent_cli.services import context
30
31
  from splent_cli.utils.feature_utils import normalize_namespace
31
32
  from splent_cli.utils.ci_workflow import keeps_ci_workflows
33
+ from splent_cli.utils.uvl_params import PARAM, VALUE
32
34
 
33
35
 
34
36
  # ═══════════════════════════════════════════════════════════════════
@@ -47,11 +49,19 @@ class SPLFeature:
47
49
  groups: list[SPLGroup] = field(default_factory=list)
48
50
  card_min: int = 1 # feature cardinality lower bound
49
51
  card_max: int = 1 # feature cardinality upper bound
52
+ attributes: dict[str, str] = field(default_factory=dict)
50
53
 
51
54
  @property
52
55
  def is_abstract(self) -> bool:
53
56
  return not self.package
54
57
 
58
+ @property
59
+ def is_parameter(self) -> bool:
60
+ """Whether the node stands for a parameter or one of its values (P7,
61
+ splent_cli.utils.uvl_params): the product's [tool.splent.config]
62
+ selects it, and it is never offered as a feature."""
63
+ return bool({PARAM, VALUE} & set(self.attributes))
64
+
55
65
 
56
66
  @dataclass
57
67
  class SPLGroup:
@@ -157,6 +167,18 @@ class SPLModel:
157
167
  stack.append(ch)
158
168
  return result
159
169
 
170
+ def is_parameter(self, name: str) -> bool:
171
+ f = self.features.get(name)
172
+ return bool(f and f.is_parameter)
173
+
174
+ def tree(self) -> ModelTree:
175
+ """The model as uvl_params reads it: parents and attributes."""
176
+ return ModelTree(
177
+ root=self.root_name,
178
+ parents={n: f.parent for n, f in self.features.items() if f.parent},
179
+ attributes={n: dict(f.attributes) for n, f in self.features.items()},
180
+ )
181
+
160
182
  def groups_needing_selection(self) -> list[tuple[str, SPLGroup]]:
161
183
  """Return (parent_name, group) pairs that require interactive selection.
162
184
 
@@ -305,6 +327,7 @@ def _load_spl_model(uvl_path: str, spl_name: str) -> SPLModel:
305
327
  org=attrs.get("org", "splent-io"),
306
328
  package=attrs.get("package", ""),
307
329
  parent=parent_name,
330
+ attributes=attrs,
308
331
  )
309
332
  model.features[node.name] = feat
310
333
 
@@ -376,6 +399,10 @@ def propagate(
376
399
  Backward:
377
400
  4. A selected ∧ (A excludes B) → B excluded
378
401
  5. B excluded ∧ (A ⇒ B) → A excluded (contrapositive)
402
+
403
+ A parameter node B is never selected by rule 1: only a value of the
404
+ parameter selects it (:func:`_parameter_selection`), and the solver
405
+ says when the values do not.
379
406
  """
380
407
  result = set(selected) | mandatory
381
408
  excl = set(excluded) if excluded else set()
@@ -386,8 +413,8 @@ def propagate(
386
413
  # Forward implication
387
414
  for c in model.constraints:
388
415
  if c.kind == "implies" and c.source in result and c.target not in result:
389
- if c.target in excl:
390
- continue # conflict — SAT validation will catch
416
+ if c.target in excl or model.is_parameter(c.target):
417
+ continue # conflict, or a value to set — SAT validation will catch
391
418
  result.add(c.target)
392
419
  changed = True
393
420
 
@@ -649,7 +676,9 @@ def _propagate_and_report(
649
676
  """Run propagation, update sets in-place, and report changes."""
650
677
  new_sel, new_excl = propagate(selected, model, mandatory, excluded)
651
678
 
652
- auto = new_sel - selected - mandatory
679
+ # A parameter's node (one that holds its choices, as a mandatory child)
680
+ # is not news: its value is the product's to set.
681
+ auto = {n for n in new_sel - selected - mandatory if not model.is_parameter(n)}
653
682
  for name in sorted(auto):
654
683
  click.echo(
655
684
  f"{prefix}\u21b3 {name}" + click.style(" (auto-selected)", fg="cyan")
@@ -708,6 +737,9 @@ def _configure_subtree(
708
737
  # Only prompt if this feature is active
709
738
  if name not in selected and name not in mandatory:
710
739
  continue
740
+ # The values of a parameter: [tool.splent.config] picks them.
741
+ if all(model.is_parameter(ch) for ch in group.children):
742
+ continue
711
743
 
712
744
  label_map = {
713
745
  "alternative": "pick exactly one",
@@ -761,7 +793,10 @@ def _configure_subtree(
761
793
  has_optionals = any(
762
794
  g.group_type == "optional"
763
795
  for g in feat.groups
764
- if any(ch not in mandatory and ch not in selected for ch in g.children)
796
+ if any(
797
+ ch not in mandatory and ch not in selected and not model.is_parameter(ch)
798
+ for ch in g.children
799
+ )
765
800
  )
766
801
  showed_header = False
767
802
 
@@ -769,6 +804,10 @@ def _configure_subtree(
769
804
  if group.group_type != "optional":
770
805
  continue
771
806
  for ch in group.children:
807
+ # A parameter, which [tool.splent.config] sets, not a feature.
808
+ if model.is_parameter(ch):
809
+ continue
810
+
772
811
  # Already handled by mandatory set
773
812
  if ch in mandatory:
774
813
  _configure_subtree(ch, selected, excluded, model, mandatory, indent)
@@ -832,6 +871,55 @@ def _configure_subtree(
832
871
  # ═══════════════════════════════════════════════════════════════════
833
872
 
834
873
 
874
+ def _feature_root(workspace: str, org: str, package: str) -> str | None:
875
+ """The copy of a feature this workspace has: its working copy at the
876
+ root, else its newest release in the cache, else None."""
877
+ from splent_cli.services.neutral_app import cache_dir, cached_versions
878
+
879
+ root = os.path.join(workspace, package)
880
+ if os.path.isfile(os.path.join(root, "pyproject.toml")):
881
+ return root
882
+ namespace = normalize_namespace(org)
883
+ for version in cached_versions(workspace, namespace, package):
884
+ return str(cache_dir(workspace, namespace, package, version))
885
+ return None
886
+
887
+
888
+ def _parameter_selection(
889
+ workspace: str, data: dict, model: SPLModel, selected: set[str]
890
+ ) -> dict[str, str]:
891
+ """The parameter nodes the product's values select (P7), each with what
892
+ selected it, for the features in ``selected``.
893
+
894
+ The values are the product's [tool.splent.config], else each feature's
895
+ default, read from the copy of the feature this workspace has. The
896
+ configurator asks for none of them: [tool.splent.config] is where they
897
+ change, and product:params shows them.
898
+ """
899
+ from splent_framework.params.schema import read_pyproject
900
+
901
+ from splent_cli.utils.uvl_params import selection
902
+
903
+ params: dict = {}
904
+ for name in sorted(selected):
905
+ feat = model.features.get(name)
906
+ if not feat or feat.is_abstract:
907
+ continue
908
+ root = _feature_root(workspace, feat.org, feat.package)
909
+ if root is None:
910
+ continue
911
+ try:
912
+ with open(os.path.join(root, "pyproject.toml"), "rb") as f:
913
+ declared = read_pyproject(feat.package, tomllib.load(f))
914
+ except (OSError, tomllib.TOMLDecodeError):
915
+ continue
916
+ for key, param in declared.params.items():
917
+ params.setdefault(key, param)
918
+ splent = data.get("tool", {}).get("splent", {})
919
+ config = splent.get("config", {}) if isinstance(splent, dict) else {}
920
+ return selection(model.tree(), params, config if isinstance(config, dict) else {})
921
+
922
+
835
923
  def _read_contract_env(
836
924
  workspace: str,
837
925
  cache_dir: str,
@@ -939,7 +1027,7 @@ def product_configure():
939
1027
  # ── Show mandatory leaf features ───────────────────────
940
1028
  click.secho(" Mandatory (auto-selected):", fg="green")
941
1029
  for name in sorted(mandatory):
942
- if name == model.root_name:
1030
+ if name == model.root_name or model.is_parameter(name):
943
1031
  continue
944
1032
  feat = model.features[name]
945
1033
  has_selection_group = any(
@@ -955,6 +1043,15 @@ def product_configure():
955
1043
  # ── Build final selection ──────────────────────────────
956
1044
  all_selected, _ = propagate(selected, model, mandatory, excluded)
957
1045
 
1046
+ # ── The parameter nodes the product's values select ────
1047
+ # They count as selected, as product:validate counts them, and what
1048
+ # they imply comes with them (a home ordered by rating needs rating).
1049
+ chosen = _parameter_selection(workspace, data, model, all_selected)
1050
+ if chosen:
1051
+ all_selected, _ = propagate(
1052
+ all_selected | set(chosen), model, mandatory, excluded
1053
+ )
1054
+
958
1055
  # ── Summary ────────────────────────────────────────────
959
1056
  click.echo()
960
1057
  click.secho(" \u2500\u2500\u2500 Configuration \u2500\u2500\u2500", bold=True)
@@ -968,7 +1065,9 @@ def product_configure():
968
1065
  pass # skip root display
969
1066
  elif name in all_selected:
970
1067
  tag = ""
971
- if name in mandatory:
1068
+ if name in chosen:
1069
+ tag = click.style(f" ({chosen[name]})", fg="cyan")
1070
+ elif name in mandatory:
972
1071
  tag = click.style(" (mandatory)", fg="bright_black")
973
1072
  click.echo(f"{pfx}\u2713 {name}{tag}")
974
1073
  else:
@@ -980,6 +1079,13 @@ def product_configure():
980
1079
 
981
1080
  render_tree(model.root_name)
982
1081
  click.echo()
1082
+ if any(f.is_parameter for f in model.features.values()):
1083
+ click.secho(
1084
+ " The nodes of parameters follow the product's [tool.splent.config], "
1085
+ "or each feature's\n default. splent product:params shows the values.",
1086
+ fg="bright_black",
1087
+ )
1088
+ click.echo()
983
1089
 
984
1090
  # ── Check excludes ─────────────────────────────────────
985
1091
  violations = check_excludes(all_selected, model)
@@ -1013,6 +1119,12 @@ def product_configure():
1013
1119
  if not ok:
1014
1120
  click.secho("UNSATISFIABLE", fg="red", bold=True)
1015
1121
  click.echo(" The selected configuration violates UVL constraints.")
1122
+ if any(f.is_parameter for f in model.features.values()):
1123
+ click.echo(
1124
+ " A value of a parameter may need a feature left out, or a feature "
1125
+ "a value\n [tool.splent.config] does not set: splent product:params "
1126
+ "shows the values."
1127
+ )
1016
1128
  raise SystemExit(1)
1017
1129
 
1018
1130
  click.secho("satisfiable", fg="green", bold=True)