splent-cli 1.35.0__tar.gz → 1.36.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 (288) hide show
  1. {splent_cli-1.35.0/src/splent_cli.egg-info → splent_cli-1.36.0}/PKG-INFO +1 -1
  2. {splent_cli-1.35.0 → splent_cli-1.36.0}/pyproject.toml +1 -1
  3. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_contracts.py +4 -2
  4. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/export/export_puml.py +4 -1
  5. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_add.py +2 -0
  6. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_attach.py +2 -0
  7. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_compat.py +177 -5
  8. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_contract.py +115 -55
  9. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_create.py +153 -7
  10. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_detach.py +2 -0
  11. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_drift.py +9 -134
  12. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_info.py +8 -3
  13. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_install.py +7 -1
  14. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_pin.py +2 -0
  15. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_refine.py +3 -1
  16. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_release.py +457 -61
  17. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_remove.py +2 -0
  18. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_rename.py +2 -0
  19. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_sync_template.py +7 -3
  20. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_test.py +92 -6
  21. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_unlock.py +2 -0
  22. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_upgrade.py +2 -0
  23. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_auto_require.py +2 -0
  24. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_configure.py +2 -0
  25. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_create.py +12 -1
  26. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_derive.py +2 -0
  27. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_drift.py +42 -0
  28. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_resolve.py +2 -0
  29. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_sync_template.py +61 -4
  30. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/context.py +11 -2
  31. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/marketplace.py +38 -4
  32. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/preflight.py +4 -2
  33. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +1 -1
  34. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_config.py.j2 +2 -1
  35. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +1 -1
  36. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +1 -1
  37. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +1 -1
  38. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +1 -1
  39. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +2 -2
  40. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +4 -1
  41. splent_cli-1.36.0/src/splent_cli/templates/product/product_ci_validate.yml.j2 +201 -0
  42. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_config.py.j2 +2 -2
  43. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_own.css +1 -1
  44. splent_cli-1.36.0/src/splent_cli/utils/ci_services.py +506 -0
  45. splent_cli-1.36.0/src/splent_cli/utils/ci_workflow.py +338 -0
  46. splent_cli-1.36.0/src/splent_cli/utils/feature_requirements.py +116 -0
  47. splent_cli-1.36.0/src/splent_cli/utils/feature_resolution.py +170 -0
  48. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/manifest.py +5 -1
  49. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/template_drift.py +68 -6
  50. {splent_cli-1.35.0 → splent_cli-1.36.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  51. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli.egg-info/SOURCES.txt +4 -0
  52. splent_cli-1.35.0/src/splent_cli/templates/product/product_ci_validate.yml.j2 +0 -65
  53. {splent_cli-1.35.0 → splent_cli-1.36.0}/COPYING +0 -0
  54. {splent_cli-1.35.0 → splent_cli-1.36.0}/COPYING.LESSER +0 -0
  55. {splent_cli-1.35.0 → splent_cli-1.36.0}/README.md +0 -0
  56. {splent_cli-1.35.0 → splent_cli-1.36.0}/setup.cfg +0 -0
  57. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/__init__.py +0 -0
  58. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/__main__.py +0 -0
  59. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/cli.py +0 -0
  60. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/__init__.py +0 -0
  61. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  62. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  63. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  64. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  65. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  66. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  67. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  68. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  69. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  70. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/check/__init__.py +0 -0
  71. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  72. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  73. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_env.py +0 -0
  74. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_features.py +0 -0
  75. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_github.py +0 -0
  76. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  77. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_product.py +0 -0
  78. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  79. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  80. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_releases.py +0 -0
  81. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  82. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  83. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  84. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  85. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/command/__init__.py +0 -0
  86. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/command/command_create.py +0 -0
  87. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/coverage.py +0 -0
  88. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_console.py +0 -0
  89. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  90. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  91. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  92. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  93. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  94. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  95. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_status.py +0 -0
  96. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  97. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/doctor.py +0 -0
  98. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/env/env_list.py +0 -0
  99. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/env/env_set.py +0 -0
  100. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/env/env_show.py +0 -0
  101. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/export/__init__.py +0 -0
  102. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  103. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  104. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  105. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  106. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  107. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  108. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  109. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  110. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  111. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  112. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  113. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  114. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  115. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  116. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  117. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  118. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  119. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  120. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  121. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  122. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  123. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  124. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  125. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/lint.py +0 -0
  126. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/locust.py +0 -0
  127. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  128. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  129. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  130. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  131. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  132. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/__init__.py +0 -0
  133. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_build.py +0 -0
  134. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  135. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  136. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_config.py +0 -0
  137. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_console.py +0 -0
  138. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  139. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
  140. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  141. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_down.py +0 -0
  142. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_env.py +0 -0
  143. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_list.py +0 -0
  144. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  145. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  146. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_port.py +0 -0
  147. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_release.py +0 -0
  148. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  149. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  150. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_run.py +0 -0
  151. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_select.py +0 -0
  152. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  153. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  154. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_test.py +0 -0
  155. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_up.py +0 -0
  156. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_validate.py +0 -0
  157. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/release/__init__.py +0 -0
  158. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/release/release_core.py +0 -0
  159. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/release/release_resume.py +0 -0
  160. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/selenium.py +0 -0
  161. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  162. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  163. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  164. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  165. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  166. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  167. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  168. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  169. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  170. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  171. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  172. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  173. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  174. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
  175. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
  176. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  177. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  178. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  179. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/commands/version.py +0 -0
  180. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/__init__.py +0 -0
  181. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/compose.py +0 -0
  182. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/credentials.py +0 -0
  183. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/deploy_bundle.py +0 -0
  184. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/image_registry.py +0 -0
  185. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/index_refresh.py +0 -0
  186. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/marketplace_api.py +0 -0
  187. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/marketplace_url.py +0 -0
  188. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/pypi_quota.py +0 -0
  189. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/registry.py +0 -0
  190. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/release.py +0 -0
  191. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/release_dry_run.py +0 -0
  192. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/release_gate.py +0 -0
  193. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/spl_store.py +0 -0
  194. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/services/web_container.py +0 -0
  195. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
  196. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
  197. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
  198. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
  199. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
  200. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
  201. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
  202. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
  203. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
  204. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
  205. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
  206. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
  207. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
  208. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
  209. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
  210. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
  211. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
  212. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
  213. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
  214. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
  215. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
  216. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
  217. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
  218. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
  219. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
  220. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
  221. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
  222. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
  223. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/licenses/COPYING +0 -0
  224. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
  225. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
  226. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +0 -0
  227. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
  228. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
  229. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
  230. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
  231. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
  232. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
  233. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -0
  234. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
  235. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
  236. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
  237. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_400.html +0 -0
  238. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_401.html +0 -0
  239. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_404.html +0 -0
  240. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_500.html +0 -0
  241. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
  242. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
  243. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
  244. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
  245. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
  246. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_app.css +0 -0
  247. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_app.js +0 -0
  248. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
  249. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
  250. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +0 -0
  251. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
  252. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
  253. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
  254. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
  255. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
  256. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
  257. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
  258. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
  259. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
  260. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
  261. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/__init__.py +0 -0
  262. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/archetype.py +0 -0
  263. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/cache_utils.py +0 -0
  264. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/command_loader.py +0 -0
  265. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  266. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/db_client.py +0 -0
  267. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/db_utils.py +0 -0
  268. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/decorators.py +0 -0
  269. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  270. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/feature_installer.py +0 -0
  271. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/feature_naming.py +0 -0
  272. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/feature_utils.py +0 -0
  273. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/forge.py +0 -0
  274. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/git_url.py +0 -0
  275. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/integrity.py +0 -0
  276. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/io_utils.py +0 -0
  277. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/lifecycle.py +0 -0
  278. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/path_utils.py +0 -0
  279. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/proc.py +0 -0
  280. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/redact.py +0 -0
  281. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/refinement_facts.py +0 -0
  282. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/running_version.py +0 -0
  283. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/toml_edit.py +0 -0
  284. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
  285. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  286. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  287. {splent_cli-1.35.0 → splent_cli-1.36.0}/src/splent_cli.egg-info/requires.txt +0 -0
  288. {splent_cli-1.35.0 → splent_cli-1.36.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.35.0
3
+ Version: 1.36.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.35.0"
7
+ version = "1.36.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"
@@ -18,6 +18,7 @@ import tomllib
18
18
 
19
19
  from splent_cli.services import context, marketplace, spl_store
20
20
  from splent_cli.utils import forge
21
+ from splent_cli.utils.feature_requirements import requirement_names
21
22
 
22
23
 
23
24
  def _snapshot_version_key(pyproject_path: Path) -> tuple:
@@ -58,9 +59,10 @@ def _local_contract_requires(workspace: str, package: str) -> list[str] | None:
58
59
  .get("contract", {})
59
60
  .get("requires", {})
60
61
  )
62
+ # Names only: an entry may carry a version specifier ("auth>=1.20.0").
61
63
  return {
62
- "features": requires.get("features", []),
63
- "optional": requires.get("features_optional", []),
64
+ "features": requirement_names(requires.get("features", [])),
65
+ "optional": requirement_names(requires.get("features_optional", [])),
64
66
  }
65
67
  return None
66
68
 
@@ -22,6 +22,7 @@ import click
22
22
  from splent_cli.commands.uvl.uvl_utils import extract_implications_from_uvl_text
23
23
  from splent_cli.services import context
24
24
  from splent_cli.utils import proc
25
+ from splent_cli.utils.feature_requirements import requirement_names
25
26
  from splent_cli.utils.feature_utils import normalize_namespace
26
27
  from splent_framework.utils.pyproject_reader import PyprojectReader
27
28
 
@@ -381,7 +382,9 @@ def _generate_feature_puml(product_name: str, uvl_data: dict, contracts: dict) -
381
382
  env_vars = requires.get("env_vars", [])
382
383
  if env_vars:
383
384
  details.append(f"**Env vars**: {_puml_join(env_vars)}")
384
- req_features = requires.get("features", [])
385
+ # Names only: an entry may carry a version specifier
386
+ # ("auth>=1.20.0"), which the diagram has no use for.
387
+ req_features = requirement_names(requires.get("features", []))
385
388
  if req_features:
386
389
  details.append(f"**Requires**: {_puml_join(req_features)}")
387
390
 
@@ -16,6 +16,7 @@ from splent_cli.utils.feature_utils import (
16
16
  from splent_cli.utils.io_utils import load_toml
17
17
  from splent_cli.utils.toml_edit import write_toml
18
18
  from splent_cli.utils.manifest import feature_key, remove_feature, set_feature_state
19
+ from splent_cli.utils.ci_workflow import keeps_ci_workflows
19
20
 
20
21
 
21
22
  @click.command(
@@ -35,6 +36,7 @@ from splent_cli.utils.manifest import feature_key, remove_feature, set_feature_s
35
36
  flag_value="prod",
36
37
  help="Add to features_prod (production only).",
37
38
  )
39
+ @keeps_ci_workflows
38
40
  def feature_add(full_name, env_scope):
39
41
  """
40
42
  Adds a local feature (no version, no repo) to the current SPLENT product.
@@ -17,6 +17,7 @@ from splent_cli.utils.feature_utils import (
17
17
  from splent_cli.utils.io_utils import load_toml
18
18
  from splent_cli.utils.toml_edit import write_toml
19
19
  from splent_cli.utils.manifest import feature_key, remove_feature, set_feature_state
20
+ from splent_cli.utils.ci_workflow import keeps_ci_workflows
20
21
 
21
22
 
22
23
  def _target_lists(env_scope, declared):
@@ -50,6 +51,7 @@ def _target_lists(env_scope, declared):
50
51
  flag_value="prod",
51
52
  help="Add to features_prod (production only).",
52
53
  )
54
+ @keeps_ci_workflows
53
55
  def feature_attach(feature_identifier, version, env_scope):
54
56
  """
55
57
  Attach a cached feature version to the current product.
@@ -11,6 +11,11 @@ import click
11
11
  from pathlib import Path
12
12
 
13
13
  from splent_cli.services import context
14
+ from splent_cli.utils.feature_requirements import (
15
+ parse_requirement,
16
+ requirement_names,
17
+ satisfies,
18
+ )
14
19
  from splent_cli.utils.feature_utils import normalize_namespace
15
20
  from splent_cli.utils.io_utils import load_toml
16
21
 
@@ -118,7 +123,9 @@ def _read_contract(cache_path: Path) -> dict:
118
123
  "docker": raw.get("provides", {}).get("docker", []),
119
124
  },
120
125
  "requires": {
121
- "features": raw.get("requires", {}).get("features", []),
126
+ # Names only: an entry may carry a version specifier
127
+ # ("auth>=1.20.0"), which version_floor_findings reads.
128
+ "features": requirement_names(raw.get("requires", {}).get("features", [])),
122
129
  "env_vars": raw.get("requires", {}).get("env_vars", []),
123
130
  },
124
131
  }
