splent-cli 1.37.0__tar.gz → 1.38.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 (291) hide show
  1. {splent_cli-1.37.0/src/splent_cli.egg-info → splent_cli-1.38.0}/PKG-INFO +1 -1
  2. {splent_cli-1.37.0 → splent_cli-1.38.0}/pyproject.toml +1 -1
  3. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_release.py +13 -0
  4. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_test.py +226 -12
  5. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_build.py +7 -4
  6. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_create.py +33 -19
  7. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_deploy.py +97 -16
  8. splent_cli-1.38.0/src/splent_cli/services/neutral_app.py +567 -0
  9. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/release.py +61 -11
  10. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/release_dry_run.py +8 -3
  11. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +12 -7
  12. {splent_cli-1.37.0 → splent_cli-1.38.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  13. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli.egg-info/SOURCES.txt +1 -0
  14. {splent_cli-1.37.0 → splent_cli-1.38.0}/COPYING +0 -0
  15. {splent_cli-1.37.0 → splent_cli-1.38.0}/COPYING.LESSER +0 -0
  16. {splent_cli-1.37.0 → splent_cli-1.38.0}/README.md +0 -0
  17. {splent_cli-1.37.0 → splent_cli-1.38.0}/setup.cfg +0 -0
  18. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/__init__.py +0 -0
  19. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/__main__.py +0 -0
  20. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/cli.py +0 -0
  21. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/__init__.py +0 -0
  22. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  23. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  24. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  25. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  26. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  27. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  28. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  29. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  30. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  31. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/__init__.py +0 -0
  32. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  33. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  34. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  35. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_env.py +0 -0
  36. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_features.py +0 -0
  37. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_github.py +0 -0
  38. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  39. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_product.py +0 -0
  40. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  41. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  42. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_releases.py +0 -0
  43. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_vendor.py +0 -0
  44. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  45. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  46. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  47. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  48. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/command/__init__.py +0 -0
  49. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/command/command_create.py +0 -0
  50. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/coverage.py +0 -0
  51. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_console.py +0 -0
  52. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  53. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  54. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  55. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  56. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  57. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  58. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_status.py +0 -0
  59. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  60. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/doctor.py +0 -0
  61. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/env/env_list.py +0 -0
  62. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/env/env_set.py +0 -0
  63. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/env/env_show.py +0 -0
  64. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/export/__init__.py +0 -0
  65. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  66. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  67. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
  68. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  69. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  70. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
  71. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  72. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
  73. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
  74. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  75. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
  76. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  77. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  78. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  79. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  80. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  81. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  82. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  83. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  84. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  85. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
  86. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  87. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  88. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  89. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  90. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  91. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  92. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  93. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  94. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
  95. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
  96. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  97. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  98. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  99. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  100. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  101. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
  102. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  103. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  104. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  105. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/lint.py +0 -0
  106. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/locust.py +0 -0
  107. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  108. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  109. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  110. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  111. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  112. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/__init__.py +0 -0
  113. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  114. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  115. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  116. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_config.py +0 -0
  117. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_configure.py +0 -0
  118. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_console.py +0 -0
  119. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  120. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  121. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  122. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_down.py +0 -0
  123. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  124. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_env.py +0 -0
  125. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_list.py +0 -0
  126. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  127. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  128. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_port.py +0 -0
  129. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_release.py +0 -0
  130. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  131. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  132. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  133. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_run.py +0 -0
  134. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_select.py +0 -0
  135. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  136. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  137. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  138. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_test.py +0 -0
  139. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_up.py +0 -0
  140. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_validate.py +0 -0
  141. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/release/__init__.py +0 -0
  142. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/release/release_core.py +0 -0
  143. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/release/release_resume.py +0 -0
  144. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/selenium.py +0 -0
  145. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  146. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  147. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  148. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  149. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  150. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  151. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  152. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  153. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  154. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  155. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  156. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  157. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  158. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
  159. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
  160. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  161. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  162. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  163. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/commands/version.py +0 -0
  164. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/__init__.py +0 -0
  165. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/compose.py +0 -0
  166. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/context.py +0 -0
  167. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/credentials.py +0 -0
  168. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/deploy_bundle.py +0 -0
  169. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/image_registry.py +0 -0
  170. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/index_refresh.py +0 -0
  171. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/marketplace.py +0 -0
  172. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/marketplace_api.py +0 -0
  173. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/marketplace_url.py +0 -0
  174. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/preflight.py +0 -0
  175. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/pypi_quota.py +0 -0
  176. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/registry.py +0 -0
  177. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/release_gate.py +0 -0
  178. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/spl_store.py +0 -0
  179. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/services/web_container.py +0 -0
  180. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
  181. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
  182. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
  183. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
  184. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
  185. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
  186. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
  187. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
  188. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
  189. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
  190. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
  191. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
  192. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
  193. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
  194. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
  195. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
  196. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
  197. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
  198. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
  199. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
  200. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
  201. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
  202. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
  203. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
  204. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
  205. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
  206. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
  207. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
  208. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
  209. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
  210. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
  211. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
  212. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
  213. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
  214. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/licenses/COPYING +0 -0
  215. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
  216. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
  217. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
  218. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
  219. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
  220. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
  221. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
  222. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
  223. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +0 -0
  224. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -0
  225. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
  226. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
  227. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
  228. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_400.html +0 -0
  229. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_401.html +0 -0
  230. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_404.html +0 -0
  231. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_500.html +0 -0
  232. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
  233. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
  234. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
  235. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
  236. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
  237. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_app.css +0 -0
  238. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_app.js +0 -0
  239. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
  240. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +0 -0
  241. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +0 -0
  242. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
  243. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
  244. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +0 -0
  245. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
  246. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
  247. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
  248. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
  249. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
  250. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
  251. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
  252. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_own.css +0 -0
  253. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
  254. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
  255. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
  256. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/__init__.py +0 -0
  257. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/archetype.py +0 -0
  258. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/cache_utils.py +0 -0
  259. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/ci_services.py +0 -0
  260. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/ci_workflow.py +0 -0
  261. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/command_loader.py +0 -0
  262. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  263. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/db_client.py +0 -0
  264. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/db_utils.py +0 -0
  265. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/decorators.py +0 -0
  266. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  267. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/feature_installer.py +0 -0
  268. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/feature_naming.py +0 -0
  269. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/feature_requirements.py +0 -0
  270. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/feature_resolution.py +0 -0
  271. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/feature_utils.py +0 -0
  272. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/forge.py +0 -0
  273. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/git_url.py +0 -0
  274. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/integrity.py +0 -0
  275. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/io_utils.py +0 -0
  276. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/lifecycle.py +0 -0
  277. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/manifest.py +0 -0
  278. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/menu_keys.py +0 -0
  279. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/migration_lock.py +0 -0
  280. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/path_utils.py +0 -0
  281. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/proc.py +0 -0
  282. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/redact.py +0 -0
  283. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/refinement_facts.py +0 -0
  284. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/running_version.py +0 -0
  285. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/template_drift.py +0 -0
  286. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/toml_edit.py +0 -0
  287. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
  288. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  289. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  290. {splent_cli-1.37.0 → splent_cli-1.38.0}/src/splent_cli.egg-info/requires.txt +0 -0
  291. {splent_cli-1.37.0 → splent_cli-1.38.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.37.0
3
+ Version: 1.38.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.37.0"
7
+ version = "1.38.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"
@@ -2008,6 +2008,14 @@ def create_versioned_snapshot(namespace, feature_name, version, workspace, repo=
2008
2008
  is_flag=True,
2009
2009
  help="If PyPI's new-project limit is spent, wait for it however long it takes.",
2010
2010
  )
2011
+ @click.option(
2012
+ "--neutral",
2013
+ is_flag=True,
2014
+ help="Gate on the tests in a neutral app (the framework, the feature, the "
2015
+ "releases of what it requires, the default configuration), and run them in "
2016
+ "the active product as a check that does not block. Automatic when the "
2017
+ "product does not declare the feature.",
2018
+ )
2011
2019
  def feature_release(
2012
2020
  feature_ref,
2013
2021
  version,
@@ -2020,6 +2028,7 @@ def feature_release(
2020
2028
  no_github,
2021
2029
  allow_unfinished,
2022
2030
  wait,
2031
+ neutral,
2023
2032
  ):
2024
2033
  no_forge = release.no_forge_requested(no_forge, no_github)
2025
2034
  workspace = str(context.workspace())
@@ -2056,6 +2065,7 @@ def feature_release(
2056
2065
  skip_checks=skip_checks,
2057
2066
  attach=attach,
2058
2067
  workspace=workspace,
2068
+ neutral=neutral,
2059
2069
  )
2060
2070
  return
2061
2071
 
@@ -2154,6 +2164,7 @@ def feature_release(
2154
2164
  pre_commit_hook=_pre_commit,
2155
2165
  post_pypi_hook=_post_pypi,
2156
2166
  wait=wait,
2167
+ neutral=neutral,
2157
2168
  )
2158
2169
 
2159
2170
  attach_cmd = f"splent feature:attach {feature_ref} {tag}"
@@ -2282,6 +2293,7 @@ def _dry_run(
2282
2293
  skip_checks,
2283
2294
  attach,
2284
2295
  workspace,
2296
+ neutral=False,
2285
2297
  ):
2286
2298
  """feature:release --dry-run. Changes nothing, exits 1 if a real run would stop."""
2287
2299
  from splent_cli.services import release_dry_run
@@ -2350,6 +2362,7 @@ def _dry_run(
2350
2362
  workspace=workspace,
2351
2363
  product=product,
2352
2364
  attach=attach,
2365
+ neutral=neutral,
2353
2366
  )
2354
2367
  if rehearsal.blockers:
2355
2368
  raise SystemExit(1)
@@ -2,6 +2,7 @@ import os
2
2
  import re
3
3
  import shlex
4
4
  import subprocess
5
+ import tempfile
5
6
  from pathlib import Path
6
7
  import click
7
8
  from dotenv import dotenv_values
@@ -14,7 +15,7 @@ from splent_cli.utils.feature_utils import (
14
15
  get_normalize_feature_name_in_splent_format,
15
16
  parse_feature_entry,
16
17
  )
17
- from splent_cli.services import context, web_container
18
+ from splent_cli.services import context, neutral_app, web_container
18
19
 
19
20
 
20
21
  # Test levels in execution order (default = first three)
@@ -229,7 +230,7 @@ def _database_name(db_url) -> str:
229
230
  return ""
230
231
 
231
232
 
232
- def _validate_testing_environment():
233
+ def _refuse_production() -> None:
233
234
  if _is_prod_active():
234
235
  click.secho(
235
236
  "❌ Cannot run tests in production.\n"
@@ -241,6 +242,10 @@ def _validate_testing_environment():
241
242
  )
242
243
  raise SystemExit(1)
243
244
 
245
+
246
+ def _validate_testing_environment():
247
+ _refuse_production()
248
+
244
249
  try:
245
250
  env = get_current_app_config_value("TESTING")
246
251
  db_url = get_current_app_config_value("SQLALCHEMY_DATABASE_URI")
@@ -561,6 +566,161 @@ def with_import_path(dirs: list[str], env: dict) -> str:
561
566
  return os.pathsep.join([*dirs, *(p for p in existing if p and p not in dirs)])
562
567
 
563
568
 
569
+ # ── The neutral app ───────────────────────────────────────────────────
570
+
571
+
572
+ def declaring_product(feature_ref: str) -> str | None:
573
+ """The active product when it declares the feature FEATURE_REF names.
574
+
575
+ None when it does not: its tests then run in the neutral app, since the
576
+ product's app does not load the feature.
577
+ """
578
+ product = context.require_app()
579
+ name = get_normalize_feature_name_in_splent_format(_pkg_name(feature_ref))
580
+ declared = any(_pkg_name(ref) == name for ref in get_features_from_pyproject())
581
+ return product if declared else None
582
+
583
+
584
+ def _neutral_refusal(text: str) -> None:
585
+ click.secho(f"❌ {text}", fg="red")
586
+ raise SystemExit(1)
587
+
588
+
589
+ def _feature_under_test(feature_ref: str, workspace: Path) -> neutral_app.Member:
590
+ """The copy of FEATURE_REF whose tests the neutral app runs.
591
+
592
+ Its working copy at the workspace root, or the release FEATURE_REF names
593
+ (``recaptcha@v0.2.2``), from the cache.
594
+ """
595
+ from splent_cli.utils.feature_resolution import resolve_feature_ref
596
+
597
+ found = resolve_feature_ref(feature_ref, workspace)
598
+ name = get_normalize_feature_name_in_splent_format(_pkg_name(feature_ref))
599
+ if found is None:
600
+ _neutral_refusal(
601
+ f"There is no copy of {name} in the workspace to test: no working\n"
602
+ f" copy at its root and no release in .splent_cache.\n"
603
+ f" Clone one first, for example with\n"
604
+ f" splent feature:clone splent-io/{name}"
605
+ )
606
+ root, namespace, name = found
607
+ version = root.name.partition("@")[2] or None
608
+ return neutral_app.Member(namespace, name, version, root, "under test")
609
+
610
+
611
+ def _tests_dir(member: neutral_app.Member) -> Path | None:
612
+ found = _collect_test_paths([(member.name, member.root)])
613
+ return found[0][1] if found else None
614
+
615
+
616
+ def _neutral_working_copies(with_refs, workspace: Path) -> list[tuple[str, Path]]:
617
+ """The working copies --with names, for the neutral app.
618
+
619
+ Unlike in the product's app, a feature need not be declared anywhere:
620
+ the neutral app loads it from its working copy, in place of its release
621
+ when the feature under test requires it, and in addition otherwise.
622
+ """
623
+ copies: list[tuple[str, Path]] = []
624
+ for ref in with_refs:
625
+ name = get_normalize_feature_name_in_splent_format(_pkg_name(ref))
626
+ path = editable_feature_dir(workspace, name)
627
+ if path is None:
628
+ short = name.removeprefix("splent_feature_")
629
+ _neutral_refusal(
630
+ f"--with {ref}, but {name} has no working copy at the workspace root.\n"
631
+ f" Run 'splent feature:unlock {short}' to make one."
632
+ )
633
+ if all(existing != name for existing, _ in copies):
634
+ copies.append((name, Path(path)))
635
+ return copies
636
+
637
+
638
+ def neutral_plan(feature_ref: str, with_refs=()) -> list[neutral_app.Member]:
639
+ """The features of the neutral app for FEATURE_REF's tests, printed.
640
+
641
+ Every release is fetched into the cache here, so the run in the
642
+ product's container finds them all.
643
+ """
644
+ workspace = context.workspace()
645
+ product = context.require_app()
646
+ feature = _feature_under_test(feature_ref, workspace)
647
+ if declaring_product(feature_ref) is None:
648
+ click.secho(
649
+ f" {feature.name} is not declared in {product}, so its tests run "
650
+ "in a neutral app.",
651
+ fg="yellow",
652
+ )
653
+ copies = _neutral_working_copies(with_refs, workspace)
654
+ try:
655
+ members = neutral_app.plan(
656
+ workspace, feature, _tests_dir(feature), copies, say=click.echo
657
+ )
658
+ except neutral_app.NeutralAppError as e:
659
+ _neutral_refusal(f"The neutral app cannot be put together: {e}")
660
+ click.echo(
661
+ " Neutral app: the framework, these features and their default "
662
+ f"configuration.\n Nothing of {product} reaches it but its test database."
663
+ )
664
+ for line in neutral_app.describe(members):
665
+ click.echo(line)
666
+ return members
667
+
668
+
669
+ def _run_neutral(
670
+ feature_ref, member_entries, with_refs, levels, keyword, verbose
671
+ ) -> None:
672
+ """Run FEATURE_REF's tests in the neutral app, in this container."""
673
+ if "e2e" in levels:
674
+ _neutral_refusal(
675
+ "End-to-end tests drive the product's running server, and the\n"
676
+ " neutral app is never served. Run them in a product that declares\n"
677
+ " the feature, without --neutral."
678
+ )
679
+ workspace = context.workspace()
680
+ try:
681
+ members = (
682
+ neutral_app.parse_members(workspace, member_entries)
683
+ if member_entries
684
+ else neutral_plan(feature_ref, with_refs)
685
+ )
686
+ except neutral_app.NeutralAppError as e:
687
+ _neutral_refusal(str(e))
688
+ name = get_normalize_feature_name_in_splent_format(_pkg_name(feature_ref))
689
+ feature = next((m for m in members if m.name == name), None)
690
+ if feature is None:
691
+ _neutral_refusal(f"The neutral app given does not hold {name}.")
692
+
693
+ _refuse_production()
694
+ problem = neutral_app.database_problem(dict(os.environ))
695
+ if problem:
696
+ _neutral_refusal(problem)
697
+ missing = neutral_app.missing_distributions(members)
698
+ if missing:
699
+ product = context.require_app()
700
+ _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."
706
+ )
707
+
708
+ test_paths = _collect_test_paths([(feature.name, feature.root)])
709
+ if not test_paths:
710
+ click.secho("⚠️ No test directories found.", fg="yellow")
711
+ return
712
+
713
+ click.secho(
714
+ f"\n🧪 Running tests for {feature_ref} in a neutral app "
715
+ f"[{', '.join(levels)}]...",
716
+ fg="cyan",
717
+ )
718
+ with tempfile.TemporaryDirectory(prefix="splent-neutral-") as tmp:
719
+ 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)
722
+
723
+
564
724
  def _resolve_levels(unit, integration, functional, e2e, load) -> tuple[str, ...]:
565
725
  """Determine which test levels to run based on CLI flags."""
566
726
  explicit = []
@@ -583,7 +743,14 @@ def _run_pytest(
583
743
  keyword: str | None,
584
744
  verbose: bool,
585
745
  extra_sources: list[tuple[str, Path]] = (),
746
+ environment: dict | None = None,
586
747
  ):
748
+ """Run each feature's tests, level by level, and exit 1 if any failed.
749
+
750
+ ENVIRONMENT, when given, is the whole environment of every run (the
751
+ neutral app's). Otherwise each run gets this process's, with the import
752
+ path and the feature sources of the active product.
753
+ """
587
754
  passed = failed = skipped = 0
588
755
  for pkg, test_dir, src_dir in test_paths:
589
756
  click.secho(f"\n ▶ {pkg}", fg="cyan", bold=True)
@@ -596,7 +763,7 @@ def _run_pytest(
596
763
  sources = [(pkg, Path(src_dir).parent)] + [
597
764
  (name, Path(root)) for name, root in extra_sources if name != pkg
598
765
  ]
599
- import_path = feature_src_dirs(sources)
766
+ import_path = feature_src_dirs(sources) if environment is None else []
600
767
 
601
768
  for level in levels:
602
769
  level_dir = test_dir / level
@@ -613,11 +780,14 @@ def _run_pytest(
613
780
  click.secho(note, fg="yellow")
614
781
  continue
615
782
 
616
- env = os.environ.copy()
617
- env["PYTHONPATH"] = with_import_path(import_path, env)
618
- env["SPLENT_FEATURE_SOURCES"] = ",".join(
619
- f"{name}={root}" for name, root in sources
620
- )
783
+ if environment is not None:
784
+ env = dict(environment)
785
+ else:
786
+ env = os.environ.copy()
787
+ env["PYTHONPATH"] = with_import_path(import_path, env)
788
+ env["SPLENT_FEATURE_SOURCES"] = ",".join(
789
+ f"{name}={root}" for name, root in sources
790
+ )
621
791
 
622
792
  cmd = [
623
793
  "pytest",
@@ -710,6 +880,17 @@ def _run_pytest(
710
880
  help="Load FEATURE from its working copy at the workspace root too, instead "
711
881
  "of the version the product pins. Repeatable.",
712
882
  )
883
+ @click.option(
884
+ "--neutral",
885
+ is_flag=True,
886
+ help="Run the tests in a neutral app instead of the product's: the "
887
+ "framework, the feature, the releases of what it requires and the "
888
+ "default configuration. Automatic for a feature the product does not "
889
+ "declare.",
890
+ )
891
+ # The neutral app as the CLI container resolved it, for the run in the
892
+ # product's container, which then fetches nothing.
893
+ @click.option("--neutral-member", "neutral_members", multiple=True, hidden=True)
713
894
  def feature_test(
714
895
  feature_ref,
715
896
  keyword,
@@ -720,6 +901,8 @@ def feature_test(
720
901
  e2e,
721
902
  load,
722
903
  with_features,
904
+ neutral,
905
+ neutral_members,
723
906
  ):
724
907
  """
725
908
  Run the test suite for features declared in the active product.
@@ -736,12 +919,22 @@ def feature_test(
736
919
  splent feature:test auth --functional # functional tests for auth
737
920
  splent feature:test -k test_login -v # keyword filter, verbose
738
921
  splent feature:test profile --with auth # profile's tests, auth's working copy
922
+ splent feature:test recaptcha # not in the product: a neutral app
923
+ splent feature:test theme --neutral # in the product, tested without it
739
924
 
740
925
  \b
741
926
  The app under test loads the feature whose tests run from the copy those
742
927
  tests come from. --with does the same for other features the tests
743
928
  cross into, so changes made to two features together are tested
744
929
  together.
930
+
931
+ A feature the product does not declare is tested in a neutral app, and
932
+ --neutral chooses it for one the product declares. The neutral app is a
933
+ new product holding the framework, the feature, the newest releases of
934
+ what it requires and of what its tests import, and their default
935
+ configuration. Nothing of the product's templates, config.py or .env
936
+ reaches it, only its test database. --with puts a working copy in place
937
+ of a release there.
745
938
  """
746
939
  levels_flags = (
747
940
  ("unit", unit),
@@ -750,12 +943,33 @@ def feature_test(
750
943
  ("e2e", e2e),
751
944
  ("load", load),
752
945
  )
946
+ neutral = neutral or bool(neutral_members)
947
+ if neutral and not feature_ref:
948
+ raise click.UsageError(
949
+ "--neutral builds the app for one feature. Name it: "
950
+ "splent feature:test <feature> --neutral"
951
+ )
753
952
  if not _is_product_container():
754
- _delegate_to_web_container(
755
- _passthrough_args(
756
- feature_ref, keyword, verbose, levels_flags, with_features
757
- )
953
+ argv = _passthrough_args(
954
+ feature_ref, keyword, verbose, levels_flags, with_features
758
955
  )
956
+ if feature_ref and (neutral or declaring_product(feature_ref) is None):
957
+ members = neutral_plan(feature_ref, with_features)
958
+ argv.append("--neutral")
959
+ for member in members:
960
+ argv += ["--neutral-member", member.entry]
961
+ _delegate_to_web_container(argv)
962
+
963
+ if feature_ref and (neutral or declaring_product(feature_ref) is None):
964
+ _run_neutral(
965
+ feature_ref,
966
+ neutral_members,
967
+ with_features,
968
+ _resolve_levels(unit, integration, functional, e2e, load),
969
+ keyword,
970
+ verbose,
971
+ )
972
+ return
759
973
 
760
974
  # Look the feature up first: an undeclared one is refused before the
761
975
  # guard builds an app that could not load it anyway.
@@ -98,10 +98,13 @@ OPTIONAL_DEPLOY_SETTINGS: tuple[tuple[str, str, tuple[str, ...]], ...] = (
98
98
  "SPLENT_TRUSTED_PROXIES",
99
99
  "1",
100
100
  (
101
- "Reverse proxies in front of the app. 1 behind the usual TLS front,",
102
- "one more for each proxy inside the stack (the nginx feature), 0 when",
103
- "nothing is in front. Unset means 0, so behind a proxy every caller",
104
- "shares the proxy's address and X-Forwarded-Proto is ignored.",
101
+ "Reverse proxies whose X-Forwarded headers reach the app. 1 behind",
102
+ "the usual TLS front, and 1 with the nginx feature too, at the edge",
103
+ "or behind that front, since nginx is the only hop the app talks to",
104
+ "and forwards one value of each header. One more only for a proxy",
105
+ "that adds its own value to them. 0 when nothing is in front. Unset",
106
+ "means 0, so behind a proxy every caller shares the proxy's address",
107
+ "and X-Forwarded-Proto is ignored.",
105
108
  ),
106
109
  ),
107
110
  (
@@ -309,6 +309,37 @@ def make_product(name, spl_name, spl_doi, features_file, force):
309
309
  )
310
310
 
311
311
 
312
+ def app_package_files(name: str) -> tuple[dict[str, str], dict[str, str]]:
313
+ """The product's own package, src/<name>/, as ``(rendered, copied)``.
314
+
315
+ Each maps a path inside the product to its template. product:create
316
+ writes them, and so does the neutral app feature:test runs a feature
317
+ in, which is a product created with that feature and nothing else
318
+ (services/neutral_app.py).
319
+ """
320
+ rendered = {
321
+ f"src/{name}/__init__.py": "product/product_init.py.j2",
322
+ f"src/{name}/config.py": "product/product_config.py.j2",
323
+ f"src/{name}/errors.py": "product/product_errors.py.j2",
324
+ f"src/{name}/logging.py": "product/product_logging.py.j2",
325
+ }
326
+ copied = {
327
+ f"src/{name}/static/css/app.css": "product/product_app.css",
328
+ f"src/{name}/static/css/dropzone.css": "product/product_dropzone.css",
329
+ f"src/{name}/static/css/own.css": "product/product_own.css",
330
+ f"src/{name}/static/favicon.svg": "product/product_favicon.svg",
331
+ f"src/{name}/static/js/app.js": "product/product_app.js",
332
+ # 400/401 are the framework fallback's lookups; 404/500 come from
333
+ # the theme (errors/404.html, errors/500.html) so they wear the skin.
334
+ # There is no 403.html. Without one the framework draws the 403
335
+ # inside base_template.html, so the account keeps its sidebar.
336
+ f"src/{name}/templates/400.html": "product/product_400.html",
337
+ f"src/{name}/templates/401.html": "product/product_401.html",
338
+ f"src/{name}/templates/base_template.html": "product/product_base_template.html",
339
+ }
340
+ return rendered, copied
341
+
342
+
312
343
  def _write_product(env, name, base_path, template_ctx):
313
344
  """Render the product tree into base_path, which does not exist yet."""
314
345
  for subdir in [
@@ -353,26 +384,9 @@ def _write_product(env, name, base_path, template_ctx):
353
384
  "package.json": "product/product_package.json.j2",
354
385
  "pyproject.toml": "product/product_pyproject.toml.j2",
355
386
  "README.md": "product/product_README.md.j2",
356
- f"src/{name}/__init__.py": "product/product_init.py.j2",
357
- f"src/{name}/config.py": "product/product_config.py.j2",
358
- f"src/{name}/errors.py": "product/product_errors.py.j2",
359
- f"src/{name}/logging.py": "product/product_logging.py.j2",
360
- }
361
-
362
- raw_files = {
363
- f"src/{name}/static/css/app.css": "product/product_app.css",
364
- f"src/{name}/static/css/dropzone.css": "product/product_dropzone.css",
365
- f"src/{name}/static/css/own.css": "product/product_own.css",
366
- f"src/{name}/static/favicon.svg": "product/product_favicon.svg",
367
- f"src/{name}/static/js/app.js": "product/product_app.js",
368
- # 400/401 are the framework fallback's lookups; 404/500 come from
369
- # the theme (errors/404.html, errors/500.html) so they wear the skin.
370
- # There is no 403.html. Without one the framework draws the 403
371
- # inside base_template.html, so the account keeps its sidebar.
372
- f"src/{name}/templates/400.html": "product/product_400.html",
373
- f"src/{name}/templates/401.html": "product/product_401.html",
374
- f"src/{name}/templates/base_template.html": "product/product_base_template.html",
375
387
  }
388
+ package_templates, raw_files = app_package_files(name)
389
+ jinja_templates.update(package_templates)
376
390
 
377
391
  for rel_path, tpl in jinja_templates.items():
378
392
  abs_path = os.path.join(base_path, rel_path)
@@ -417,6 +417,95 @@ def compose_environment(
417
417
  return env
418
418
 
419
419
 
420
+ # ── SPLENT_TRUSTED_PROXIES ──────────────────────────────────────────────
421
+
422
+ NGINX_FEATURE = "splent_feature_nginx"
423
+ #: The first nginx release that forwards one value of each X-Forwarded
424
+ #: header, after deciding with realip whether to believe a TLS front.
425
+ NGINX_ONE_VALUE = (1, 4, 0)
426
+
427
+
428
+ def nginx_version(product_path: str) -> str | None:
429
+ """The version of the nginx feature the product deploys.
430
+
431
+ ``""`` when it declares nginx without a version, None when it does not
432
+ declare it, for production, at all.
433
+ """
434
+ from splent_cli.utils.feature_utils import (
435
+ load_product_features,
436
+ parse_feature_entry,
437
+ )
438
+
439
+ try:
440
+ entries = load_product_features(product_path, "prod")
441
+ except Exception: # noqa: BLE001 - only the advice depends on it
442
+ return None
443
+ for entry in entries:
444
+ _, name, version = parse_feature_entry(entry)
445
+ if name == NGINX_FEATURE:
446
+ return version or ""
447
+ return None
448
+
449
+
450
+ def _release(version: str) -> tuple[int, ...] | None:
451
+ m = re.match(r"v?(\d+)\.(\d+)\.(\d+)", version or "")
452
+ return tuple(int(g) for g in m.groups()) if m else None
453
+
454
+
455
+ def trusted_proxies_advice(env_vars: dict, nginx: str | None) -> list[str]:
456
+ """What to tell the operator about SPLENT_TRUSTED_PROXIES, line by line.
457
+
458
+ Empty when the value looks right. The number counts the proxies whose
459
+ X-Forwarded values reach the app, one value per proxy, and the app
460
+ ignores the headers when it finds fewer values than that.
461
+
462
+ NGINX is the version of the nginx feature the product deploys (see
463
+ :func:`nginx_version`). That feature is the only hop the app talks to,
464
+ whether it is the edge or sits behind a TLS front: it decides with
465
+ NGINX_TRUSTED_PROXY_CIDRS whether to believe the front and forwards one
466
+ value of each header, so the right number with it is 1. 2, which the
467
+ CLI used to suggest for nginx behind a TLS front, made the app ignore
468
+ the headers: http:// links, cookies without Secure, no HSTS.
469
+ """
470
+ raw = (env_vars.get("SPLENT_TRUSTED_PROXIES") or "").strip()
471
+ if not raw:
472
+ if nginx is not None:
473
+ return [
474
+ "SPLENT_TRUSTED_PROXIES is not set in deploy/.env, so the app "
475
+ "believes no X-Forwarded header.",
476
+ "The product runs the nginx feature, the only hop the app talks "
477
+ "to, so set it to 1, whether nginx is the edge or sits behind a "
478
+ "TLS front.",
479
+ ]
480
+ return [
481
+ "SPLENT_TRUSTED_PROXIES is not set in deploy/.env, so the app "
482
+ "believes no X-Forwarded header.",
483
+ "Behind a reverse proxy set it to the number of proxies in front "
484
+ "(1 behind the usual TLS front). Set it to 0 when nothing is in "
485
+ "front.",
486
+ ]
487
+ try:
488
+ hops = int(raw)
489
+ except ValueError:
490
+ return []
491
+ if nginx is None or hops <= 1:
492
+ return []
493
+ lines = [
494
+ f"SPLENT_TRUSTED_PROXIES is {hops} in deploy/.env, and the app talks to "
495
+ "nginx alone, which forwards one value of each X-Forwarded header.",
496
+ f"The app looks for {hops}, finds one and ignores them all: http:// "
497
+ "links, cookies without Secure, no HSTS. Set it to 1.",
498
+ ]
499
+ release = _release(nginx)
500
+ if release is not None and release < NGINX_ONE_VALUE:
501
+ lines.append(
502
+ f"splent_feature_nginx {nginx} adds the TLS front to X-Forwarded-For "
503
+ "as well, so behind one the app takes the front for the client. "
504
+ "Upgrade it to v1.4.0 or later."
505
+ )
506
+ return lines
507
+
508
+
420
509
  def _compose_cmd(product: str, product_path: str, env_path: str, *args) -> list:
421
510
  """``docker compose`` aimed at the deploy bundle, as a host would run it."""
422
511
  return [
@@ -645,16 +734,11 @@ def product_deploy(down, ci, image):
645
734
  # this setting the app takes the proxy for the caller: one address for
646
735
  # every client and plain http in every link it builds. A deployment that
647
736
  # predates the template line (or deleted it) is told once per deploy;
648
- # an explicit 0 says nothing is in front and silences it.
649
- proxies_unset = "SPLENT_TRUSTED_PROXIES" not in env_vars
650
- if proxies_unset:
651
- click.secho(
652
- " SPLENT_TRUSTED_PROXIES is not set in deploy/.env, so the app "
653
- "believes no X-Forwarded header.\n"
654
- " Behind a reverse proxy set it to the number of proxies (1 behind "
655
- "the usual TLS front). Set it to 0 when nothing is in front.",
656
- fg="yellow",
657
- )
737
+ # an explicit 0 says nothing is in front and silences it. With the nginx
738
+ # feature a number above 1 is as bad, and said too.
739
+ proxy_advice = trusted_proxies_advice(env_vars, nginx_version(product_path))
740
+ if proxy_advice:
741
+ click.secho("\n".join(f" {line}" for line in proxy_advice), fg="yellow")
658
742
 
659
743
  # ---------------------------------------------------------
660
744
  # Warn about write directories no volume preserves
@@ -897,14 +981,11 @@ def product_deploy(down, ci, image):
897
981
  # compose output. Never set automatically, because a deployment that
898
982
  # publishes the app port beyond the host would then let any client forge
899
983
  # X-Forwarded-For.
900
- if proxies_unset:
984
+ if proxy_advice:
901
985
  click.echo()
902
986
  click.secho(
903
- " SPLENT_TRUSTED_PROXIES is not set in deploy/.env, so the "
904
- "app believes no X-Forwarded header.\n"
905
- " Uncomment '# SPLENT_TRUSTED_PROXIES=1' there (2 with the nginx "
906
- "feature behind the TLS front, 0 when nothing is in front) and run "
907
- "product:deploy again.",
987
+ "\n".join(f" {line}" for line in proxy_advice)
988
+ + "\n Change it in deploy/.env and run product:deploy again.",
908
989
  fg="yellow",
909
990
  )
910
991