splent-cli 1.44.0__tar.gz → 1.45.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 (306) hide show
  1. {splent_cli-1.44.0/src/splent_cli.egg-info → splent_cli-1.45.0}/PKG-INFO +1 -1
  2. {splent_cli-1.44.0 → splent_cli-1.45.0}/pyproject.toml +1 -1
  3. splent_cli-1.45.0/src/splent_cli/__init__.py +42 -0
  4. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/cli.py +6 -0
  5. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/check/check_env.py +4 -2
  6. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/env/env_set.py +6 -0
  7. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_attach.py +28 -2
  8. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_compat.py +37 -23
  9. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_contract.py +45 -7
  10. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_release.py +157 -6
  11. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_test.py +6 -61
  12. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_unlock.py +13 -1
  13. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +33 -0
  14. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_build.py +9 -4
  15. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_config.py +2 -0
  16. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_console.py +2 -0
  17. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_derive.py +34 -1
  18. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_restart.py +8 -1
  19. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_routes.py +2 -0
  20. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_signals.py +2 -0
  21. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_validate.py +132 -31
  22. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/release/release_core.py +7 -1
  23. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/spl_publish.py +16 -3
  24. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/version.py +10 -8
  25. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/deploy_bundle.py +6 -6
  26. splent_cli-1.45.0/src/splent_cli/services/editable_refresh.py +129 -0
  27. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/marketplace_api.py +11 -1
  28. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/neutral_app.py +65 -9
  29. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/preflight.py +4 -1
  30. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/release.py +42 -0
  31. splent_cli-1.45.0/src/splent_cli/services/web_container.py +146 -0
  32. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +21 -2
  33. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +11 -0
  34. splent_cli-1.45.0/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +28 -0
  35. splent_cli-1.45.0/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +51 -0
  36. splent_cli-1.45.0/src/splent_cli/templates/product/product_base_template.html +200 -0
  37. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +6 -0
  38. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/archetype.py +18 -3
  39. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/ci_workflow.py +17 -0
  40. splent_cli-1.45.0/src/splent_cli/utils/decorators.py +34 -0
  41. splent_cli-1.45.0/src/splent_cli/utils/deploy_freshness.py +183 -0
  42. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/forge.py +26 -0
  43. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/integrity.py +5 -0
  44. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/io_utils.py +86 -0
  45. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/menu_keys.py +3 -0
  46. splent_cli-1.45.0/src/splent_cli/utils/running_version.py +94 -0
  47. splent_cli-1.45.0/src/splent_cli/utils/uvl_diagnosis.py +190 -0
  48. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/uvl_params.py +20 -6
  49. {splent_cli-1.44.0 → splent_cli-1.45.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  50. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli.egg-info/SOURCES.txt +3 -0
  51. splent_cli-1.44.0/src/splent_cli/__init__.py +0 -15
  52. splent_cli-1.44.0/src/splent_cli/services/web_container.py +0 -69
  53. splent_cli-1.44.0/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -16
  54. splent_cli-1.44.0/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -18
  55. splent_cli-1.44.0/src/splent_cli/templates/product/product_base_template.html +0 -163
  56. splent_cli-1.44.0/src/splent_cli/utils/decorators.py +0 -17
  57. splent_cli-1.44.0/src/splent_cli/utils/running_version.py +0 -28
  58. {splent_cli-1.44.0 → splent_cli-1.45.0}/COPYING +0 -0
  59. {splent_cli-1.44.0 → splent_cli-1.45.0}/COPYING.LESSER +0 -0
  60. {splent_cli-1.44.0 → splent_cli-1.45.0}/README.md +0 -0
  61. {splent_cli-1.44.0 → splent_cli-1.45.0}/setup.cfg +0 -0
  62. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/__main__.py +0 -0
  63. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/__init__.py +0 -0
  64. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  65. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  66. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  67. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  68. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  69. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  70. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  71. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  72. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  73. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/check/__init__.py +0 -0
  74. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  75. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  76. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  77. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/check/check_features.py +0 -0
  78. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/check/check_github.py +0 -0
  79. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  80. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/check/check_product.py +0 -0
  81. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  82. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  83. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/check/check_releases.py +0 -0
  84. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/check/check_vendor.py +0 -0
  85. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  86. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  87. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  88. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  89. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/command/__init__.py +0 -0
  90. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/command/command_create.py +0 -0
  91. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/coverage.py +0 -0
  92. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/database/db_console.py +0 -0
  93. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  94. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  95. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  96. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  97. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  98. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  99. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/database/db_status.py +0 -0
  100. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  101. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/doctor.py +0 -0
  102. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/env/env_list.py +0 -0
  103. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/env/env_show.py +0 -0
  104. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/export/__init__.py +0 -0
  105. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  106. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  107. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  108. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  109. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  110. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
  111. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  112. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
  113. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  114. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  115. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  116. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  117. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  118. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  119. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  120. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  121. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  122. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
  123. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  124. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  125. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  126. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  127. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  128. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  129. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  130. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  131. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
  132. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
  133. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  134. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  135. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  136. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  137. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  138. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  139. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  140. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  141. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/lint.py +0 -0
  142. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/locust.py +0 -0
  143. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  144. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  145. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  146. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  147. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/__init__.py +0 -0
  148. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  149. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  150. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  151. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_configure.py +0 -0
  152. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  153. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_create.py +0 -0
  154. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
  155. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  156. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_down.py +0 -0
  157. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  158. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_env.py +0 -0
  159. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_list.py +0 -0
  160. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  161. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  162. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_params.py +0 -0
  163. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_port.py +0 -0
  164. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_release.py +0 -0
  165. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  166. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_run.py +0 -0
  167. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_select.py +0 -0
  168. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  169. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  170. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_test.py +0 -0
  171. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/product/product_up.py +0 -0
  172. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/release/__init__.py +0 -0
  173. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/release/release_resume.py +0 -0
  174. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/selenium.py +0 -0
  175. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  176. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  177. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  178. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  179. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  180. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  181. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  182. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  183. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  184. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  185. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  186. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  187. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  188. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
  189. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  190. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  191. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  192. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/__init__.py +0 -0
  193. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/compose.py +0 -0
  194. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/context.py +0 -0
  195. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/credentials.py +0 -0
  196. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/image_registry.py +0 -0
  197. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/index_refresh.py +0 -0
  198. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/marketplace.py +0 -0
  199. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/marketplace_url.py +0 -0
  200. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/own_commands.py +0 -0
  201. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/pypi_quota.py +0 -0
  202. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/registry.py +0 -0
  203. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/release_dry_run.py +0 -0
  204. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/release_gate.py +0 -0
  205. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/services/spl_store.py +0 -0
  206. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
  207. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
  208. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
  209. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
  210. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
  211. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
  212. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
  213. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
  214. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
  215. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
  216. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
  217. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
  218. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
  219. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
  220. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
  221. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
  222. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
  223. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
  224. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
  225. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
  226. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
  227. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
  228. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
  229. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
  230. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
  231. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_services_service.py.j2 +0 -0
  232. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
  233. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
  234. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
  235. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
  236. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
  237. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
  238. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
  239. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
  240. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
  241. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/licenses/COPYING +0 -0
  242. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
  243. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
  244. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +0 -0
  245. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
  246. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
  247. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
  248. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +0 -0
  249. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -0
  250. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
  251. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
  252. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_400.html +0 -0
  253. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_401.html +0 -0
  254. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_404.html +0 -0
  255. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_500.html +0 -0
  256. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
  257. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
  258. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
  259. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
  260. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
  261. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_app.css +0 -0
  262. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_app.js +0 -0
  263. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +0 -0
  264. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +0 -0
  265. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
  266. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +0 -0
  267. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
  268. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
  269. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
  270. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
  271. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
  272. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
  273. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
  274. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_own.css +0 -0
  275. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
  276. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
  277. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
  278. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/__init__.py +0 -0
  279. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/cache_utils.py +0 -0
  280. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/ci_services.py +0 -0
  281. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/command_loader.py +0 -0
  282. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  283. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/db_client.py +0 -0
  284. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/db_utils.py +0 -0
  285. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  286. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/feature_installer.py +0 -0
  287. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/feature_naming.py +0 -0
  288. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/feature_params.py +0 -0
  289. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/feature_requirements.py +0 -0
  290. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/feature_resolution.py +0 -0
  291. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/feature_utils.py +0 -0
  292. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/git_url.py +0 -0
  293. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/lifecycle.py +0 -0
  294. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/manifest.py +0 -0
  295. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/migration_lock.py +0 -0
  296. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/path_utils.py +0 -0
  297. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/proc.py +0 -0
  298. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/redact.py +0 -0
  299. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/refinement_facts.py +0 -0
  300. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/template_drift.py +0 -0
  301. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/toml_edit.py +0 -0
  302. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
  303. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  304. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  305. {splent_cli-1.44.0 → splent_cli-1.45.0}/src/splent_cli.egg-info/requires.txt +0 -0
  306. {splent_cli-1.44.0 → splent_cli-1.45.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.44.0
3
+ Version: 1.45.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.44.0"
7
+ version = "1.45.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"
@@ -0,0 +1,42 @@
1
+ """The SPLENT command-line interface."""
2
+
3
+ import os as _os
4
+ from types import MappingProxyType as _MappingProxyType
5
+
6
+ # What the operator actually exported, taken before anything reads a .env.
7
+ #
8
+ # This must stay the first thing the package does. cli.py runs load_dotenv(),
9
+ # and so do framework modules it imports (path_utils, configuration), which
10
+ # puts every workspace .env key into os.environ before any command runs. Read
11
+ # afterwards, the shell and the file look identical: env:show could never say
12
+ # "not loaded", env:list never showed "·", and the workspace .env passed for
13
+ # shell values and beat the product's docker/.env. Compare against this
14
+ # instead of os.environ whenever the question is "what did the shell export".
15
+ SHELL_ENV = _MappingProxyType(dict(_os.environ))
16
+
17
+
18
+ def _bytecode_away_from_the_working_copies() -> None:
19
+ """Write root's bytecode outside the workspace.
20
+
21
+ A ``docker exec`` into the CLI's container runs as root unless it says
22
+ ``-u``, and root's __pycache__ directories in the working copies of the
23
+ CLI, the framework and the features were the ones the owner's next
24
+ release could not rewrite. Root's go under /tmp instead; everyone else's
25
+ stay where Python puts them. A PYTHONPYCACHEPREFIX already set wins, and
26
+ so does a workspace root itself owns. What the CLI runs (pytest, the
27
+ release build) inherits it through the environment.
28
+ """
29
+ import sys
30
+
31
+ if sys.pycache_prefix or not hasattr(_os, "geteuid") or _os.geteuid() != 0:
32
+ return
33
+ try:
34
+ owner = _os.stat(_os.environ.get("WORKING_DIR") or "/workspace").st_uid
35
+ except OSError:
36
+ return
37
+ if owner != 0:
38
+ sys.pycache_prefix = "/tmp/splent-root-pycache"
39
+ _os.environ.setdefault("PYTHONPYCACHEPREFIX", sys.pycache_prefix)
40
+
41
+
42
+ _bytecode_away_from_the_working_copies()
@@ -199,6 +199,12 @@ class SPLENTCLI(click.Group):
199
199
  from splent_cli.utils.dynamic_imports import get_app
200
200
  from splent_cli.utils.db_utils import check_db_connection
201
201
 
202
+ # The environment a command that boots the app builds it in when
203
+ # nothing says which (utils.decorators.boots_the_app).
204
+ boot_env = getattr(command, "boot_env", None)
205
+ if boot_env:
206
+ os.environ.setdefault("SPLENT_ENV", boot_env)
207
+
202
208
  # For another product than the selection the CLI's container stays
203
209
  # on the selection's network (compose.attach_self_to_network), so
204
210
  # what reaches this product's services runs where they are.
@@ -6,16 +6,18 @@ import os
6
6
  import sys
7
7
 
8
8
  import click
9
- import importlib.metadata
10
9
 
11
10
  from splent_cli.services import context
12
11
  from splent_cli.services.release_gate import pypi_credentials
13
12
  from splent_cli.utils import forge
13
+ from splent_cli.utils.running_version import installed_version
14
14
 
15
15
 
16
16
  def _pkg_version(name: str) -> str | None:
17
+ # The checkout's version for an editable install, as `splent version`
18
+ # reports it: pip's metadata keeps the version it was installed at.
17
19
  try:
18
- return importlib.metadata.version(name)
20
+ return installed_version(name)
19
21
  except Exception:
20
22
  return None
21
23
 
@@ -263,3 +263,9 @@ def run_wizard():
263
263
  break
264
264
  else:
265
265
  click.secho("Invalid option. Try again.", fg="red")
266
+
267
+
268
+ # The group alone. The loader's fallback registers every command it finds in
269
+ # a module, and found the subcommands too, so `splent forge`, `splent pypi`
270
+ # and `splent github` existed as top-level copies of env:set's.
271
+ cli_command = env_set_group
@@ -3,6 +3,7 @@ import click
3
3
  from splent_cli.services import context, compose
4
4
  from splent_cli.utils.feature_utils import (
5
5
  drop_feature_entries,
6
+ entry_matches_feature,
6
7
  find_feature_entries,
7
8
  hot_reinstall,
8
9
  parse_feature_entry,
@@ -160,10 +161,35 @@ def feature_attach(feature_identifier, version, env_scope):
160
161
 
161
162
  attaching = sorted(declared) != sorted(wanted)
162
163
  if attaching:
163
- drop_feature_entries(data, feature_name, namespace=namespace_fs)
164
+ # A list that declares the feature keeps it where it is, so a new pin
165
+ # does not move the entry away from its neighbours and its comment
166
+ # to the end of the list (uvlhub's, after each release --attach).
167
+ # Only a list it is new to gets it at the end, and the lists it
168
+ # leaves lose it.
169
+ keep_in_place: dict[str, int] = {}
170
+ for key, entry in declared:
171
+ if key in target_keys:
172
+ at = read_feature_list(data, key).index(entry)
173
+ keep_in_place[key] = min(at, keep_in_place.get(key, at))
174
+ drop_feature_entries(
175
+ data,
176
+ feature_name,
177
+ namespace=namespace_fs,
178
+ keys=tuple(key for key, _ in declared if key not in keep_in_place),
179
+ )
164
180
  for key in target_keys:
165
181
  features = read_feature_list(data, key)
166
- features.append(full_name)
182
+ if key in keep_in_place:
183
+ features = [
184
+ entry
185
+ for entry in features
186
+ if not entry_matches_feature(
187
+ entry, feature_name, namespace=namespace_fs
188
+ )
189
+ ]
190
+ features.insert(keep_in_place[key], full_name)
191
+ else:
192
+ features.append(full_name)
167
193
  write_features_to_data(data, features, key=key)
168
194
 
169
195
  # ── PyPI name, when it is not the feature's own ──────────────────
@@ -259,14 +259,22 @@ def _parse_pyproject_entry(entry: str) -> tuple[str, str, str | None]:
259
259
  return ns, name, version or None
260
260
 
261
261
 
262
+ def _product_reader(product_dir: str, pyproject_path: str | None = None):
263
+ """The reader of the product's pyproject.toml, or of PYPROJECT_PATH, a
264
+ candidate pyproject checked in its place (product:validate --pyproject)."""
265
+ from splent_framework.utils.pyproject_reader import PyprojectReader
266
+
267
+ if pyproject_path:
268
+ return PyprojectReader(pyproject_path)
269
+ return PyprojectReader.for_product(product_dir)
270
+
271
+
262
272
  def _resolve_all_product_features(
263
- product_dir: str, workspace: str
273
+ product_dir: str, workspace: str, pyproject_path: str | None = None
264
274
  ) -> list[tuple[str, Path]]:
265
275
  """Read all features from the product and resolve to (label, path)."""
266
- from splent_framework.utils.pyproject_reader import PyprojectReader
267
-
268
276
  try:
269
- features_raw = PyprojectReader.for_product(product_dir).features
277
+ features_raw = _product_reader(product_dir, pyproject_path).features
270
278
  except FileNotFoundError:
271
279
  raise SystemExit("❌ pyproject.toml not found in product.")
272
280
 
@@ -435,7 +443,9 @@ def _contract_env(path: Path) -> str | None:
435
443
  return data.get("tool", {}).get("splent", {}).get("contract", {}).get("env")
436
444
 
437
445
 
438
- def env_scope_findings(workspace: str, product_dir: str) -> list[dict]:
446
+ def env_scope_findings(
447
+ workspace: str, product_dir: str, pyproject_path: str | None = None
448
+ ) -> list[dict]:
439
449
  """Features confined to one environment, in a list that installs them in
440
450
  the other.
441
451
 
@@ -443,9 +453,7 @@ def env_scope_findings(workspace: str, product_dir: str) -> list[dict]:
443
453
  list the production image installed them too, phpMyAdmin with autologin
444
454
  to the database, and nothing said so.
445
455
  """
446
- from splent_framework.utils.pyproject_reader import PyprojectReader
447
-
448
- reader = PyprojectReader.for_product(product_dir)
456
+ reader = _product_reader(product_dir, pyproject_path)
449
457
  lists = {
450
458
  "features": reader.features,
451
459
  "features_dev": reader.features_dev,
@@ -501,7 +509,9 @@ def _joins_the_shared_network(compose_file: Path) -> bool:
501
509
  return "SPLENT_NETWORK" not in str(spec.get("name", ""))
502
510
 
503
511
 
504
- def network_findings(workspace: str, product_dir: str) -> list[dict]:
512
+ def network_findings(
513
+ workspace: str, product_dir: str, pyproject_path: str | None = None
514
+ ) -> list[dict]:
505
515
  """Feature services the product cannot reach.
506
516
 
507
517
  Each product has a network of its own, and a feature's services join it
@@ -509,9 +519,7 @@ def network_findings(workspace: str, product_dir: str) -> list[dict]:
509
519
  instead, so the app could not send mail to Mailhog and phpMyAdmin could
510
520
  not reach the database, and nothing said why.
511
521
  """
512
- from splent_framework.utils.pyproject_reader import PyprojectReader
513
-
514
- reader = PyprojectReader.for_product(product_dir)
522
+ reader = _product_reader(product_dir, pyproject_path)
515
523
  findings = []
516
524
  for entry in reader.features + reader.features_dev + reader.features_prod:
517
525
  ns, name, version = _parse_pyproject_entry(entry)
@@ -537,14 +545,14 @@ def network_findings(workspace: str, product_dir: str) -> list[dict]:
537
545
  return findings
538
546
 
539
547
 
540
- def naming_findings(workspace: str, product_dir: str) -> list[dict]:
548
+ def naming_findings(
549
+ workspace: str, product_dir: str, pyproject_path: str | None = None
550
+ ) -> list[dict]:
541
551
  """Features named after a product (auth_uvlhub). A warning, so the debt
542
552
  shows in every validation until the product drops them."""
543
- from splent_framework.utils.pyproject_reader import PyprojectReader
544
-
545
553
  from splent_cli.utils.feature_naming import product_suffix, product_suffix_message
546
554
 
547
- reader = PyprojectReader.for_product(product_dir)
555
+ reader = _product_reader(product_dir, pyproject_path)
548
556
  findings = []
549
557
  for entry in reader.features + reader.features_dev + reader.features_prod:
550
558
  _, name, _ = _parse_pyproject_entry(entry)
@@ -688,16 +696,14 @@ def version_floor_findings(labeled_paths: list[tuple[str, Path]]) -> list[dict]:
688
696
 
689
697
 
690
698
  def _environment_only_features(
691
- product_dir: str, workspace: str, env: str | None
699
+ product_dir: str, workspace: str, env: str | None, pyproject_path: str | None = None
692
700
  ) -> list[tuple[str, Path]]:
693
701
  """The features ``features_<env>`` adds to the base list, resolved."""
694
- from splent_framework.utils.pyproject_reader import PyprojectReader
695
-
696
702
  env = context.normalize_env(env)
697
703
  if not env:
698
704
  return []
699
705
  try:
700
- reader = PyprojectReader.for_product(product_dir)
706
+ reader = _product_reader(product_dir, pyproject_path)
701
707
  base = set(reader.features)
702
708
  extra = [e for e in reader.features_for_env(env) if e not in base]
703
709
  except (FileNotFoundError, ValueError):
@@ -706,7 +712,10 @@ def _environment_only_features(
706
712
 
707
713
 
708
714
  def run_all_product_check(
709
- workspace: str, product_dir: str, env: str | None = None
715
+ workspace: str,
716
+ product_dir: str,
717
+ env: str | None = None,
718
+ pyproject_path: str | None = None,
710
719
  ) -> list[dict]:
711
720
  """Run the full product compatibility check. Returns findings list.
712
721
 
@@ -714,9 +723,13 @@ def run_all_product_check(
714
723
  floors are checked across the features the product loads in ``env``
715
724
  (SPLENT_ENV when None, ``dev`` when that is unset too), base plus
716
725
  ``features_dev`` or ``features_prod``, the selection phase 1 checks.
726
+ ``pyproject_path`` names a candidate pyproject to read the features
727
+ from instead of the product's.
717
728
  """
718
729
  try:
719
- labeled_paths = _resolve_all_product_features(product_dir, workspace)
730
+ labeled_paths = _resolve_all_product_features(
731
+ product_dir, workspace, pyproject_path
732
+ )
720
733
  except SystemExit:
721
734
  return []
722
735
  if not labeled_paths:
@@ -725,7 +738,8 @@ def run_all_product_check(
725
738
  env = os.getenv("SPLENT_ENV") or "dev"
726
739
  labeled_contracts = [(lbl, _read_contract(path)) for lbl, path in labeled_paths]
727
740
  findings = _analyse_all(labeled_contracts) + version_floor_findings(
728
- labeled_paths + _environment_only_features(product_dir, workspace, env)
741
+ labeled_paths
742
+ + _environment_only_features(product_dir, workspace, env, pyproject_path)
729
743
  )
730
744
  return sorted(findings, key=lambda f: SEVERITY_ORDER[f["severity"]])
731
745
 
@@ -21,6 +21,7 @@ from pathlib import Path
21
21
 
22
22
  from splent_cli.services import context
23
23
  from splent_cli.commands.feature.feature_release import (
24
+ _interactive,
24
25
  infer_contract,
25
26
  merge_preserved,
26
27
  requirement_problems,
@@ -341,8 +342,16 @@ def _print_diff(current: dict, inferred: dict) -> bool:
341
342
  is_flag=True,
342
343
  help="Write the inferred contract to pyproject.toml (default: dry-run only).",
343
344
  )
345
+ @click.option(
346
+ "--inject-config/--no-inject-config",
347
+ "inject_config",
348
+ default=None,
349
+ help="With --write, run feature:inject-config when config.py is missing or "
350
+ "lacks a variable, without asking. Without a terminal and without either "
351
+ "flag it is not run.",
352
+ )
344
353
  @context.requires_product
345
- def feature_contract(feature_ref, write):
354
+ def feature_contract(feature_ref, write, inject_config):
346
355
  """
347
356
  Infer the feature contract from source code at any time during development.
348
357
 
@@ -362,6 +371,12 @@ def feature_contract(feature_ref, write):
362
371
  which a product's config.py may set. The keys of Flask, the framework,
363
372
  the extensions it sets up and other features are left out.
364
373
 
374
+ \b
375
+ When --write finds config.py missing, or missing a variable the feature
376
+ reads, it offers feature:inject-config. --inject-config runs it without
377
+ asking and --no-inject-config never does; without a terminal and
378
+ without either flag it is not run, and the command says how to run it.
379
+
365
380
  \b
366
381
  FEATURE_REF is a short name (notes), a package name
367
382
  (splent_feature_notes), either one under a namespace (splent-io/notes),
@@ -482,7 +497,7 @@ def feature_contract(feature_ref, write):
482
497
  click.secho(" ✅ Contract written to pyproject.toml.", fg="green")
483
498
 
484
499
  # Check if config.py needs updating
485
- _check_config_py(cache_path, ns, name, inferred)
500
+ _check_config_py(cache_path, ns, name, inferred, inject_config)
486
501
 
487
502
  # And whether the settings it reads are declared where a product looks
488
503
  _check_declared_config(cache_path, inferred)
@@ -637,8 +652,29 @@ def _report_params(feature_path, name):
637
652
  )
638
653
 
639
654
 
640
- def _check_config_py(feature_path, ns, name, inferred):
641
- """Warn if config.py is missing or stale relative to the inferred contract."""
655
+ def _wants_inject_config(question: str, name: str, inject: bool | None) -> bool:
656
+ """Whether to run feature:inject-config: the flag when one was given,
657
+ else the answer to QUESTION. Without a terminal nobody can answer, and
658
+ click.confirm aborted the whole command after the contract was written,
659
+ so it is not asked and not run."""
660
+ if inject is not None:
661
+ return inject
662
+ if not _interactive():
663
+ click.secho(
664
+ f" Not asked without a terminal. Run 'splent feature:inject-config "
665
+ f"{name}', or pass --inject-config.",
666
+ dim=True,
667
+ )
668
+ return False
669
+ return click.confirm(question, default=True)
670
+
671
+
672
+ def _check_config_py(feature_path, ns, name, inferred, inject=None):
673
+ """Warn if config.py is missing or stale relative to the inferred contract.
674
+
675
+ ``inject`` is --inject-config (True), --no-inject-config (False) or
676
+ neither (None), see :func:`_wants_inject_config`.
677
+ """
642
678
  from splent_cli.commands.feature.feature_inject_config import (
643
679
  _is_regenerable,
644
680
  read_config_env_vars,
@@ -660,8 +696,8 @@ def _check_config_py(feature_path, ns, name, inferred):
660
696
  f" ⚠ config.py not found — {len(env_vars)} env var(s) detected but not injected into app.config.",
661
697
  fg="yellow",
662
698
  )
663
- if click.confirm(
664
- " Run feature:inject-config to generate it?", default=True
699
+ if _wants_inject_config(
700
+ " Run feature:inject-config to generate it?", name, inject
665
701
  ):
666
702
  from splent_cli.commands.feature.feature_inject_config import (
667
703
  feature_inject_config,
@@ -691,7 +727,9 @@ def _check_config_py(feature_path, ns, name, inferred):
691
727
  dim=True,
692
728
  )
693
729
  return
694
- if click.confirm(" Run feature:inject-config to update it?", default=True):
730
+ if _wants_inject_config(
731
+ " Run feature:inject-config to update it?", name, inject
732
+ ):
695
733
  from splent_cli.commands.feature.feature_inject_config import (
696
734
  feature_inject_config,
697
735
  )
@@ -196,6 +196,35 @@ def _extract_registered_services(init_path: Path) -> list[str]:
196
196
  )
197
197
 
198
198
 
199
+ def _extract_registered_providers(init_path: Path) -> list[str]:
200
+ """Classes handed to another feature's ``register_<kind>_provider(app, X)``.
201
+
202
+ dataset's register_deposition_provider and captcha's
203
+ register_captcha_provider put the class in the service locator for the
204
+ feature that calls them (zenodo, recaptcha), so it is a service that
205
+ feature provides, as one it registers with register_service is. Only a
206
+ class named in the call counts, not one built there.
207
+ """
208
+ if not init_path.exists():
209
+ return []
210
+ try:
211
+ tree = ast.parse(init_path.read_text())
212
+ except (SyntaxError, OSError, UnicodeDecodeError):
213
+ return []
214
+ found: set[str] = set()
215
+ for node in ast.walk(tree):
216
+ if not isinstance(node, ast.Call) or len(node.args) < 2:
217
+ continue
218
+ name = _call_name(node).rsplit(".", 1)[-1]
219
+ provider = node.args[1]
220
+ if _PROVIDER_REGISTRATION.fullmatch(name) and isinstance(provider, ast.Name):
221
+ found.add(provider.id)
222
+ return sorted(found)
223
+
224
+
225
+ _PROVIDER_REGISTRATION = re.compile(r"register_\w+_provider")
226
+
227
+
199
228
  def _extract_templates(src_dir: Path) -> list[str]:
200
229
  templates_dir = src_dir / "templates"
201
230
  if not templates_dir.exists():
@@ -867,13 +896,16 @@ def _scan_dependencies(
867
896
  except SyntaxError:
868
897
  tree = None
869
898
  if tree is not None:
899
+ # An import inside a try that carries on without the module is an
900
+ # optional dependency (_extract_guarded_imports), not a hard one.
901
+ guarded = _guarded_ranges(tree, _IMPORT_TOLERANT_HANDLERS)
870
902
  imported: list[str] = []
871
903
  for node in ast.walk(tree):
872
- if isinstance(node, ast.Import):
873
- imported.extend(alias.name for alias in node.names)
874
- elif isinstance(node, ast.ImportFrom):
875
- imported.append(node.module or "")
876
- imported.extend(alias.name for alias in node.names)
904
+ if not isinstance(node, (ast.Import, ast.ImportFrom)):
905
+ continue
906
+ if _in_ranges(node.lineno, guarded):
907
+ continue
908
+ imported.extend(_imported_names(node))
877
909
  else:
878
910
  # Unparseable file: the line scan, on the code alone.
879
911
  imported = [
@@ -899,6 +931,38 @@ def _scan_dependencies(
899
931
  return sorted(required_features), sorted(env_vars)
900
932
 
901
933
 
934
+ def _imported_names(node) -> list[str]:
935
+ """The dotted names an import statement mentions."""
936
+ if isinstance(node, ast.Import):
937
+ return [alias.name for alias in node.names]
938
+ return [node.module or "", *(alias.name for alias in node.names)]
939
+
940
+
941
+ def _extract_guarded_imports(src_dir: Path, own_feature_name: str) -> set[str]:
942
+ """Other features an import statement names only inside a tolerant try.
943
+
944
+ ``try: from splent_io.splent_feature_x import y`` with a handler that
945
+ catches ``ImportError`` (``ModuleNotFoundError``, ``Exception``, a bare
946
+ ``except``) and carries on is the static spelling of the optional
947
+ import :func:`_extract_dynamic_imports` reads. The same statement
948
+ outside such a try fails without the feature, so it stays a hard
949
+ requirement there (:func:`_scan_dependencies`).
950
+ """
951
+ found: set[str] = set()
952
+ for _, tree in _parsed_sources(src_dir):
953
+ guarded = _guarded_ranges(tree, _IMPORT_TOLERANT_HANDLERS)
954
+ for node in ast.walk(tree):
955
+ if not isinstance(node, (ast.Import, ast.ImportFrom)):
956
+ continue
957
+ if not _in_ranges(node.lineno, guarded):
958
+ continue
959
+ for item in _imported_names(node):
960
+ for short in re.findall(r"splent_feature_(\w+)", item):
961
+ if f"splent_feature_{short}" != own_feature_name:
962
+ found.add(short)
963
+ return found
964
+
965
+
902
966
  def _extract_service_proxies(src_dir: Path) -> tuple[set[str], set[str]]:
903
967
  """Service class names looked up via ``service_proxy("XService")``,
904
968
  split into ``(hard, soft)`` dependencies.
@@ -1224,6 +1288,8 @@ def build_service_map(workspace: str) -> dict[str, str]:
1224
1288
  for init_path in feature_dir.glob(f"src/*/{dir_name}/__init__.py"):
1225
1289
  for svc in _extract_registered_services(init_path):
1226
1290
  _add(svc, short)
1291
+ for svc in _extract_registered_providers(init_path):
1292
+ _add(svc, short)
1227
1293
 
1228
1294
  try:
1229
1295
  data = tomllib.loads(py_path.read_text())
@@ -1420,6 +1486,7 @@ def infer_contract(
1420
1486
  services = sorted(
1421
1487
  set(_extract_services(src_dir / "services.py"))
1422
1488
  | set(_extract_registered_services(src_dir / "__init__.py"))
1489
+ | set(_extract_registered_providers(src_dir / "__init__.py"))
1423
1490
  )
1424
1491
  templates = _extract_templates(src_dir)
1425
1492
  template_hook_slots = _extract_template_hook_slots(src_dir)
@@ -1495,6 +1562,7 @@ def infer_contract(
1495
1562
  slot_map = _default_map(build_slot_map)
1496
1563
  candidates = {service_map.get(svc) for svc in soft_proxies - own_services}
1497
1564
  candidates |= soft_imports
1565
+ candidates |= _extract_guarded_imports(src_dir, feature_name)
1498
1566
  candidates |= {
1499
1567
  signal_map.get(name)
1500
1568
  for name in _extract_calls_named(src_dir, "connect_signal")
@@ -1581,6 +1649,87 @@ def _sections_not_owned_by_the_contract(tail: str) -> str:
1581
1649
  return "\n\n".join(kept)
1582
1650
 
1583
1651
 
1652
+ _KEY_LINE = re.compile(r"^\s*([A-Za-z0-9_-]+|\"[^\"]*\")\s*=(.*)$")
1653
+
1654
+
1655
+ def _inline_comment(rest: str) -> str:
1656
+ """The comment after a value written on one line, ``""`` without one.
1657
+
1658
+ ``rest`` is what follows the ``=``. A ``#`` inside a string is not a
1659
+ comment, so the cut is the first one before which the value parses.
1660
+ """
1661
+ for index, char in enumerate(rest):
1662
+ if char != "#":
1663
+ continue
1664
+ value = rest[:index].strip()
1665
+ if not value:
1666
+ return ""
1667
+ try:
1668
+ tomllib.loads(f"v = {value}")
1669
+ except tomllib.TOMLDecodeError:
1670
+ continue
1671
+ return rest[index:].strip()
1672
+ return ""
1673
+
1674
+
1675
+ def _contract_comments(region: str) -> dict:
1676
+ """The comments a maintainer wrote inside the contract's tables.
1677
+
1678
+ ``{(table, key): (lines above, inline comment)}``, where ``key`` is None
1679
+ for the comments above a subtable's header. The contract is written
1680
+ again from the source, so the comments go with what they are about: the
1681
+ key under or after them in the same table. The banner above
1682
+ ``[tool.splent.contract]`` is the writer's own and is written anyway.
1683
+ """
1684
+ comments: dict = {}
1685
+ table = None
1686
+ above: list[str] = []
1687
+ for line in region.splitlines():
1688
+ stripped = line.strip()
1689
+ if stripped.startswith("["):
1690
+ header = stripped.split("#", 1)[0].strip()
1691
+ if not header.startswith("[tool.splent.contract"):
1692
+ break
1693
+ if above and header != "[tool.splent.contract]":
1694
+ comments[(header, None)] = (above, "")
1695
+ table, above = header, []
1696
+ elif stripped.startswith("#"):
1697
+ above.append(stripped)
1698
+ elif stripped:
1699
+ match = _KEY_LINE.match(line)
1700
+ if match and table is not None:
1701
+ inline = _inline_comment(match.group(2))
1702
+ if above or inline:
1703
+ comments[(table, match.group(1))] = (above, inline)
1704
+ above = []
1705
+ return comments
1706
+
1707
+
1708
+ def _with_comments(block: str, comments: dict) -> str:
1709
+ """BLOCK, the contract as written from the source, with COMMENTS back
1710
+ above and after the keys and subtables they were written for."""
1711
+ if not comments:
1712
+ return block
1713
+ out: list[str] = []
1714
+ table = None
1715
+ for line in block.split("\n"):
1716
+ stripped = line.strip()
1717
+ if stripped.startswith("["):
1718
+ table = stripped
1719
+ above, _ = comments.get((table, None), ([], ""))
1720
+ out.extend(above)
1721
+ out.append(line)
1722
+ continue
1723
+ match = _KEY_LINE.match(line)
1724
+ if match and (table, match.group(1)) in comments:
1725
+ above, inline = comments[(table, match.group(1))]
1726
+ out.extend(above)
1727
+ out.append(f"{line} {inline}" if inline else line)
1728
+ continue
1729
+ out.append(line)
1730
+ return "\n".join(out)
1731
+
1732
+
1584
1733
  def _toml_value(value) -> str:
1585
1734
  """One value as TOML, escaped: lists inline, everything else via tomli_w.
1586
1735
 
@@ -1726,7 +1875,9 @@ def write_contract(pyproject_path: str, contract: dict, feature_name: str) -> No
1726
1875
  # [tool.splent.config] vanished from five features the moment they were
1727
1876
  # released, silently taking every declared default with them.
1728
1877
  refinement_block = ""
1878
+ comments: dict = {}
1729
1879
  if match:
1880
+ comments = _contract_comments(text[match.start() :])
1730
1881
  preserved = _sections_not_owned_by_the_contract(text[match.start() :])
1731
1882
  if preserved:
1732
1883
  refinement_block = "\n" + preserved + "\n"
@@ -1828,7 +1979,7 @@ def write_contract(pyproject_path: str, contract: dict, feature_name: str) -> No
1828
1979
  f"services = {_toml_value(dc.get('depends_on_services', []))}\n"
1829
1980
  )
1830
1981
 
1831
- path.write_text(text + contract_block + refinement_block)
1982
+ path.write_text(text + _with_comments(contract_block, comments) + refinement_block)
1832
1983
 
1833
1984
 
1834
1985
  def _commands_help_block(commands_help: dict) -> str: