splent-cli 1.38.0__tar.gz → 1.40.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 (293) hide show
  1. {splent_cli-1.38.0/src/splent_cli.egg-info → splent_cli-1.40.0}/PKG-INFO +2 -2
  2. {splent_cli-1.38.0 → splent_cli-1.40.0}/pyproject.toml +9 -6
  3. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_contract.py +45 -1
  4. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_release.py +98 -6
  5. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_build.py +24 -2
  6. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_config.py +37 -9
  7. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_env.py +39 -5
  8. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_validate.py +179 -41
  9. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/uvl/uvl_utils.py +51 -27
  10. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/preflight.py +31 -0
  11. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/release_dry_run.py +27 -0
  12. splent_cli-1.40.0/src/splent_cli/utils/feature_params.py +551 -0
  13. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/redact.py +18 -6
  14. splent_cli-1.40.0/src/splent_cli/utils/uvl_params.py +254 -0
  15. {splent_cli-1.38.0 → splent_cli-1.40.0/src/splent_cli.egg-info}/PKG-INFO +2 -2
  16. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli.egg-info/SOURCES.txt +3 -1
  17. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli.egg-info/requires.txt +1 -1
  18. {splent_cli-1.38.0 → splent_cli-1.40.0}/COPYING +0 -0
  19. {splent_cli-1.38.0 → splent_cli-1.40.0}/COPYING.LESSER +0 -0
  20. {splent_cli-1.38.0 → splent_cli-1.40.0}/README.md +0 -0
  21. {splent_cli-1.38.0 → splent_cli-1.40.0}/setup.cfg +0 -0
  22. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/__init__.py +0 -0
  23. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/__main__.py +0 -0
  24. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/cli.py +0 -0
  25. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/__init__.py +0 -0
  26. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  27. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  28. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  29. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  30. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  31. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  32. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  33. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  34. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  35. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/check/__init__.py +0 -0
  36. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  37. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  38. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  39. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/check/check_env.py +0 -0
  40. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/check/check_features.py +0 -0
  41. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/check/check_github.py +0 -0
  42. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  43. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/check/check_product.py +0 -0
  44. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  45. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  46. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/check/check_releases.py +0 -0
  47. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/check/check_vendor.py +0 -0
  48. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  49. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  50. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  51. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  52. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/command/__init__.py +0 -0
  53. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/command/command_create.py +0 -0
  54. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/coverage.py +0 -0
  55. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/database/db_console.py +0 -0
  56. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  57. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  58. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  59. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  60. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  61. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  62. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/database/db_status.py +0 -0
  63. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  64. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/doctor.py +0 -0
  65. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/env/env_list.py +0 -0
  66. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/env/env_set.py +0 -0
  67. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/env/env_show.py +0 -0
  68. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/export/__init__.py +0 -0
  69. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  70. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  71. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
  72. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  73. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  74. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
  75. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  76. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
  77. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  78. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
  79. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  80. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  81. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  82. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  83. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  84. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  85. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  86. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  87. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  88. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
  89. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  90. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  91. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  92. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  93. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  94. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  95. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  96. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  97. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
  98. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
  99. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  100. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  101. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  102. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  103. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_test.py +0 -0
  104. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  105. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
  106. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  107. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  108. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  109. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/lint.py +0 -0
  110. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/locust.py +0 -0
  111. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  112. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  113. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  114. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  115. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  116. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/__init__.py +0 -0
  117. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  118. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  119. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  120. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_configure.py +0 -0
  121. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_console.py +0 -0
  122. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  123. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_create.py +0 -0
  124. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
  125. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  126. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  127. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_down.py +0 -0
  128. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  129. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_list.py +0 -0
  130. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  131. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  132. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_port.py +0 -0
  133. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_release.py +0 -0
  134. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  135. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  136. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  137. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_run.py +0 -0
  138. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_select.py +0 -0
  139. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  140. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  141. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  142. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_test.py +0 -0
  143. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/product/product_up.py +0 -0
  144. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/release/__init__.py +0 -0
  145. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/release/release_core.py +0 -0
  146. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/release/release_resume.py +0 -0
  147. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/selenium.py +0 -0
  148. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  149. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  150. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  151. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  152. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  153. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  154. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  155. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  156. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  157. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  158. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  159. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  160. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  161. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
  162. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
  163. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  164. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  165. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/commands/version.py +0 -0
  166. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/__init__.py +0 -0
  167. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/compose.py +0 -0
  168. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/context.py +0 -0
  169. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/credentials.py +0 -0
  170. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/deploy_bundle.py +0 -0
  171. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/image_registry.py +0 -0
  172. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/index_refresh.py +0 -0
  173. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/marketplace.py +0 -0
  174. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/marketplace_api.py +0 -0
  175. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/marketplace_url.py +0 -0
  176. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/neutral_app.py +0 -0
  177. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/pypi_quota.py +0 -0
  178. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/registry.py +0 -0
  179. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/release.py +0 -0
  180. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/release_gate.py +0 -0
  181. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/spl_store.py +0 -0
  182. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/services/web_container.py +0 -0
  183. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
  184. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
  185. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
  186. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
  187. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
  188. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
  189. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
  190. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
  191. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
  192. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
  193. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
  194. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
  195. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
  196. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
  197. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
  198. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
  199. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
  200. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
  201. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
  202. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
  203. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
  204. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
  205. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
  206. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
  207. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
  208. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
  209. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
  210. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
  211. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
  212. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
  213. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
  214. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
  215. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
  216. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
  217. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/licenses/COPYING +0 -0
  218. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
  219. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
  220. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +0 -0
  221. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
  222. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
  223. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
  224. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
  225. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
  226. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
  227. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +0 -0
  228. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -0
  229. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
  230. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
  231. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
  232. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_400.html +0 -0
  233. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_401.html +0 -0
  234. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_404.html +0 -0
  235. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_500.html +0 -0
  236. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
  237. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
  238. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
  239. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
  240. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
  241. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_app.css +0 -0
  242. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_app.js +0 -0
  243. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
  244. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +0 -0
  245. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +0 -0
  246. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
  247. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
  248. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +0 -0
  249. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
  250. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
  251. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
  252. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
  253. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
  254. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
  255. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
  256. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_own.css +0 -0
  257. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
  258. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
  259. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
  260. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/__init__.py +0 -0
  261. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/archetype.py +0 -0
  262. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/cache_utils.py +0 -0
  263. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/ci_services.py +0 -0
  264. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/ci_workflow.py +0 -0
  265. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/command_loader.py +0 -0
  266. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  267. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/db_client.py +0 -0
  268. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/db_utils.py +0 -0
  269. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/decorators.py +0 -0
  270. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  271. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/feature_installer.py +0 -0
  272. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/feature_naming.py +0 -0
  273. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/feature_requirements.py +0 -0
  274. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/feature_resolution.py +0 -0
  275. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/feature_utils.py +0 -0
  276. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/forge.py +0 -0
  277. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/git_url.py +0 -0
  278. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/integrity.py +0 -0
  279. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/io_utils.py +0 -0
  280. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/lifecycle.py +0 -0
  281. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/manifest.py +0 -0
  282. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/menu_keys.py +0 -0
  283. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/migration_lock.py +0 -0
  284. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/path_utils.py +0 -0
  285. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/proc.py +0 -0
  286. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/refinement_facts.py +0 -0
  287. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/running_version.py +0 -0
  288. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/template_drift.py +0 -0
  289. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/toml_edit.py +0 -0
  290. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
  291. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  292. {splent_cli-1.38.0 → splent_cli-1.40.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  293. {splent_cli-1.38.0 → splent_cli-1.40.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.38.0
3
+ Version: 1.40.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
@@ -16,7 +16,7 @@ Requires-Dist: packaging>=24.0
16
16
  Requires-Dist: python-dotenv>=1.0
17
17
  Requires-Dist: pyyaml>=6.0
18
18
  Requires-Dist: requests>=2.31.0
19
- Requires-Dist: splent_framework<2,>=1.18.0
19
+ Requires-Dist: splent_framework<2,>=1.22.0
20
20
  Requires-Dist: tomli_w>=1.0.0
21
21
  Provides-Extra: dev
22
22
  Requires-Dist: setuptools==80.3.1; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.38.0"
7
+ version = "1.40.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"
@@ -24,11 +24,14 @@ dependencies = [
24
24
  "pyyaml>=6.0",
25
25
  "requests>=2.31.0",
26
26
  # The CLI drives the framework (paths, feature order, migrations, the
27
- # app factory) and only works with the same major version of it. 1.18.0
28
- # resolves a feature to the copy feature:test names (SPLENT_FEATURE_SOURCES)
29
- # and brings the production defaults the deploy templates explain; 1.17.0
30
- # the refinement rules product:validate shares with the boot.
31
- "splent_framework>=1.18.0,<2",
27
+ # app factory) and only works with the same major version of it. 1.22.0
28
+ # brings splent_framework.params, the [tool.splent.config_schema] reader
29
+ # product:validate, product:env and product:build share with the boot;
30
+ # 1.18.0 resolves a feature to the copy feature:test names
31
+ # (SPLENT_FEATURE_SOURCES) and brings the production defaults the deploy
32
+ # templates explain; 1.17.0 the refinement rules product:validate shares
33
+ # with the boot.
34
+ "splent_framework>=1.22.0,<2",
32
35
  "tomli_w>=1.0.0",
33
36
  ]
34
37
 
@@ -456,6 +456,7 @@ def feature_contract(feature_ref, write):
456
456
  # nobody would think to look.
457
457
  _check_declared_config(cache_path, inferred)
458
458
  _report_config_keys(inferred)
459
+ _report_params(cache_path, name)
459
460
  click.echo()
460
461
  return
461
462
 
@@ -468,6 +469,7 @@ def feature_contract(feature_ref, write):
468
469
  )
469
470
  _check_declared_config(cache_path, inferred)
470
471
  _report_config_keys(inferred)
472
+ _report_params(cache_path, name)
471
473
  click.echo()
472
474
  return
473
475
 
@@ -485,6 +487,7 @@ def feature_contract(feature_ref, write):
485
487
  # And whether the settings it reads are declared where a product looks
486
488
  _check_declared_config(cache_path, inferred)
487
489
  _report_config_keys(inferred)
490
+ _report_params(cache_path, name)
488
491
 
489
492
  click.echo()
490
493
 
@@ -535,7 +538,8 @@ def _check_declared_config(feature_path, inferred):
535
538
  except (OSError, tomllib.TOMLDecodeError):
536
539
  return
537
540
 
538
- declared = set(data.get("tool", {}).get("splent", {}).get("config", {}))
541
+ splent = data.get("tool", {}).get("splent", {})
542
+ declared = set(splent.get("config", {})) | set(splent.get("config_schema", {}))
539
543
  # A feature that ships a service may state a default next to its compose
540
544
  # file instead, which is equally visible to product:env --merge.
541
545
  env_example = feature_path / "docker" / ".env.example"
@@ -593,6 +597,46 @@ def _report_config_keys(inferred):
593
597
  )
