splent-cli 1.34.1__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.34.1/src/splent_cli.egg-info → splent_cli-1.36.0}/PKG-INFO +1 -1
  2. {splent_cli-1.34.1 → splent_cli-1.36.0}/pyproject.toml +1 -1
  3. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/cli.py +48 -1
  4. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_contracts.py +4 -2
  5. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/export/export_puml.py +4 -1
  6. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_add.py +2 -0
  7. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_attach.py +2 -0
  8. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_compat.py +312 -5
  9. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_contract.py +115 -55
  10. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_create.py +160 -7
  11. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_detach.py +6 -3
  12. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_drift.py +9 -134
  13. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_env.py +4 -1
  14. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_info.py +8 -3
  15. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_install.py +7 -1
  16. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_pin.py +2 -0
  17. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_refine.py +10 -1
  18. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_release.py +457 -61
  19. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_remove.py +7 -4
  20. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_rename.py +2 -0
  21. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_sync_template.py +7 -3
  22. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_test.py +97 -26
  23. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_unlock.py +2 -0
  24. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_upgrade.py +2 -0
  25. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_auto_require.py +2 -0
  26. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_configure.py +2 -0
  27. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_create.py +12 -1
  28. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_derive.py +2 -0
  29. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_drift.py +42 -0
  30. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_resolve.py +10 -1
  31. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_sync_template.py +82 -3
  32. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_validate.py +9 -1
  33. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/context.py +11 -2
  34. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/marketplace.py +38 -4
  35. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/preflight.py +4 -2
  36. splent_cli-1.36.0/src/splent_cli/services/web_container.py +66 -0
  37. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +1 -1
  38. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_config.py.j2 +2 -1
  39. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +1 -1
  40. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +1 -1
  41. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +1 -1
  42. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +1 -1
  43. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +2 -2
  44. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +4 -1
  45. splent_cli-1.36.0/src/splent_cli/templates/product/product_ci_validate.yml.j2 +201 -0
  46. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_config.py.j2 +2 -2
  47. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_own.css +1 -1
  48. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/cache_utils.py +34 -0
  49. splent_cli-1.36.0/src/splent_cli/utils/ci_services.py +506 -0
  50. splent_cli-1.36.0/src/splent_cli/utils/ci_workflow.py +338 -0
  51. splent_cli-1.36.0/src/splent_cli/utils/feature_naming.py +58 -0
  52. splent_cli-1.36.0/src/splent_cli/utils/feature_requirements.py +116 -0
  53. splent_cli-1.36.0/src/splent_cli/utils/feature_resolution.py +170 -0
  54. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/integrity.py +2 -2
  55. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/manifest.py +5 -1
  56. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/template_drift.py +68 -6
  57. {splent_cli-1.34.1 → splent_cli-1.36.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  58. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli.egg-info/SOURCES.txt +6 -0
  59. splent_cli-1.34.1/src/splent_cli/templates/product/product_ci_validate.yml.j2 +0 -50
  60. {splent_cli-1.34.1 → splent_cli-1.36.0}/COPYING +0 -0
  61. {splent_cli-1.34.1 → splent_cli-1.36.0}/COPYING.LESSER +0 -0
  62. {splent_cli-1.34.1 → splent_cli-1.36.0}/README.md +0 -0
  63. {splent_cli-1.34.1 → splent_cli-1.36.0}/setup.cfg +0 -0
  64. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/__init__.py +0 -0
  65. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/__main__.py +0 -0
  66. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/__init__.py +0 -0
  67. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  68. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  69. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  70. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  71. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  72. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  73. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  74. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  75. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  76. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/check/__init__.py +0 -0
  77. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  78. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  79. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_env.py +0 -0
  80. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_features.py +0 -0
  81. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_github.py +0 -0
  82. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  83. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_product.py +0 -0
  84. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  85. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  86. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/check/check_releases.py +0 -0
  87. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  88. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  89. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  90. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  91. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/command/__init__.py +0 -0
  92. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/command/command_create.py +0 -0
  93. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/coverage.py +0 -0
  94. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_console.py +0 -0
  95. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  96. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  97. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  98. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  99. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  100. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  101. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_status.py +0 -0
  102. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  103. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/doctor.py +0 -0
  104. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/env/env_list.py +0 -0
  105. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/env/env_set.py +0 -0
  106. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/env/env_show.py +0 -0
  107. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/export/__init__.py +0 -0
  108. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  109. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  110. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  111. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  112. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  113. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  114. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  115. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  116. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  117. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  118. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  119. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  120. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  121. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  122. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  123. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  124. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  125. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  126. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  127. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  128. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  129. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  130. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/lint.py +0 -0
  131. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/locust.py +0 -0
  132. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  133. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  134. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  135. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  136. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  137. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/__init__.py +0 -0
  138. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_build.py +0 -0
  139. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  140. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  141. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_config.py +0 -0
  142. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_console.py +0 -0
  143. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  144. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
  145. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  146. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_down.py +0 -0
  147. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_env.py +0 -0
  148. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_list.py +0 -0
  149. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  150. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  151. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_port.py +0 -0
  152. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_release.py +0 -0
  153. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  154. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  155. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_run.py +0 -0
  156. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_select.py +0 -0
  157. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  158. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  159. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_test.py +0 -0
  160. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/product/product_up.py +0 -0
  161. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/release/__init__.py +0 -0
  162. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/release/release_core.py +0 -0
  163. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/release/release_resume.py +0 -0
  164. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/selenium.py +0 -0
  165. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  166. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  167. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  168. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  169. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  170. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  171. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  172. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  173. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  174. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  175. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  176. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  177. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  178. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
  179. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
  180. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  181. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  182. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  183. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/commands/version.py +0 -0
  184. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/__init__.py +0 -0
  185. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/compose.py +0 -0
  186. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/credentials.py +0 -0
  187. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/deploy_bundle.py +0 -0
  188. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/image_registry.py +0 -0
  189. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/index_refresh.py +0 -0
  190. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/marketplace_api.py +0 -0
  191. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/marketplace_url.py +0 -0
  192. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/pypi_quota.py +0 -0
  193. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/registry.py +0 -0
  194. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/release.py +0 -0
  195. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/release_dry_run.py +0 -0
  196. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/release_gate.py +0 -0
  197. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/services/spl_store.py +0 -0
  198. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
  199. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
  200. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
  201. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
  202. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
  203. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
  204. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
  205. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
  206. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
  207. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
  208. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
  209. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
  210. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
  211. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
  212. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
  213. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
  214. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
  215. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
  216. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
  217. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
  218. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
  219. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
  220. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
  221. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
  222. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
  223. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
  224. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
  225. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
  226. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/licenses/COPYING +0 -0
  227. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
  228. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
  229. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +0 -0
  230. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
  231. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
  232. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
  233. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
  234. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
  235. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
  236. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -0
  237. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
  238. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
  239. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
  240. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_400.html +0 -0
  241. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_401.html +0 -0
  242. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_404.html +0 -0
  243. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_500.html +0 -0
  244. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
  245. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
  246. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
  247. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
  248. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
  249. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_app.css +0 -0
  250. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_app.js +0 -0
  251. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
  252. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
  253. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +0 -0
  254. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
  255. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
  256. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
  257. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
  258. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
  259. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
  260. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
  261. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
  262. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
  263. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
  264. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/__init__.py +0 -0
  265. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/archetype.py +0 -0
  266. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/command_loader.py +0 -0
  267. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  268. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/db_client.py +0 -0
  269. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/db_utils.py +0 -0
  270. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/decorators.py +0 -0
  271. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  272. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/feature_installer.py +0 -0
  273. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/feature_utils.py +0 -0
  274. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/forge.py +0 -0
  275. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/git_url.py +0 -0
  276. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/io_utils.py +0 -0
  277. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/lifecycle.py +0 -0
  278. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/path_utils.py +0 -0
  279. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/proc.py +0 -0
  280. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/redact.py +0 -0
  281. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/refinement_facts.py +0 -0
  282. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/running_version.py +0 -0
  283. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/toml_edit.py +0 -0
  284. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
  285. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  286. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  287. {splent_cli-1.34.1 → splent_cli-1.36.0}/src/splent_cli.egg-info/requires.txt +0 -0
  288. {splent_cli-1.34.1 → 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.34.1
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.34.1"
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"
@@ -11,6 +11,15 @@ from splent_cli.utils.command_loader import load_commands
11
11
  load_dotenv()
12
12
 
13
13
 
14
+ def _missing_module(exc: BaseException) -> str | None:
15
+ """The module whose absence made building the app fail, if that is why."""
16
+ while exc is not None:
17
+ if isinstance(exc, ModuleNotFoundError) and exc.name:
18
+ return exc.name
19
+ exc = exc.__cause__ or exc.__context__
20
+ return None
21
+
22
+
14
23
  class SPLENTCLI(click.Group):
15
24
  """
16
25
  Main SPLENT CLI class.
@@ -101,15 +110,53 @@ class SPLENTCLI(click.Group):
101
110
 
102
111
  # ── App context injection ─────────────────────────────────────
103
112
 
113
+ def _delegate_to_the_product(self, ctx) -> None:
114
+ """Run this command in the product's web container, and exit with its
115
+ code; return when that container is not running.
116
+
117
+ Called only when the app cannot be built here: building it imports
118
+ what the features declare, which the product's image installs and the
119
+ CLI's may not (see services/web_container.py).
120
+ """
121
+ from splent_cli.services import web_container
122
+
123
+ product = os.getenv("SPLENT_APP")
124
+ if not product or web_container.is_product_container():
125
+ return
126
+ if not web_container.running(web_container.name(product)):
127
+ return
128
+ argv = [*ctx.protected_args, *ctx.args]
129
+ raise SystemExit(web_container.delegate(product, argv))
130
+
104
131
  def invoke(self, ctx):
105
132
  cmd_name = ctx.protected_args[0] if ctx.protected_args else None
106
133
  command = self.get_command(ctx, cmd_name)
107
134
 
135
+ # A feature's own commands are only found by building the app, so
136
+ # here they may be missing although the product has them.
137
+ if command is None and cmd_name and cmd_name.startswith("feature:"):
138
+ self._delegate_to_the_product(ctx)
139
+
108
140
  if command and getattr(command, "requires_app", False):
109
141
  from splent_cli.utils.dynamic_imports import get_app
110
142
  from splent_cli.utils.db_utils import check_db_connection
111
143
 
112
- app = get_app()
144
+ try:
145
+ app = get_app()
146
+ except Exception as exc:
147
+ missing = _missing_module(exc)
148
+ if missing is None:
149
+ raise
150
+ # Only then: where the app builds here, the command runs here
151
+ # with the tools the CLI image has (ruff formats migrations).
152
+ self._delegate_to_the_product(ctx)
153
+ product = os.getenv("SPLENT_APP", "The product")
154
+ raise click.ClickException(
155
+ f"{product}'s features need {missing}, which the product's "
156
+ "image installs and the CLI's does not.\n"
157
+ " Start the product (splent product:up --dev) and run the "
158
+ "command again: it then runs in the product's web container."
159
+ ) from exc
113
160
  if getattr(command, "requires_db", False):
114
161
  if not check_db_connection(app):
115
162
  raise SystemExit(1)
@@ -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:
@@ -411,16 +426,308 @@ def _analyse_all(labeled_contracts: list[tuple[str, dict]]) -> list[dict]:
411
426
  return sorted(findings, key=lambda f: SEVERITY_ORDER[f["severity"]])
412
427
 
413
428
 
414
- def run_all_product_check(workspace: str, product_dir: str) -> list[dict]:
415
- """Run the full product compatibility check. Returns findings list."""
429
+ def _contract_env(path: Path) -> str | None:
430
+ """The environment a feature's contract confines it to, if any."""
431
+ pyproject = path / "pyproject.toml"
432
+ if not pyproject.exists():
433
+ return None
434
+ data = load_toml(pyproject, what=f"pyproject.toml for {path.name}")
435
+ return data.get("tool", {}).get("splent", {}).get("contract", {}).get("env")
436
+
437
+
438
+ def env_scope_findings(workspace: str, product_dir: str) -> list[dict]:
439
+ """Features confined to one environment, in a list that installs them in
440
+ the other.
441
+
442
+ mailhog and phpmyadmin declare env = "dev" in their contract. In the base
443
+ list the production image installed them too, phpMyAdmin with autologin
444
+ to the database, and nothing said so.
445
+ """
446
+ from splent_framework.utils.pyproject_reader import PyprojectReader
447
+
448
+ reader = PyprojectReader.for_product(product_dir)
449
+ lists = {
450
+ "features": reader.features,
451
+ "features_dev": reader.features_dev,
452
+ "features_prod": reader.features_prod,
453
+ }
454
+ findings = []
455
+ for key, entries in lists.items():
456
+ for entry in entries:
457
+ ns, name, version = _parse_pyproject_entry(entry)
458
+ path = product_feature_dir(workspace, product_dir, ns, name, version)
459
+ if path is None:
460
+ continue
461
+ scope = _contract_env(path)
462
+ short = name.removeprefix("splent_feature_")
463
+ if scope == "dev" and key != "features_dev":
464
+ message = (
465
+ f"{short} is a development tool (its contract says env = "
466
+ f'"dev"), but [tool.splent] {key} puts it in the production '
467
+ "image too. Move it to features_dev."
468
+ )
469
+ elif scope == "prod" and key == "features_dev":
470
+ message = (
471
+ f"{short} only runs in production (its contract says env = "
472
+ '"prod"), but it is in [tool.splent] features_dev. Move it '
473
+ "to features_prod."
474
+ )
475
+ else:
476
+ continue
477
+ findings.append(
478
+ {
479
+ "severity": "error",
480
+ "field": "env",
481
+ "message": message,
482
+ "features": [short],
483
+ }
484
+ )
485
+ return findings
486
+
487
+
488
+ def _joins_the_shared_network(compose_file: Path) -> bool:
489
+ """Whether a compose file puts its services on the shared splent_network
490
+ instead of the product's own (``name: ${SPLENT_NETWORK:-splent_network}``)."""
491
+ import yaml
492
+
493
+ try:
494
+ data = yaml.safe_load(compose_file.read_text()) or {}
495
+ except (OSError, yaml.YAMLError):
496
+ return False
497
+ networks = data.get("networks") or {}
498
+ spec = networks.get("splent_network")
499
+ if not isinstance(spec, dict) or not spec.get("external"):
500
+ return False
501
+ return "SPLENT_NETWORK" not in str(spec.get("name", ""))
502
+
503
+
504
+ def network_findings(workspace: str, product_dir: str) -> list[dict]:
505
+ """Feature services the product cannot reach.
506
+
507
+ Each product has a network of its own, and a feature's services join it
508
+ through SPLENT_NETWORK. mailhog and phpmyadmin named the shared network
509
+ instead, so the app could not send mail to Mailhog and phpMyAdmin could
510
+ not reach the database, and nothing said why.
511
+ """
512
+ from splent_framework.utils.pyproject_reader import PyprojectReader
513
+
514
+ reader = PyprojectReader.for_product(product_dir)
515
+ findings = []
516
+ for entry in reader.features + reader.features_dev + reader.features_prod:
517
+ ns, name, version = _parse_pyproject_entry(entry)
518
+ path = product_feature_dir(workspace, product_dir, ns, name, version)
519
+ if path is None:
520
+ continue
521
+ files = sorted((path / "docker").glob("docker-compose*.yml"))
522
+ if any(_joins_the_shared_network(f) for f in files):
523
+ short = name.removeprefix("splent_feature_")
524
+ findings.append(
525
+ {
526
+ "severity": "error",
527
+ "field": "docker",
528
+ "message": (
529
+ f"{short}'s services join the shared splent_network, "
530
+ "where the product's app cannot reach them. Its compose "
531
+ "file needs name: ${SPLENT_NETWORK:-splent_network} "
532
+ "under networks.splent_network."
533
+ ),
534
+ "features": [short],
535
+ }
536
+ )
537
+ return findings
538
+
539
+
540
+ def naming_findings(workspace: str, product_dir: str) -> list[dict]:
541
+ """Features named after a product (auth_uvlhub). A warning, so the debt
542
+ shows in every validation until the product drops them."""
543
+ from splent_framework.utils.pyproject_reader import PyprojectReader
544
+
545
+ from splent_cli.utils.feature_naming import product_suffix, product_suffix_message
546
+
547
+ reader = PyprojectReader.for_product(product_dir)
548
+ findings = []
549
+ for entry in reader.features + reader.features_dev + reader.features_prod:
550
+ _, name, _ = _parse_pyproject_entry(entry)
551
+ product = product_suffix(name, workspace)
552
+ if product:
553
+ findings.append(
554
+ {
555
+ "severity": "warning",
556
+ "field": "name",
557
+ "message": product_suffix_message(name, product),
558
+ "features": [name.removeprefix("splent_feature_")],
559
+ }
560
+ )
561
+ return findings
562
+
563
+
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
+ """
416
718
  try:
417
719
  labeled_paths = _resolve_all_product_features(product_dir, workspace)
418
720
  except SystemExit:
419
721
  return []
420
722
  if not labeled_paths:
421
723
  return []
724
+ if env is None:
725
+ env = os.getenv("SPLENT_ENV") or "dev"
422
726
  labeled_contracts = [(lbl, _read_contract(path)) for lbl, path in labeled_paths]
423
- 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"]])
424
731
 
425
732
 
426
733
  # ─────────────────────────────────────────────────────────────────────────────