@@ -266,8 +273,16 @@ def _resolve_all_product_features(
266
273
  if not features_raw:
267
274
  raise SystemExit(" No features declared in pyproject.toml.")
268
275
 
276
+ return _resolve_entries(features_raw, product_dir, workspace)
277
+
278
+
279
+ def _resolve_entries(
280
+ entries: list[str], product_dir: str, workspace: str
281
+ ) -> list[tuple[str, Path]]:
282
+ """Feature entries of the product as ``(label, path)``, skipping, with a
283
+ warning, those that are not in the workspace."""
269
284
  resolved = []
270
- for entry in features_raw:
285
+ for entry in entries:
271
286
  ns, name, version = _parse_pyproject_entry(entry)
272
287
  label = f"{name}@{version}" if version else name
273
288
  if version:
@@ -546,16 +561,173 @@ def naming_findings(workspace: str, product_dir: str) -> list[dict]:
546
561
  return findings
547
562
 
548
563
 
549
- def run_all_product_check(workspace: str, product_dir: str) -> list[dict]:
550
- """Run the full product compatibility check. Returns findings list."""
564
+ def _written_requires(path: Path) -> dict[str, list]:
565
+ """The requires lists of a feature's contract, entries as written."""
566
+ pyproject = path / "pyproject.toml"
567
+ if not pyproject.exists():
568
+ return {}
569
+ data = load_toml(pyproject, what=f"pyproject.toml for {path.name}")
570
+ requires = (
571
+ data.get("tool", {}).get("splent", {}).get("contract", {}).get("requires", {})
572
+ )
573
+ if not isinstance(requires, dict):
574
+ return {}
575
+ return {
576
+ key: requires.get(key)
577
+ for key in ("features", "features_manual", "features_optional")
578
+ if isinstance(requires.get(key), list)
579
+ }
580
+
581
+
582
+ def _project_version(path: Path) -> str | None:
583
+ pyproject = path / "pyproject.toml"
584
+ if not pyproject.exists():
585
+ return None
586
+ data = load_toml(pyproject, what=f"pyproject.toml for {path.name}")
587
+ version = data.get("project", {}).get("version")
588
+ return str(version) if version else None
589
+
590
+
591
+ def version_floor_findings(labeled_paths: list[tuple[str, Path]]) -> list[dict]:
592
+ """Requirements whose version specifier the product's copy does not meet.
593
+
594
+ A requires entry may say which releases of another feature it works with
595
+ (``auth>=1.20.0``), because it calls an API that release added. Against
596
+ a version the product pins, a miss is an error: the product would run a
597
+ release the feature was never written for. Against an editable feature
598
+ it is a warning, read from the working copy's ``[project].version``,
599
+ since that copy is not released yet. A specifier that is not PEP 440 is
600
+ an error, and a feature the product lacks is reported elsewhere.
601
+ """
602
+ present: dict[str, tuple[str, Path, str | None]] = {}
603
+ for label, path in labeled_paths:
604
+ name, _, version = label.partition("@")
605
+ present[name.removeprefix("splent_feature_")] = (label, path, version or None)
606
+
607
+ findings: list[dict] = []
608
+ for label, path in labeled_paths:
609
+ short = label.split("@", 1)[0].removeprefix("splent_feature_")
610
+ seen: set[tuple[str, str]] = set()
611
+ for key, entries in _written_requires(path).items():
612
+ for entry in entries:
613
+ try:
614
+ req = parse_requirement(entry)
615
+ except ValueError as e:
616
+ findings.append(
617
+ {
618
+ "severity": "error",
619
+ "field": "requires",
620
+ "values": [str(entry)],
621
+ "features": [label],
622
+ "message": f"In {short}'s requires.{key}, {e}.",
623
+ }
624
+ )
625
+ continue
626
+ if not req.specifier or (req.name, req.specifier) in seen:
627
+ continue
628
+ seen.add((req.name, req.specifier))
629
+ target = present.get(req.name)
630
+ if target is None:
631
+ continue
632
+ target_label, target_path, pinned = target
633
+ if pinned:
634
+ met = satisfies(req.specifier, pinned)
635
+ if met is None:
636
+ findings.append(
637
+ {
638
+ "severity": "warning",
639
+ "field": "requires",
640
+ "values": [str(req)],
641
+ "features": [label, target_label],
642
+ "message": (
643
+ f"{short} requires {req}, and the product pins "
644
+ f"{req.name} at {pinned}, which is not a "
645
+ "version that can be compared."
646
+ ),
647
+ }
648
+ )
649
+ elif not met:
650
+ findings.append(
651
+ {
652
+ "severity": "error",
653
+ "field": "requires",
654
+ "values": [str(req)],
655
+ "features": [label, target_label],
656
+ "message": (
657
+ f"{short} requires {req}, but the product pins "
658
+ f"{req.name} at {pinned}. Pin a release of "
659
+ f"{req.name} that satisfies it."
660
+ ),
661
+ }
662
+ )
663
+ continue
664
+ working = _project_version(target_path)
665
+ met = satisfies(req.specifier, working) if working else None
666
+ if met is False or met is None:
667
+ found = (
668
+ f"the working copy of {req.name} is {working}"
669
+ if working
670
+ else f"the version of the working copy of {req.name} "
671
+ "cannot be read"
672
+ )
673
+ findings.append(
674
+ {
675
+ "severity": "warning",
676
+ "field": "requires",
677
+ "values": [str(req)],
678
+ "features": [label, target_label],
679
+ "message": (
680
+ f"{short} requires {req}, and {found} "
681
+ f"([project].version). Release {req.name} at a "
682
+ "version that satisfies it before the product "
683
+ "pins it."
684
+ ),
685
+ }
686
+ )
687
+ return findings
688
+
689
+
690
+ def _environment_only_features(
691
+ product_dir: str, workspace: str, env: str | None
692
+ ) -> list[tuple[str, Path]]:
693
+ """The features ``features_<env>`` adds to the base list, resolved."""
694
+ from splent_framework.utils.pyproject_reader import PyprojectReader
695
+
696
+ env = context.normalize_env(env)
697
+ if not env:
698
+ return []
699
+ try:
700
+ reader = PyprojectReader.for_product(product_dir)
701
+ base = set(reader.features)
702
+ extra = [e for e in reader.features_for_env(env) if e not in base]
703
+ except (FileNotFoundError, ValueError):
704
+ return []
705
+ return _resolve_entries(extra, product_dir, workspace)
706
+
707
+
708
+ def run_all_product_check(
709
+ workspace: str, product_dir: str, env: str | None = None
710
+ ) -> list[dict]:
711
+ """Run the full product compatibility check. Returns findings list.
712
+
713
+ The collisions are checked between the base features. The version
714
+ floors are checked across the features the product loads in ``env``
715
+ (SPLENT_ENV when None, ``dev`` when that is unset too), base plus
716
+ ``features_dev`` or ``features_prod``, the selection phase 1 checks.
717
+ """
551
718
  try:
552
719
  labeled_paths = _resolve_all_product_features(product_dir, workspace)
553
720
  except SystemExit:
554
721
  return []
555
722
  if not labeled_paths:
556
723
  return []
724
+ if env is None:
725
+ env = os.getenv("SPLENT_ENV") or "dev"
557
726
  labeled_contracts = [(lbl, _read_contract(path)) for lbl, path in labeled_paths]
558
- return _analyse_all(labeled_contracts)
727
+ findings = _analyse_all(labeled_contracts) + version_floor_findings(
728
+ labeled_paths + _environment_only_features(product_dir, workspace, env)
729
+ )
730
+ return sorted(findings, key=lambda f: SEVERITY_ORDER[f["severity"]])
559
731
 
560
732
 
561
733
  # ─────────────────────────────────────────────────────────────────────────────
@@ -20,7 +20,13 @@ import click
20
20
  from pathlib import Path
21
21
 
22
22
  from splent_cli.services import context
23
- from splent_cli.commands.feature.feature_release import infer_contract, write_contract
23
+ from splent_cli.commands.feature.feature_release import (
24
+ infer_contract,
25
+ merge_preserved,
26
+ requirement_problems,
27
+ write_contract,
28
+ )
29
+ from splent_cli.utils.feature_resolution import package_identifier, resolve_feature_ref
24
30
  from splent_cli.utils.feature_utils import normalize_namespace
25
31
  from splent_cli.utils.contract_freshness import mark_contract_fresh
26
32
  from splent_cli.utils.io_utils import load_toml
@@ -51,39 +57,24 @@ def update_contract(feature_path: str, namespace: str, feature_name: str) -> dic
51
57
 
52
58
 
53
59
  def _resolve_feature(feature_ref: str, workspace: str) -> tuple[Path, str, str]:
54
- """Resolve a feature: workspace root first, then cache."""
55
- if "/" in feature_ref:
56
- ns_raw, rest = feature_ref.split("/", 1)
57
- ns = normalize_namespace(ns_raw)
58
- else:
59
- ns = DEFAULT_NAMESPACE
60
- rest = feature_ref
61
-
62
- name, _, version = rest.partition("@")
63
-
64
- # 1. Workspace root (editable features)
65
- workspace_path = Path(workspace) / name
66
- if workspace_path.exists():
67
- return workspace_path, ns, name
68
-
69
- # 2. Cache (pinned features)
70
- cache_base = Path(workspace) / ".splent_cache" / "features" / ns
71
-
72
- if version:
73
- cache_path = cache_base / f"{name}@{version}"
74
- else:
75
- cache_path = cache_base / name
76
- if not cache_path.exists():
77
- candidates = sorted(cache_base.glob(f"{name}@*"))
78
- if candidates:
79
- cache_path = candidates[-1]
80
-
81
- if not cache_path.exists():
60
+ """Resolve a feature as feature:drift does, see
61
+ :mod:`splent_cli.utils.feature_resolution`, or exit naming where it was
62
+ looked for."""
63
+ found = resolve_feature_ref(feature_ref, Path(workspace))
64
+ if found is None:
65
+ identifier = package_identifier(feature_ref)
66
+ base, at, version = identifier.partition("@")
67
+ ns_raw, slash, name = base.rpartition("/")
68
+ ns = normalize_namespace(ns_raw) if slash else DEFAULT_NAMESPACE
69
+ wanted = f"{name}@{version}" if at else name
82
70
  raise SystemExit(
83
- f"❌ Feature not found: {cache_path}\n"
71
+ f"❌ Feature not found: {wanted}\n"
72
+ f" Looked for it at the workspace root and in "
73
+ f".splent_cache/features/{ns}/.\n"
84
74
  f" Run: splent feature:clone {ns.replace('_', '-')}/{name}"
85
75
  )
86
- return cache_path, ns, name
76
+ path, ns, name = found
77
+ return path, ns, name
87
78
 
88
79
 
89
80
  def _in_cache(path: Path, workspace: str) -> bool:
@@ -101,6 +92,15 @@ def _in_cache(path: Path, workspace: str) -> bool:
101
92
  return True
102
93
 
103
94
 
95
+ def _written_contract(pyproject_path: Path) -> dict:
96
+ """The ``[tool.splent.contract]`` table as written, or empty."""
97
+ if not pyproject_path.exists():
98
+ return {}
99
+ data = load_toml(pyproject_path, what="pyproject.toml")
100
+ raw = data.get("tool", {}).get("splent", {}).get("contract", {})
101
+ return raw if isinstance(raw, dict) else {}
102
+
103
+
104
104
  def _read_current_contract(pyproject_path: Path) -> dict:
105
105
  """Read the contract block currently written in pyproject.toml."""
106
106
  if not pyproject_path.exists():
@@ -116,6 +116,7 @@ def _read_current_contract(pyproject_path: Path) -> dict:
116
116
  "blueprints": raw.get("provides", {}).get("blueprints", []),
117
117
  "models": raw.get("provides", {}).get("models", []),
118
118
  "commands": raw.get("provides", {}).get("commands", []),
119
+ "commands_help": raw.get("provides", {}).get("commands_help", {}),
119
120
  "hooks": raw.get("provides", {}).get("hooks", []),
120
121
  "services": raw.get("provides", {}).get("services", []),
121
122
  "signals": raw.get("provides", {}).get("signals", []),
@@ -153,6 +154,44 @@ def _diff_field(label: str, old: list, new: list) -> list[str]:
153
154
  return lines
154
155
 
155
156
 
157
+ def _help_entries(table) -> dict[str, tuple[str, str]]:
158
+ """``provides.commands_help`` as ``{name: (usage, help)}``."""
159
+ if not isinstance(table, dict):
160
+ return {}
161
+ entries = {}
162
+ for name, entry in table.items():
163
+ entry = entry if isinstance(entry, dict) else {}
164
+ entries[str(name)] = (
165
+ str(entry.get("usage", "") or ""),
166
+ str(entry.get("help", "") or ""),
167
+ )
168
+ return entries
169
+
170
+
171
+ def _diff_commands_help(old_table, new_table) -> list[str]:
172
+ """What a rewrite changes in ``provides.commands_help``: the usage and
173
+ the help line of each command, which come from its decorators and its
174
+ docstring."""
175
+ old, new = _help_entries(old_table), _help_entries(new_table)
176
+ label = "provides.commands_help"
177
+ lines = []
178
+ for name in sorted(set(old) | set(new)):
179
+ if name not in old:
180
+ lines.append(click.style(f" + {label}: {name}", fg="green"))
181
+ elif name not in new:
182
+ lines.append(click.style(f" - {label}: {name}", fg="red"))
183
+ else:
184
+ for field, before, after in zip(("usage", "help"), old[name], new[name]):
185
+ if before != after:
186
+ lines.append(
187
+ click.style(
188
+ f" ~ {label}.{name}.{field}: {before!r} → {after!r}",
189
+ fg="green",
190
+ )
191
+ )
192
+ return lines
193
+
194
+
156
195
  def _print_contract(contract: dict, feature_name: str) -> None:
157
196
  """Pretty-print an inferred contract dict."""
158
197
 
@@ -264,6 +303,13 @@ def _print_diff(current: dict, inferred: dict) -> bool:
264
303
  diff_lines.extend(
265
304
  _diff_field(label, current.get(key, []), inferred.get(key, []))
266
305
  )
306
+ # A changed docstring changes only this table, which --write rewrites
307
+ # too: compared here, or the contract never got it.
308
+ diff_lines.extend(
309
+ _diff_commands_help(
310
+ current.get("commands_help", {}), inferred.get("commands_help", {})
311
+ )
312
+ )
267
313
 
268
314
  if not diff_lines:
269
315
  return False
@@ -304,9 +350,14 @@ def feature_contract(feature_ref, write):
304
350
  Use --write to update [tool.splent.contract] in pyproject.toml without
305
351
  touching version, git tags, or publishing anything.
306
352
 
353
+ \b
354
+ FEATURE_REF is a short name (notes), a package name
355
+ (splent_feature_notes), either one under a namespace (splent-io/notes),
356
+ or a pinned snapshot (splent-io/notes@v1.2.0).
357
+
307
358
  \b
308
359
  Examples:
309
- splent feature:contract splent_feature_notes
360
+ splent feature:contract notes
310
361
  splent feature:contract splent_feature_notes --write
311
362
  splent feature:contract splent-io/splent_feature_notes --write
312
363
  """
@@ -333,33 +384,42 @@ def feature_contract(feature_ref, write):
333
384
  click.echo(" 🔍 Scanning source code…")
334
385
  inferred = infer_contract(str(cache_path), ns, name)
335
386
 
336
- # Deps declared in requires.features_manual are preserved by
337
- # write_contract; merge them here too so the dry-run diff shows exactly
338
- # what --write would produce (including real drops of stale inferred deps).
387
+ # What --write keeps from the written contract (features_manual,
388
+ # routes_manual, features_optional) is merged here the same way, so the
389
+ # dry-run diff shows exactly what --write would produce, including real
390
+ # drops of stale inferred deps.
339
391
  current = _read_current_contract(pyproject_path)
340
- manual = sorted(set(current.get("requires_features_manual", [])))
341
- if manual:
342
- inferred["requires_features"] = sorted(
343
- set(inferred["requires_features"]) | set(manual)
344
- )
345
- click.echo(
346
- click.style(" Preserving requires.features_manual: ", fg="bright_black")
347
- + ", ".join(manual)
348
- )
392
+ scanned_optional = set(inferred.get("requires_features_optional", []))
393
+ inferred = merge_preserved(inferred, _written_contract(pyproject_path))
394
+ for label, values in (
395
+ ("requires.features_manual", inferred["requires_features_manual"]),
396
+ ("provides.routes_manual", inferred["routes_manual"]),
397
+ (
398
+ "requires.features_optional the scan does not find",
399
+ sorted(set(inferred["requires_features_optional"]) - scanned_optional),
400
+ ),
401
+ ):
402
+ if values:
403
+ click.echo(
404
+ click.style(f" Preserving {label}: ", fg="bright_black")
405
+ + ", ".join(values)
406
+ )
349
407
 
350
- # Routes a feature builds at registration time, from configuration, are
351
- # invisible to a source scan: the rule is assembled from app.config, so
352
- # there is no literal path to find. A feature that serves its pages
353
- # under a word the product chooses would otherwise publish a contract
354
- # claiming it serves nothing, or only its admin screens, which is worse
355
- # than an empty contract because it reads as complete.
356
- manual_routes = sorted(set(current.get("routes_manual", [])))
357
- if manual_routes:
358
- inferred["routes"] = sorted(set(inferred["routes"]) | set(manual_routes))
408
+ # A version specifier is written by hand after a name. One that is not
409
+ # PEP 440 would break every reader of the contract, so it is refused
410
+ # here, in the dry run too, before anyone publishes it.
411
+ problems = requirement_problems(inferred)
412
+ if problems:
413
+ click.secho(
414
+ " ❌ These requirements are not valid, so the contract cannot be written:",
415
+ fg="red",
416
+ )
417
+ for problem in problems:
418
+ click.secho(f" {problem}", fg="red")
359
419
  click.echo(
360
- click.style(" Preserving provides.routes_manual: ", fg="bright_black")
361
- + ", ".join(manual_routes)
420
+ ' Write a short name with an optional PEP 440 specifier, such as "auth>=1.20.0".'
362
421
  )
422
+ raise SystemExit(1)
363
423
 
364
424
  # Show inferred contract
365
425
  click.echo(click.style(" Inferred contract:", bold=True))