594
598
 
595
599
 
600
+ def _report_params(feature_path, name):
601
+ """Say what [tool.splent.config_schema] declares, and what of it changed
602
+ since the latest release in a way that breaks a product: feature:release
603
+ refuses that change without a major bump (a minor one below 1.0.0)."""
604
+ from splent_framework.params.schema import read_pyproject
605
+
606
+ from splent_cli.utils.feature_params import release_compatibility
607
+
608
+ pyproject = feature_path / "pyproject.toml"
609
+ try:
610
+ with open(pyproject, "rb") as handle:
611
+ declared = read_pyproject(name, tomllib.load(handle))
612
+ except (OSError, tomllib.TOMLDecodeError):
613
+ return
614
+ if not declared.declared:
615
+ return
616
+ click.echo()
617
+ count = len(declared.params)
618
+ click.secho(
619
+ f" ℹ {count} parameter{'s' if count != 1 else ''} declared in "
620
+ "[tool.splent.config_schema]",
621
+ fg="cyan",
622
+ )
623
+ for error in declared.errors:
624
+ click.secho(f" ❌ [tool.splent.config_schema] {error}", fg="red")
625
+ compat = release_compatibility(str(feature_path), name)
626
+ if compat.changes:
627
+ click.secho(
628
+ f" ⚠ Since {compat.previous}, changes that break a product that "
629
+ "sets them:",
630
+ fg="yellow",
631
+ )
632
+ for change in compat.changes:
633
+ click.secho(f" {change}", fg="yellow")
634
+ click.secho(
635
+ f" The next release must be a {compat.required_bump()} version.",
636
+ dim=True,
637
+ )
638
+
639
+
596
640
  def _check_config_py(feature_path, ns, name, inferred):
