splent-cli 1.40.0__tar.gz → 1.42.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 (297) hide show
  1. {splent_cli-1.40.0/src/splent_cli.egg-info → splent_cli-1.42.0}/PKG-INFO +1 -1
  2. {splent_cli-1.40.0 → splent_cli-1.42.0}/pyproject.toml +1 -1
  3. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/cli.py +93 -34
  4. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_status.py +18 -3
  5. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_create.py +2 -1
  6. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_test.py +122 -22
  7. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_auto_require.py +24 -1
  8. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_build.py +35 -10
  9. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_commands.py +71 -37
  10. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_configure.py +118 -6
  11. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_deploy.py +8 -6
  12. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_missing.py +26 -5
  13. splent_cli-1.42.0/src/splent_cli/commands/product/product_params.py +231 -0
  14. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_test.py +74 -4
  15. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_validate.py +3 -11
  16. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/uvl/uvl_utils.py +27 -0
  17. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/neutral_app.py +107 -9
  18. splent_cli-1.42.0/src/splent_cli/services/own_commands.py +76 -0
  19. splent_cli-1.42.0/src/splent_cli/templates/feature/feature_services_service.py.j2 +8 -0
  20. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +7 -0
  21. splent_cli-1.42.0/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +78 -0
  22. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +7 -3
  23. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/feature_params.py +89 -0
  24. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/uvl_params.py +31 -0
  25. {splent_cli-1.40.0 → splent_cli-1.42.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  26. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli.egg-info/SOURCES.txt +3 -0
  27. splent_cli-1.40.0/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -38
  28. {splent_cli-1.40.0 → splent_cli-1.42.0}/COPYING +0 -0
  29. {splent_cli-1.40.0 → splent_cli-1.42.0}/COPYING.LESSER +0 -0
  30. {splent_cli-1.40.0 → splent_cli-1.42.0}/README.md +0 -0
  31. {splent_cli-1.40.0 → splent_cli-1.42.0}/setup.cfg +0 -0
  32. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/__init__.py +0 -0
  33. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/__main__.py +0 -0
  34. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/__init__.py +0 -0
  35. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  36. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  37. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  38. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  39. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  40. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  41. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  42. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  43. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  44. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/__init__.py +0 -0
  45. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  46. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  47. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  48. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_env.py +0 -0
  49. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_features.py +0 -0
  50. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_github.py +0 -0
  51. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  52. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_product.py +0 -0
  53. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  54. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  55. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_releases.py +0 -0
  56. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/check/check_vendor.py +0 -0
  57. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  58. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  59. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  60. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  61. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/command/__init__.py +0 -0
  62. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/command/command_create.py +0 -0
  63. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/coverage.py +0 -0
  64. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_console.py +0 -0
  65. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  66. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  67. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  68. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  69. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  70. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  71. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  72. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/doctor.py +0 -0
  73. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/env/env_list.py +0 -0
  74. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/env/env_set.py +0 -0
  75. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/env/env_show.py +0 -0
  76. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/export/__init__.py +0 -0
  77. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  78. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  79. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
  80. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  81. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  82. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
  83. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  84. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
  85. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  86. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
  87. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  88. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  89. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  90. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  91. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  92. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  93. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  94. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  95. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  96. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
  97. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  98. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  99. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  100. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  101. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  102. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  103. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  104. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  105. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
  106. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_release.py +0 -0
  107. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
  108. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  109. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  110. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  111. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  112. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  113. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
  114. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  115. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  116. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  117. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/lint.py +0 -0
  118. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/locust.py +0 -0
  119. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  120. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  121. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  122. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  123. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  124. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/__init__.py +0 -0
  125. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  126. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_config.py +0 -0
  127. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_console.py +0 -0
  128. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  129. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_create.py +0 -0
  130. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  131. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  132. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_down.py +0 -0
  133. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  134. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_env.py +0 -0
  135. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_list.py +0 -0
  136. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  137. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_port.py +0 -0
  138. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_release.py +0 -0
  139. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  140. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  141. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  142. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_run.py +0 -0
  143. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_select.py +0 -0
  144. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  145. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  146. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  147. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/product/product_up.py +0 -0
  148. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/release/__init__.py +0 -0
  149. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/release/release_core.py +0 -0
  150. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/release/release_resume.py +0 -0
  151. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/selenium.py +0 -0
  152. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  153. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  154. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  155. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  156. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  157. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  158. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  159. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  160. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  161. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  162. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  163. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  164. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  165. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
  166. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
  167. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  168. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  169. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/commands/version.py +0 -0
  170. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/__init__.py +0 -0
  171. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/compose.py +0 -0
  172. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/context.py +0 -0
  173. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/credentials.py +0 -0
  174. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/deploy_bundle.py +0 -0
  175. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/image_registry.py +0 -0
  176. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/index_refresh.py +0 -0
  177. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/marketplace.py +0 -0
  178. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/marketplace_api.py +0 -0
  179. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/marketplace_url.py +0 -0
  180. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/preflight.py +0 -0
  181. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/pypi_quota.py +0 -0
  182. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/registry.py +0 -0
  183. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/release.py +0 -0
  184. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/release_dry_run.py +0 -0
  185. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/release_gate.py +0 -0
  186. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/spl_store.py +0 -0
  187. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/services/web_container.py +0 -0
  188. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
  189. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
  190. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
  191. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
  192. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
  193. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
  194. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
  195. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
  196. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
  197. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
  198. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
  199. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
  200. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
  201. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
  202. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
  203. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
  204. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
  205. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
  206. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
  207. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
  208. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
  209. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
  210. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
  211. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
  212. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
  213. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
  214. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
  215. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
  216. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
  217. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
  218. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
  219. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
  220. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
  221. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
  222. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/licenses/COPYING +0 -0
  223. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
  224. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
  225. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
  226. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
  227. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
  228. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
  229. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
  230. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
  231. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +0 -0
  232. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
  233. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
  234. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
  235. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_400.html +0 -0
  236. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_401.html +0 -0
  237. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_404.html +0 -0
  238. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_500.html +0 -0
  239. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
  240. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
  241. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
  242. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
  243. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
  244. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_app.css +0 -0
  245. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_app.js +0 -0
  246. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
  247. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +0 -0
  248. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +0 -0
  249. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
  250. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
  251. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
  252. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
  253. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
  254. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
  255. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
  256. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
  257. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
  258. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_own.css +0 -0
  259. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
  260. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
  261. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
  262. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/__init__.py +0 -0
  263. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/archetype.py +0 -0
  264. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/cache_utils.py +0 -0
  265. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/ci_services.py +0 -0
  266. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/ci_workflow.py +0 -0
  267. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/command_loader.py +0 -0
  268. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  269. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/db_client.py +0 -0
  270. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/db_utils.py +0 -0
  271. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/decorators.py +0 -0
  272. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  273. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/feature_installer.py +0 -0
  274. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/feature_naming.py +0 -0
  275. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/feature_requirements.py +0 -0
  276. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/feature_resolution.py +0 -0
  277. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/feature_utils.py +0 -0
  278. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/forge.py +0 -0
  279. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/git_url.py +0 -0
  280. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/integrity.py +0 -0
  281. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/io_utils.py +0 -0
  282. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/lifecycle.py +0 -0
  283. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/manifest.py +0 -0
  284. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/menu_keys.py +0 -0
  285. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/migration_lock.py +0 -0
  286. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/path_utils.py +0 -0
  287. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/proc.py +0 -0
  288. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/redact.py +0 -0
  289. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/refinement_facts.py +0 -0
  290. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/running_version.py +0 -0
  291. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/template_drift.py +0 -0
  292. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/toml_edit.py +0 -0
  293. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
  294. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  295. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  296. {splent_cli-1.40.0 → splent_cli-1.42.0}/src/splent_cli.egg-info/requires.txt +0 -0
  297. {splent_cli-1.40.0 → splent_cli-1.42.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.42.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.42.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"
@@ -1,3 +1,4 @@
1
+ import contextlib
1
2
  import logging
2
3
  import os
3
4
 
@@ -20,6 +21,27 @@ def _missing_module(exc: BaseException) -> str | None:
20
21
  return None
21
22
 
22
23
 
24
+ @contextlib.contextmanager
25
+ def _quiet_unless(debug: bool):
26
+ """The framework's logging muted while the app is built for discovery.
27
+
28
+ Discovery runs before every command, including the ones a product that
29
+ is not built yet (selected, not configured or derived) runs while
30
+ getting there. Building its app then fails, and the framework logged
31
+ that failure with a full traceback on every single command, although
32
+ nothing was wrong. The failure is expected there and handled, so the
33
+ framework's logging is muted for the attempt.
34
+ """
35
+ if debug:
36
+ yield
37
+ return
38
+ logging.disable(logging.CRITICAL)
39
+ try:
40
+ yield
41
+ finally:
42
+ logging.disable(logging.NOTSET)
43
+
44
+
23
45
  class SPLENTCLI(click.Group):
24
46
  """
25
47
  Main SPLENT CLI class.
@@ -27,6 +49,7 @@ class SPLENTCLI(click.Group):
27
49
  - Automatically injects the Flask app context for commands marked with `requires_app = True`.
28
50
  - Checks DB connectivity for commands marked with `requires_db = True`.
29
51
  - Discovers CLI commands contributed by features via ``app.extensions["splent_feature_commands"]``.
52
+ - Discovers the product's own commands, ``product:<name>`` (services/own_commands.py).
30
53
  - Displays commands grouped by category for a cleaner, more readable help output.
31
54
  """
32
55
 
@@ -49,44 +72,65 @@ class SPLENTCLI(click.Group):
49
72
  return self._feature_cmds_cache
50
73
 
51
74
  self._feature_cmds_cache: dict[str, click.BaseCommand] = {}
52
- # Discovery runs before every command, including the ones a product
53
- # that is not built yet (selected, not configured or derived) runs
54
- # while getting there. Building its app then fails, and the framework
55
- # logged that failure with a full traceback on every single command,
56
- # although nothing was wrong. The failure is expected here and
57
- # handled below, so the framework's logging is muted for the attempt.
58
75
  debug = bool(os.getenv("SPLENT_DEBUG"))
59
- if not debug:
60
- logging.disable(logging.CRITICAL)
61
76
  try:
62
- from splent_cli.utils.dynamic_imports import get_app
77
+ with _quiet_unless(debug):
78
+ from splent_cli.utils.dynamic_imports import get_app
63
79
 
64
- app = get_app()
65
- with app.app_context():
66
- registry = app.extensions.get("splent_feature_commands", {})
67
- for feature_short, commands in registry.items():
68
- cmd_names = ", ".join(c.name for c in commands if c.name)
69
- group = click.Group(
70
- name=f"feature:{feature_short}",
71
- help=f"Commands contributed by splent_feature_{feature_short}.",
72
- short_help=f"Subcommands: {cmd_names}"
73
- if cmd_names
74
- else f"Commands from splent_feature_{feature_short}.",
75
- )
76
- group.requires_app = True # type: ignore[attr-defined]
77
- for cmd in commands:
78
- group.add_command(cmd)
79
- self._feature_cmds_cache[group.name] = group
80
+ app = get_app()
81
+ with app.app_context():
82
+ registry = app.extensions.get("splent_feature_commands", {})
83
+ for feature_short, commands in registry.items():
84
+ cmd_names = ", ".join(c.name for c in commands if c.name)
85
+ group = click.Group(
86
+ name=f"feature:{feature_short}",
87
+ help=f"Commands contributed by splent_feature_{feature_short}.",
88
+ short_help=f"Subcommands: {cmd_names}"
89
+ if cmd_names
90
+ else f"Commands from splent_feature_{feature_short}.",
91
+ )
92
+ group.requires_app = True # type: ignore[attr-defined]
93
+ for cmd in commands:
94
+ group.add_command(cmd)
95
+ self._feature_cmds_cache[group.name] = group
80
96
  except Exception as e:
81
97
  if debug:
82
98
  click.secho(
83
99
  f" ⚠ Feature commands not loaded: {e}", fg="yellow", err=True
84
100
  )
85
- finally:
86
- if not debug:
87
- logging.disable(logging.NOTSET)
88
101
  return self._feature_cmds_cache
89
102
 
103
+ def load_own_commands(self):
104
+ """The product's own commands, ``product:<name>``, as an
105
+ :class:`~splent_cli.services.own_commands.OwnCommands`.
106
+
107
+ Found like a feature's: once the product's app is built, which
108
+ fails quietly here for a product that cannot be built yet. The
109
+ CLI's own product:* commands win over a product's.
110
+ """
111
+ if hasattr(self, "_own_cmds_cache"):
112
+ return self._own_cmds_cache
113
+
114
+ from splent_cli.services import own_commands
115
+
116
+ self._own_cmds_cache = own_commands.NONE
117
+ debug = bool(os.getenv("SPLENT_DEBUG"))
118
+ try:
119
+ with _quiet_unless(debug):
120
+ from splent_cli.utils.dynamic_imports import get_app
121
+
122
+ app = get_app()
123
+ with app.app_context():
124
+ self._own_cmds_cache = own_commands.collect(
125
+ os.getenv("SPLENT_APP"), set(self.commands)
126
+ )
127
+ except Exception as e:
128
+ if debug:
129
+ click.secho(
130
+ f" ⚠ Product commands not loaded: {e}", fg="yellow", err=True
131
+ )
132
+ return self._own_cmds_cache
133
+
90
134
  def get_command(self, ctx, cmd_name):
91
135
  # "feature:<short>" belongs to the feature called <short> whenever the
92
136
  # product installs one. Built-ins win everywhere else.
@@ -100,13 +144,19 @@ class SPLENTCLI(click.Group):
100
144
  feat_cmds = self._load_feature_commands()
101
145
  if cmd_name in feat_cmds:
102
146
  return feat_cmds[cmd_name]
103
- return super().get_command(ctx, cmd_name)
147
+ command = super().get_command(ctx, cmd_name)
148
+ # "product:<name>" is the product's only where the CLI has no
149
+ # command of that name, so asking for a built-in never imports it.
150
+ if command is None and cmd_name and cmd_name.startswith("product:"):
151
+ command = self.load_own_commands().commands.get(cmd_name)
152
+ return command
104
153
 
105
154
  def list_commands(self, ctx):
106
155
  builtin = super().list_commands(ctx)
107
156
  feat = sorted(self._load_feature_commands().keys())
157
+ own = sorted(self.load_own_commands().commands)
108
158
  # A name a feature claimed is listed once, as the feature's.
109
- return [name for name in builtin if name not in feat] + feat
159
+ return [name for name in builtin if name not in feat] + feat + own
110
160
 
111
161
  # ── App context injection ─────────────────────────────────────
112
162
 
@@ -132,9 +182,14 @@ class SPLENTCLI(click.Group):
132
182
  cmd_name = ctx.protected_args[0] if ctx.protected_args else None
133
183
  command = self.get_command(ctx, cmd_name)
134
184
 
135
- # A feature's own commands are only found by building the app, so
136
- # here they may be missing although the product has them.
137
- if command is None and cmd_name and cmd_name.startswith("feature:"):
185
+ # A feature's commands, and the product's own, are only found by
186
+ # building the app, so here they may be missing although the
187
+ # product has them.
188
+ if (
189
+ command is None
190
+ and cmd_name
191
+ and cmd_name.startswith(("feature:", "product:"))
192
+ ):
138
193
  self._delegate_to_the_product(ctx)
139
194
 
140
195
  if command and getattr(command, "requires_app", False):
@@ -169,6 +224,7 @@ class SPLENTCLI(click.Group):
169
224
  """Group SPLENT commands by category in the CLI help output."""
170
225
  all_cmds = self.list_commands(ctx)
171
226
  feat_cmds = self._load_feature_commands()
227
+ own_cmds = self.load_own_commands().commands
172
228
  groups = {
173
229
  "🌿 Feature Management": [
174
230
  cmd
@@ -176,7 +232,9 @@ class SPLENTCLI(click.Group):
176
232
  if cmd.startswith("feature:") and cmd not in feat_cmds
177
233
  ],
178
234
  "🏗️ Product Management": [
179
- cmd for cmd in all_cmds if cmd.startswith("product:")
235
+ cmd
236
+ for cmd in all_cmds
237
+ if cmd.startswith("product:") and cmd not in own_cmds
180
238
  ],
181
239
  "🧬 SPL & Variability": [
182
240
  cmd for cmd in all_cmds if cmd.startswith(("spl:", "uvl:"))
@@ -207,6 +265,7 @@ class SPLENTCLI(click.Group):
207
265
  if cmd in ("lint", "coverage", "selenium", "locust", "locust:stop")
208
266
  ],
209
267
  "🔌 Feature Commands": [cmd for cmd in all_cmds if cmd in feat_cmds],
268
+ "🔌 Product Commands": [cmd for cmd in all_cmds if cmd in own_cmds],
210
269
  }
211
270
  total = 0
212
271
  for title, cmds in groups.items():
@@ -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,79 @@ def _delegate_to_web_container(argv: list[str]) -> None:
84
87
  )
85
88
 
86
89
 
87
- def run_tests_in_web_container(product: str, argv: list[str]) -> int:
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
+
153
+ def run_tests_in_web_container(product: str, argv: list[str], env=()) -> 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. ``env`` names more variables of this process's
162
+ environment for the run (product:test's [tool.splent.test] env).
94
163
  """
95
164
  name = web_container.name(product)
96
165
  if not _container_running(name):
@@ -112,6 +181,11 @@ def run_tests_in_web_container(product: str, argv: list[str]) -> int:
112
181
  value = os.environ.get(var)
113
182
  if value:
114
183
  passthrough += ["-e", f"{var}={value}"]
184
+ # By name alone: docker reads each value from this process's
185
+ # environment, so a password never shows in the exec's arguments.
186
+ for var in env:
187
+ if var in os.environ:
188
+ passthrough += ["-e", var]
115
189
  # The exec reads a pipe only this process writes to, so it sees the end
116
190
  # of its input exactly when this process ends, however it ends.
117
191
  read_end, write_end = os.pipe()
@@ -121,6 +195,7 @@ def run_tests_in_web_container(product: str, argv: list[str]) -> int:
121
195
  "docker",
122
196
  "exec",
123
197
  "-i",
198
+ *workspace_user(name),
124
199
  "-e",
125
200
  f"SPLENT_APP={product}",
126
201
  *passthrough,
@@ -694,15 +769,19 @@ def _run_neutral(
694
769
  problem = neutral_app.database_problem(dict(os.environ))
695
770
  if problem:
696
771
  _neutral_refusal(problem)
697
- missing = neutral_app.missing_distributions(members)
698
- if missing:
699
- product = context.require_app()
772
+ product = context.require_app()
773
+ unmet = neutral_app.unmet_requirements(members)
774
+ from_workspace = [str(u) for u in unmet if not u.installable]
775
+ if from_workspace:
776
+ # The framework, the CLI and the features are the workspace's
777
+ # copies, which a run never replaces with a release.
700
778
  _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."
779
+ f"The neutral app needs SPLENT packages that {product}'s container\n"
780
+ " does not have at the version asked for:\n"
781
+ + "\n".join(f" {line}" for line in from_workspace)
782
+ + "\n The container has the workspace's copies of them. Update the\n"
783
+ " working copy, or run the tests in a product that declares the\n"
784
+ " feature."
706
785
  )
707
786
 
708
787
  test_paths = _collect_test_paths([(feature.name, feature.root)])
@@ -710,15 +789,33 @@ def _run_neutral(
710
789
  click.secho("⚠️ No test directories found.", fg="yellow")
711
790
  return
712
791
 
713
- click.secho(
714
- f"\n🧪 Running tests for {feature_ref} in a neutral app "
715
- f"[{', '.join(levels)}]...",
716
- fg="cyan",
717
- )
718
792
  with tempfile.TemporaryDirectory(prefix="splent-neutral-") as tmp:
793
+ venv_dir = None
794
+ if unmet:
795
+ click.echo(
796
+ f" {product}'s image lacks what these need, so the run installs "
797
+ "it in a\n virtual environment of its own, removed after it:"
798
+ )
799
+ for line in unmet:
800
+ click.echo(f" {line}")
801
+ try:
802
+ venv_dir = neutral_app.install_requirements(Path(tmp), unmet)
803
+ except neutral_app.NeutralAppError as e:
804
+ _neutral_refusal(
805
+ f"{e}\n Install them in the container, or run the tests "
806
+ "in a product\n that declares the feature."
807
+ )
808
+ click.secho(
809
+ f"\n🧪 Running tests for {feature_ref} in a neutral app "
810
+ f"[{', '.join(levels)}]...",
811
+ fg="cyan",
812
+ )
719
813
  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)
814
+ env = neutral_app.environment(Path(tmp), members, venv_dir=venv_dir)
815
+ python = str(venv_dir / "bin" / "python") if venv_dir else None
816
+ _run_pytest(
817
+ test_paths, levels, keyword, verbose, environment=env, python=python
818
+ )
722
819
 
723
820
 
724
821
  def _resolve_levels(unit, integration, functional, e2e, load) -> tuple[str, ...]:
@@ -744,12 +841,15 @@ def _run_pytest(
744
841
  verbose: bool,
745
842
  extra_sources: list[tuple[str, Path]] = (),
746
843
  environment: dict | None = None,
844
+ python: str | None = None,
747
845
  ):
748
846
  """Run each feature's tests, level by level, and exit 1 if any failed.
749
847
 
750
848
  ENVIRONMENT, when given, is the whole environment of every run (the
751
849
  neutral app's). Otherwise each run gets this process's, with the import
752
- path and the feature sources of the active product.
850
+ path and the feature sources of the active product. PYTHON, when given,
851
+ is the interpreter that runs pytest (the neutral app's virtual
852
+ environment).
753
853
  """
754
854
  passed = failed = skipped = 0
755
855
  for pkg, test_dir, src_dir in test_paths:
@@ -790,7 +890,7 @@ def _run_pytest(
790
890
  )
791
891
 
792
892
  cmd = [
793
- "pytest",
893
+ *([python, "-m", "pytest"] if python else ["pytest"]),
794
894
  str(level_dir),
795
895
  "--rootdir=.",
796
896
  "-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.