597
641
  """Warn if config.py is missing or stale relative to the inferred contract."""
598
642
  from splent_cli.commands.feature.feature_inject_config import (
@@ -488,17 +488,33 @@ def _spelled_string(node, constants: dict[str, str]) -> str | None:
488
488
  return None
489
489
 
490
490
 
491
+ def _is_param_call(node) -> bool:
492
+ """Whether ``node`` is ``param("feature", "KEY")``, the reader of the
493
+ parameters a feature declares (splent_framework.params)."""
494
+ if not isinstance(node, ast.Call) or len(node.args) < 2:
495
+ return False
496
+ func = node.func
497
+ name = func.id if isinstance(func, ast.Name) else getattr(func, "attr", None)
498
+ return name == "param"
499
+
500
+
491
501
  def _config_keys(tree) -> tuple[set[str], set[str]]:
492
502
  """``(read, written)``: the app.config keys a module reads and sets.
493
503
 
494
- Read: ``.config.get("KEY")`` and ``.config["KEY"]``. Set:
495
- ``.config["KEY"] = ...``, ``.config.setdefault("KEY", ...)`` and the
496
- literal keys of ``.config.update(...)`` or ``.config.from_mapping(...)``.
504
+ Read: ``.config.get("KEY")``, ``.config["KEY"]`` and
505
+ ``param("feature", "KEY")``. Set: ``.config["KEY"] = ...``,
506
+ ``.config.setdefault("KEY", ...)`` and the literal keys of
507
+ ``.config.update(...)`` or ``.config.from_mapping(...)``.
497
508
  """
498
509
  constants = _module_string_constants(tree)
499
510
  read: set[str] = set()
500
511
  written: set[str] = set()
501
512
  for node in ast.walk(tree):
513
+ if _is_param_call(node):
514
+ key = _spelled_string(node.args[1], constants)
515
+ if key:
516
+ read.add(key)
517
+ continue
502
518
  if isinstance(node, ast.Subscript) and _is_app_config(node.value):
503
519
  key = _spelled_string(node.slice, constants)
504
520
  if key and isinstance(node.ctx, ast.Store):
@@ -737,7 +753,7 @@ def _config_keys_of(py_files) -> tuple[set[str], set[str]]:
737
753
  try:
738
754
  text = py_file.read_text()
739
755
  # Most modules never touch the config, and parsing is the cost.
740
- if ".config" not in text:
756
+ if ".config" not in text and "param(" not in text:
741
757
  continue
742
758
  tree = ast.parse(text)
743
759
  except (SyntaxError, OSError, UnicodeDecodeError):
@@ -768,12 +784,45 @@ def config_reads(
768
784
  variable holds, can only come from a product's config.py.
769
785
  """
770
786
  read, written = _config_keys_of(_feature_sources(src_dir))
787
+ read |= _template_param_reads(src_dir)
771
788
  own_short = src_dir.name.removeprefix("splent_feature_")
772
789
  taken = {key for key, shorts in (others or {}).items() if shorts - {own_short}}
773
790
  left = read - written - taken - _framework_config_keys()
774
791
  return {key for key in left if not _is_extension_key(key)}
775
792
 
776
793
 
794
+ # {{ param('media', 'MEDIA_PUBLIC_GALLERY') }} in a template.
795
+ _TEMPLATE_PARAM = re.compile(
796
+ r"""\bparam\(\s*['"][\w.]+['"]\s*,\s*['"]([A-Z][A-Z0-9_]+)['"]"""
797
+ )
798
+
799
+
800
+ def _template_param_reads(src_dir: Path) -> set[str]:
801
+ """The parameters the feature's templates read with param()."""
802
+ keys: set[str] = set()
803
+ for path in sorted(src_dir.rglob("*.html")):
804
+ if "tests" in path.relative_to(src_dir).parts[:-1]:
805
+ continue
806
+ try:
807
+ text = path.read_text(errors="replace")
808
+ except OSError:
809
+ continue
810
+ if "param(" in text:
811
+ keys.update(_TEMPLATE_PARAM.findall(text))
812
+ return keys
813
+
814
+
815
+ def declared_param_keys(feature_root: Path) -> set[str]:
816
+ """The keys the feature declares in [tool.splent.config_schema]. The
817
+ framework sets them on its behalf, so it reads them as its own."""
818
+ try:
819
+ data = tomllib.loads((Path(feature_root) / "pyproject.toml").read_text())
820
+ except (OSError, UnicodeDecodeError, tomllib.TOMLDecodeError):
821
+ return set()
822
+ schema = data.get("tool", {}).get("splent", {}).get("config_schema", {})
823
+ return set(schema) if isinstance(schema, dict) else set()
824
+
825
+
777
826
  def default_config_map() -> dict[str, frozenset[str]]:
778
827
  """:func:`build_config_map` over the workspace in reach, built once per
779
828
  process, or empty without one."""
@@ -788,7 +837,10 @@ def build_config_map(workspace: str) -> dict[str, frozenset[str]]:
788
837
  of every feature in reach took half a second, once per contract.
789
838
  """
790
839
  owners: dict[str, set[str]] = {}
791
- for short, package, _, _ in _feature_dirs_in_reach(workspace):
840
+ for short, package, feature_dir, _ in _feature_dirs_in_reach(workspace):
841
+ # A parameter the feature declares is set by the framework for it.
842
+ for key in declared_param_keys(feature_dir):
843
+ owners.setdefault(key, set()).add(short)
792
844
  if package is None:
793
845
  continue
794
846
  files = (package / "config.py", package / "__init__.py")
@@ -1383,7 +1435,11 @@ def infer_contract(
1383
1435
  # is not one a product must put in its .env.
1384
1436
  if config_map is None:
1385
1437
  config_map = default_config_map()
1386
- config_keys = sorted(config_reads(src_dir, config_map) - set(env_vars))
1438
+ config_keys = sorted(
1439
+ config_reads(src_dir, config_map)
1440
+ - set(env_vars)
1441
+ - declared_param_keys(feature_root)
1442
+ )
1387
1443
  signals_provided, signals_required = _extract_signals(src_dir / "signals.py")
1388
1444
  translations = _extract_translations(src_dir / "translations")
1389
1445
 
@@ -2111,6 +2167,10 @@ def feature_release(
2111
2167
  normalized = version.lstrip("v")
2112
2168
  tag = f"v{normalized}"
2113
2169
 
2170
+ # A parameter change that breaks a product needs the version to say so,
2171
+ # before anything is bumped, committed or tagged.
2172
+ _refuse_breaking_params(feature_path, feature_name, tag)
2173
+
2114
2174
  # Why the forge is off decides the advice: with --no-forge, release:resume
2115
2175
  # pushes the tag later; a package that does not publish to the forge
2116
2176
  # never gets its tag there, release:resume included.
@@ -2225,6 +2285,38 @@ def feature_release(
2225
2285
  ctx.invoke(feature_attach, feature_identifier=feature_ref, version=tag)
2226
2286
 
2227
2287
 
2288
+ def _refuse_breaking_params(feature_path, feature_name, tag) -> None:
2289
+ """Stop the release when the parameters the feature declares changed in
2290
+ a way that breaks a product (a key removed, a type changed, choices or a
2291
+ range narrowed, a key made required) and ``tag`` does not bump the
2292
+ version that says so: the major, or the minor below 1.0.0.
2293
+
2294
+ Compared with the pyproject.toml of the latest release tag in the
2295
+ feature's repository. A feature never released, or without a schema
2296
+ then and now, has nothing to compare.
2297
+ """
2298
+ from splent_cli.utils.feature_params import release_compatibility
2299
+
2300
+ compat = release_compatibility(feature_path, feature_name)
2301
+ if compat.allows(tag):
2302
+ return
2303
+ click.secho(
2304
+ f" error: since {compat.previous} the parameters of {feature_name} "
2305
+ "changed in a way that breaks a product that sets them:",
2306
+ fg="red",
2307
+ )
2308
+ for change in compat.changes:
2309
+ click.secho(f" {change}", fg="red")
2310
+ click.echo(
2311
+ f" {tag} keeps the {compat.required_bump()} version of "
2312
+ f"{compat.previous}. Release it with --bump {compat.required_bump()}"
2313
+ + (" (or major)" if compat.required_bump() == "minor" else "")
2314
+ + ", or keep the old declaration compatible."
2315
+ )
2316
+ click.echo(" Nothing was changed. No version bump, no commit, no tag, no release.")
2317
+ raise SystemExit(1)
2318
+
2319
+
2228
2320
  def _refuse_invalid_requirements(feature_path, namespace, feature_name) -> None:
2229
2321
  """Stop the release, before it changes anything, when the contract it
2230
2322
  would write has a requirement that does not parse (``auth>=soon``).
@@ -1132,7 +1132,15 @@ def _contracts_snapshot(workspace: str, declared_features: Sequence[str]) -> dic
1132
1132
  A feature that declares neither is still listed, so the framework knows
1133
1133
  it was looked at. One whose pyproject cannot be found is left out with a
1134
1134
  warning, and the framework then reports it as missing.
1135
+
1136
+ A feature's [tool.splent.config_schema] goes in too, as
1137
+ ``config_schema``: the image resolves and checks its parameters from it.
1138
+ Each default is filled in from [tool.splent.config], except a secret's,
1139
+ which never goes to a file baked into the image. A schema that cannot be
1140
+ used is left for the pre-flight to report; only its usable keys go in.
1135
1141
  """
1142
+ from splent_framework.params.schema import read_pyproject
1143
+
1136
1144
  features: dict[str, dict] = {}
1137
1145
  missing: list[str] = []
1138
1146
  for feat in declared_features:
@@ -1144,7 +1152,8 @@ def _contracts_snapshot(workspace: str, declared_features: Sequence[str]) -> dic
1144
1152
  if not os.path.isfile(pyproject):
1145
1153
  missing.append(name)
1146
1154
  continue
1147
- splent = load_toml(pyproject).get("tool", {}).get("splent", {})
1155
+ data = load_toml(pyproject)
1156
+ splent = data.get("tool", {}).get("splent", {})
1148
1157
  item: dict = {"entry": clean}
1149
1158
  if isinstance(splent, dict):
1150
1159
  if "refinement" in splent:
@@ -1152,6 +1161,11 @@ def _contracts_snapshot(workspace: str, declared_features: Sequence[str]) -> dic
1152
1161
  contract = splent.get("contract")
1153
1162
  if isinstance(contract, dict) and "extensible" in contract:
1154
1163
  item["extensible"] = contract["extensible"]
1164
+ declared = read_pyproject(name, data)
1165
+ if declared.declared:
1166
+ item["config_schema"] = {
1167
+ key: param.as_dict() for key, param in declared.params.items()
1168
+ }
1155
1169
  features[name] = item
1156
1170
  if missing:
1157
1171
  click.secho(
@@ -1501,7 +1515,15 @@ def write_deploy_artifacts(
1501
1515
  # defaults and loses to the product's own env file, exactly as in
1502
1516
  # product:env --merge. The two paths have to agree: a deploy template
1503
1517
  # that disagreed with dev would only be found in production.
1504
- env_result = merge_env_dicts(feature_defaults, _declared_config(pyproject_path))
1518
+ from splent_cli.utils.feature_params import product_params
1519
+
1520
+ env_result = merge_env_dicts(
1521
+ feature_defaults,
1522
+ _declared_config(
1523
+ pyproject_path,
1524
+ product_params(workspace, load_toml(pyproject_path), "prod"),
1525
+ ),
1526
+ )
1505
1527
  env_result = merge_env_dicts(env_result, product_env)
1506
1528
 
1507
1529
  # Resolve the product placeholders. A feature that talks to the product's
@@ -50,14 +50,16 @@ def _short_value(value, max_len: int = 50) -> str:
50
50
  return _truncate(repr(value), max_len)
51
51
 
52
52
 
53
- def _redacted(key: str, value) -> tuple[str, bool]:
53
+ def _redacted(key: str, value, secrets=()) -> tuple[str, bool]:
54
54
  """repr(*value*) as it may be printed, and whether anything was hidden.
55
55
 
56
- A secret-looking key's string is masked, and the password of any URI is
57
- dropped, also inside a dict or a list such as SQLALCHEMY_BINDS.
56
+ A secret-looking key's string is masked, and so is one a feature's
57
+ [tool.splent.config_schema] marks as a secret (``secrets``), and the
58
+ password of any URI is dropped, also inside a dict or a list such as
59
+ SQLALCHEMY_BINDS.
58
60
  """
59
61
  masked = False
60
- if is_secret_key(key) and isinstance(value, (str, bytes)) and value:
62
+ if is_secret_key(key, secrets) and isinstance(value, (str, bytes)) and value:
61
63
  value = mask_value(value) if isinstance(value, str) else MASK.encode()
62
64
  masked = True
63
65
  text = repr(value)
@@ -68,10 +70,13 @@ def _redacted(key: str, value) -> tuple[str, bool]:
68
70
  def _source_label(info: dict) -> str:
69
71
  """Human-readable label for who set this key."""
70
72
  source = info.get("source", "unknown")
71
- # Prettify feature import names: "splent_io.splent_feature_redis" -> "feature (splent_feature_redis)"
72
- if "." in source:
73
- parts = source.rsplit(".", 1)
74
- return f"feature ({parts[1]})"
73
+ # Prettify feature import names: "splent_io.splent_feature_redis" -> "feature (splent_feature_redis)",
74
+ # and a feature's default parameter: "... (default)" -> "feature (...) default".
75
+ # Only an import name: "product pyproject ([tool.splent.config])" has dots too.
76
+ name, default, _ = source.partition(" (default)")
77
+ if "." in name and " " not in name:
78
+ label = f"feature ({name.rsplit('.', 1)[1]})"
79
+ return f"{label} default" if default else label
75
80
  return source
76
81
 
77
82
 
@@ -120,11 +125,13 @@ def product_config(show_all, filter_feature, filter_key, show_secrets):
120
125
  """
121
126
  masked_any = False
122
127
 
128
+ secrets: set = set()
129
+
123
130
  def shown(key, value, max_len=50):
124
131
  nonlocal masked_any
125
132
  if show_secrets:
126
133
  return _short_value(value, max_len)
127
- text, changed = _redacted(key, value)
134
+ text, changed = _redacted(key, value, secrets)
128
135
  masked_any = masked_any or changed
129
136
  return _truncate(text, max_len)
130
137
 
@@ -140,6 +147,9 @@ def product_config(show_all, filter_feature, filter_key, show_secrets):
140
147
  raise click.ClickException(f"could not boot app: {exc}")
141
148
 
142
149
  trace: dict = app.extensions.get("splent_config_trace", {})
150
+ # The keys the features' schemas mark as secrets, masked like the ones
151
+ # whose name says so.
152
+ secrets |= {key for key, info in trace.items() if info.get("secret")}
143
153
 
144
154
  # Separate overwritten keys
145
155
  overwrites = {k: v for k, v in trace.items() if v.get("action") == "overwritten"}
@@ -214,6 +224,24 @@ def product_config(show_all, filter_feature, filter_key, show_secrets):
214
224
  f" {key}: {prev_val} ({prev_src}) \u2192 {new_val} ({new_src})"
215
225
  )
216
226
 
227
+ # Drift: the environment replaced what the product decided (for a
228
+ # declared parameter, one bound at derivation). Accepted, and said.
229
+ drifted = {k: v for k, v in trace.items() if v.get("drift")}
230
+ if drifted:
231
+ click.echo()
232
+ click.secho(
233
+ " \u26a0 Drift, the environment overrides what the product decided:",
234
+ fg="yellow",
235
+ bold=True,
236
+ )
237
+ for key, info in sorted(drifted.items()):
238
+ before = shown(key, info.get("overruled_value"), 30)
239
+ after = shown(key, info.get("value"), 30)
240
+ click.echo(
241
+ f" {key}: {before} ({info.get('overruled_source', '?')}) "
242
+ f"\u2192 {after} (operator environment)"
243
+ )
244
+
217
245
  if masked_any:
218
246
  click.echo()
219
247
  click.secho(
@@ -31,17 +31,35 @@ def _scalar(value) -> str | None:
31
31
  return None
32
32
 
33
33
 
34
- def _env_value(pyproject_path: str, key: str, value) -> str:
34
+ def _env_value(pyproject_path: str, key: str, value, param=None) -> str:
35
35
  """One declared value as the string the environment will carry.
36
36
 
37
37
  Raises ClickException for what an environment variable cannot hold
38
38
  without losing it: a table, a list of tables or of lists, and a list
39
39
  item with a comma in it, which the reader would split in two.
40
+
41
+ A key some feature declares in [tool.splent.config_schema] (``param``)
42
+ is written the way the framework reads it back at boot: a list item
43
+ with a comma, or a map, as JSON (splent_framework.params.schema
44
+ .env_text). A value the schema refuses raises ClickException too, so
45
+ the environment never carries what the app would refuse to start with.
40
46
  """
47
+ where = f"{key} in [tool.splent.config] of {pyproject_path}"
48
+ if param is not None:
49
+ from splent_framework.params.schema import PLACEHOLDER, ParamError, env_text
50
+
51
+ if value == PLACEHOLDER:
52
+ return value
53
+ try:
54
+ return env_text(param, value)
55
+ except ParamError as e:
56
+ raise click.ClickException(
57
+ f"{where}: {e} ({param.short} declares it in "
58
+ "[tool.splent.config_schema])"
59
+ ) from None
41
60
  text = _scalar(value)
42
61
  if text is not None:
43
62
  return text
44
- where = f"{key} in [tool.splent.config] of {pyproject_path}"
45
63
  if isinstance(value, list):
46
64
  items = [_scalar(item) for item in value]
47
65
  if any(item is None for item in items):
@@ -68,7 +86,7 @@ def _env_value(pyproject_path: str, key: str, value) -> str:
68
86
  )
69
87
 
70
88
 
71
- def _declared_config(pyproject_path: str) -> dict:
89
+ def _declared_config(pyproject_path: str, params: dict | None = None) -> dict:
72
90
  """The ``[tool.splent.config]`` block of a pyproject, as strings.
73
91
 
74
92
  A feature declares the variables it reads and what they default to; a
@@ -83,6 +101,11 @@ def _declared_config(pyproject_path: str) -> dict:
83
101
  can carry (a table, a list of tables) raises ClickException naming the
84
102
  key and the file: it used to be dropped without a word, and the feature
85
103
  then ran on its default while the pyproject said otherwise.
104
+
105
+ ``params`` are the declared parameters the values are written for, by
106
+ key (feature_params.product_params for a product's pyproject). Left
107
+ out, they are the file's own [tool.splent.config_schema], which is the
108
+ case of a feature's pyproject. A list or a map is then carried, typed.
86
109
  """
87
110
  try:
88
111
  with open(pyproject_path, "rb") as handle:
@@ -94,8 +117,15 @@ def _declared_config(pyproject_path: str) -> dict:
94
117
  if not isinstance(declared, dict):
95
118
  return {}
96
119
 
120
+ if params is None:
121
+ from splent_framework.params.schema import read_pyproject
122
+
123
+ feature = os.path.basename(os.path.dirname(pyproject_path)).split("@")[0]
124
+ params = read_pyproject(feature, data).params
125
+
97
126
  return {
98
- key: _env_value(pyproject_path, key, value) for key, value in declared.items()
127
+ key: _env_value(pyproject_path, key, value, params.get(key))
128
+ for key, value in declared.items()
99
129
  }
100
130
 
101
131
 
@@ -398,7 +428,11 @@ def product_env(generate, merge, env_name, process_all):
398
428
  k for k in defaults.declared if k not in stated_by_the_product
399
429
  ]
400
430
 
401
- product_declared = _declared_config(py_path)
431
+ from splent_cli.utils.feature_params import product_params
432
+
433
+ product_declared = _declared_config(
434
+ py_path, product_params(workspace, data, env_name)
435
+ )
402
436
  for key, value in product_declared.items():
403
437
  # Beats the feature's default, which is the point of writing it
404
438
  # here, and loses to the product's own env file, which is the