splent-cli 1.36.0__tar.gz → 1.38.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. {splent_cli-1.36.0/src/splent_cli.egg-info → splent_cli-1.38.0}/PKG-INFO +1 -1
  2. {splent_cli-1.36.0 → splent_cli-1.38.0}/pyproject.toml +1 -1
  3. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_product.py +59 -1
  4. splent_cli-1.38.0/src/splent_cli/commands/check/check_vendor.py +271 -0
  5. splent_cli-1.38.0/src/splent_cli/commands/clear/clear_uploads.py +298 -0
  6. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_reset.py +4 -2
  7. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_upgrade.py +51 -4
  8. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/export/export_puml.py +3 -0
  9. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_clean.py +4 -3
  10. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_contract.py +53 -4
  11. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_create.py +42 -6
  12. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_info.py +1 -0
  13. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_refine.py +1 -1
  14. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_release.py +373 -8
  15. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_test.py +340 -74
  16. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_build.py +7 -4
  17. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_clean.py +4 -3
  18. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_create.py +33 -19
  19. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_deploy.py +97 -16
  20. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_derive.py +7 -0
  21. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_env.py +57 -12
  22. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_sync_template.py +3 -2
  23. splent_cli-1.38.0/src/splent_cli/commands/product/product_test.py +274 -0
  24. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_validate.py +181 -7
  25. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/context.py +25 -11
  26. splent_cli-1.38.0/src/splent_cli/services/neutral_app.py +567 -0
  27. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/release.py +61 -11
  28. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/release_dry_run.py +8 -3
  29. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/web_container.py +9 -6
  30. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +9 -8
  31. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +12 -2
  32. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +26 -3
  33. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +23 -2
  34. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +14 -7
  35. splent_cli-1.38.0/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +38 -0
  36. splent_cli-1.38.0/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +32 -0
  37. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +62 -12
  38. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/ci_services.py +41 -0
  39. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/ci_workflow.py +3 -2
  40. splent_cli-1.38.0/src/splent_cli/utils/menu_keys.py +424 -0
  41. splent_cli-1.38.0/src/splent_cli/utils/migration_lock.py +98 -0
  42. {splent_cli-1.36.0 → splent_cli-1.38.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  43. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli.egg-info/SOURCES.txt +4 -0
  44. splent_cli-1.36.0/src/splent_cli/commands/clear/clear_uploads.py +0 -110
  45. splent_cli-1.36.0/src/splent_cli/commands/product/product_test.py +0 -141
  46. splent_cli-1.36.0/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -23
  47. splent_cli-1.36.0/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -22
  48. {splent_cli-1.36.0 → splent_cli-1.38.0}/COPYING +0 -0
  49. {splent_cli-1.36.0 → splent_cli-1.38.0}/COPYING.LESSER +0 -0
  50. {splent_cli-1.36.0 → splent_cli-1.38.0}/README.md +0 -0
  51. {splent_cli-1.36.0 → splent_cli-1.38.0}/setup.cfg +0 -0
  52. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/__init__.py +0 -0
  53. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/__main__.py +0 -0
  54. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/cli.py +0 -0
  55. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/__init__.py +0 -0
  56. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  57. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  58. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  59. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  60. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  61. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  62. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  63. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  64. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  65. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/__init__.py +0 -0
  66. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  67. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  68. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  69. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_env.py +0 -0
  70. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_features.py +0 -0
  71. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_github.py +0 -0
  72. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  73. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  74. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  75. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/check/check_releases.py +0 -0
  76. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  77. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  78. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  79. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/command/__init__.py +0 -0
  80. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/command/command_create.py +0 -0
  81. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/coverage.py +0 -0
  82. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_console.py +0 -0
  83. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  84. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  85. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  86. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  87. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  88. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/database/db_status.py +0 -0
  89. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/doctor.py +0 -0
  90. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/env/env_list.py +0 -0
  91. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/env/env_set.py +0 -0
  92. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/env/env_show.py +0 -0
  93. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/export/__init__.py +0 -0
  94. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  95. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
  96. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  97. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
  98. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  99. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  100. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
  101. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  102. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  103. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  104. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  105. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  106. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  107. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  108. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  109. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  110. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  111. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  112. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  113. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  114. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  115. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  116. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  117. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  118. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
  119. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  120. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  121. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  122. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  123. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  124. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
  125. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  126. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  127. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  128. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/lint.py +0 -0
  129. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/locust.py +0 -0
  130. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  131. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  132. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  133. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  134. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  135. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/__init__.py +0 -0
  136. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  137. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  138. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_config.py +0 -0
  139. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_configure.py +0 -0
  140. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_console.py +0 -0
  141. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  142. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  143. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_down.py +0 -0
  144. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  145. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_list.py +0 -0
  146. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  147. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  148. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_port.py +0 -0
  149. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_release.py +0 -0
  150. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  151. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  152. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  153. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_run.py +0 -0
  154. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_select.py +0 -0
  155. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  156. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  157. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/product/product_up.py +0 -0
  158. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/release/__init__.py +0 -0
  159. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/release/release_core.py +0 -0
  160. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/release/release_resume.py +0 -0
  161. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/selenium.py +0 -0
  162. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  163. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  164. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  165. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  166. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  167. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  168. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  169. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  170. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  171. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  172. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  173. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  174. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  175. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
  176. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
  177. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  178. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  179. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  180. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/commands/version.py +0 -0
  181. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/__init__.py +0 -0
  182. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/compose.py +0 -0
  183. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/credentials.py +0 -0
  184. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/deploy_bundle.py +0 -0
  185. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/image_registry.py +0 -0
  186. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/index_refresh.py +0 -0
  187. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/marketplace.py +0 -0
  188. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/marketplace_api.py +0 -0
  189. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/marketplace_url.py +0 -0
  190. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/preflight.py +0 -0
  191. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/pypi_quota.py +0 -0
  192. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/registry.py +0 -0
  193. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/release_gate.py +0 -0
  194. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/services/spl_store.py +0 -0
  195. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
  196. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
  197. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
  198. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
  199. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
  200. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
  201. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
  202. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
  203. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
  204. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
  205. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
  206. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
  207. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
  208. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
  209. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
  210. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
  211. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
  212. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
  213. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
  214. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
  215. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
  216. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
  217. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
  218. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
  219. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
  220. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
  221. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
  222. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
  223. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
  224. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
  225. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/licenses/COPYING +0 -0
  226. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
  227. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
  228. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
  229. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
  230. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
  231. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
  232. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
  233. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
  234. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +0 -0
  235. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
  236. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
  237. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_400.html +0 -0
  238. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_401.html +0 -0
  239. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_404.html +0 -0
  240. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_500.html +0 -0
  241. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
  242. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
  243. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
  244. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
  245. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
  246. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_app.css +0 -0
  247. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_app.js +0 -0
  248. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
  249. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +0 -0
  250. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
  251. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
  252. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +0 -0
  253. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
  254. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
  255. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
  256. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
  257. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
  258. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
  259. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
  260. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_own.css +0 -0
  261. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
  262. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
  263. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
  264. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/__init__.py +0 -0
  265. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/archetype.py +0 -0
  266. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/cache_utils.py +0 -0
  267. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/command_loader.py +0 -0
  268. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  269. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/db_client.py +0 -0
  270. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/db_utils.py +0 -0
  271. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/decorators.py +0 -0
  272. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  273. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/feature_installer.py +0 -0
  274. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/feature_naming.py +0 -0
  275. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/feature_requirements.py +0 -0
  276. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/feature_resolution.py +0 -0
  277. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/feature_utils.py +0 -0
  278. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/forge.py +0 -0
  279. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/git_url.py +0 -0
  280. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/integrity.py +0 -0
  281. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/io_utils.py +0 -0
  282. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/lifecycle.py +0 -0
  283. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/manifest.py +0 -0
  284. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/path_utils.py +0 -0
  285. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/proc.py +0 -0
  286. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/redact.py +0 -0
  287. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/refinement_facts.py +0 -0
  288. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/running_version.py +0 -0
  289. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/template_drift.py +0 -0
  290. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/toml_edit.py +0 -0
  291. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
  292. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  293. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  294. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli.egg-info/requires.txt +0 -0
  295. {splent_cli-1.36.0 → splent_cli-1.38.0}/src/splent_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splent_cli
3
- Version: 1.36.0
3
+ Version: 1.38.0
4
4
  Summary: SPLENT-CLI is a CLI to be able to work on your development more easily.
5
5
  Author-email: DiversoLab <diversolab@us.es>
6
6
  License-Expression: LGPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.36.0"
7
+ version = "1.38.0"
8
8
  description = "SPLENT-CLI is a CLI to be able to work on your development more easily."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -2,7 +2,8 @@
2
2
  check:product — Product-level health checks.
3
3
 
4
4
  Validates that the active product is correctly wired:
5
- 1. Required env vars from feature contracts are set
5
+ 1. Required env vars from feature contracts are set, and the app.config
6
+ keys the contracts read (requires.config_keys) are listed, set or not
6
7
  2. Feature symlinks resolve to real directories
7
8
  3. Config overwrites are flagged
8
9
  4. Features that loaded but registered no blueprints are warned
@@ -176,6 +177,62 @@ def _check_env_vars(workspace, product_path, features, counters):
176
177
  counters["ok"] += 1
177
178
 
178
179
 
180
+ def _check_config_keys(workspace, product_path, features):
181
+ """List the app.config keys the features read and no feature sets.
182
+
183
+ ``requires.config_keys`` of each contract. They are not environment
184
+ variables: a feature reads most of them with a default, and a list or a
185
+ table (theme's SITE_NAV) can only come from the product's config.py. So
186
+ a key the product leaves alone is shown, never counted as a failure,
187
+ which is what made check:product fail for every product once the
188
+ contracts of auth and theme listed them among the env_vars.
189
+ """
190
+ var_features: dict[str, list[str]] = {}
191
+ for entry in features:
192
+ ns_safe, name, version = parse_feature_entry(entry)
193
+ data = _resolve_feature_pyproject(
194
+ workspace, product_path, ns_safe, name, version, None
195
+ )
196
+ keys = (
197
+ (data or {})
198
+ .get("tool", {})
199
+ .get("splent", {})
200
+ .get("contract", {})
201
+ .get("requires", {})
202
+ .get("config_keys", [])
203
+ )
204
+ for key in keys if isinstance(keys, list) else []:
205
+ var_features.setdefault(str(key), []).append(_short(name))
206
+ if not var_features:
207
+ return
208
+
209
+ click.echo()
210
+ click.secho(" Config Keys", bold=True, fg="cyan")
211
+ click.echo()
212
+
213
+ config = None
214
+ try:
215
+ from splent_cli.utils.dynamic_imports import get_app
216
+
217
+ config = get_app().config
218
+ except Exception:
219
+ pass
220
+
221
+ col_var = 30
222
+ col_feat = 25
223
+ click.echo(f" {'Key':<{col_var}} {'Feature(s)':<{col_feat}} Status")
224
+ click.echo(f" {'-' * col_var} {'-' * col_feat} {'-' * 24}")
225
+ for key in sorted(var_features):
226
+ feats = ", ".join(sorted(var_features[key]))
227
+ if config is None:
228
+ status = click.style("? app not built", fg="bright_black")
229
+ elif key in config:
230
+ status = click.style("✔ set", fg="green")
231
+ else:
232
+ status = click.style("· the feature's default", fg="bright_black")
233
+ click.echo(f" {key:<{col_var}} {feats:<{col_feat}} {status}")
234
+
235
+
179
236
  # ── Symlinks ─────────────────────────────────────────────────────────────────
180
237
 
181
238
 
@@ -434,6 +491,7 @@ def check_product():
434
491
  check_and_refresh_contracts(workspace, features, interactive=False)
435
492
 
436
493
  _check_env_vars(workspace, product_path, features, counters)
494
+ _check_config_keys(workspace, product_path, features)
437
495
  _check_symlinks(product_path, features, counters)
438
496
  _check_config_overwrites(counters)
439
497
  _check_blueprints(counters)
@@ -0,0 +1,271 @@
1
+ """
2
+ check:vendor — Find Keenthemes (Metronic) code in a product and its features.
3
+
4
+ Metronic is Keenthemes' commercial theme, and its licence does not allow
5
+ redistributing it, so neither a product of the line nor a feature any
6
+ product can pin may carry it. A feature written against it also breaks as
7
+ soon as the product that ships its files drops them: its icons go blank and
8
+ its scripts call globals nobody defines.
9
+
10
+ It is found by content, not by where it sits: the globals its JavaScript
11
+ defines, its icon classes, its data attributes and its name, in the
12
+ product's own tree and in the src/ of every feature the product declares,
13
+ at the copy the product runs. Only Duotune's icons, SVG with no mark of
14
+ their own, are found by the name in their path. tests/ is left out
15
+ everywhere, since a test may name the patterns it guards against.
16
+ """
17
+
18
+ import os
19
+ import re
20
+
21
+ import click
22
+
23
+ from splent_cli.services import context
24
+
25
+ # (name, regular expression). The globals and classes are matched as the
26
+ # start of a word, the name in any case.
27
+ PATTERNS = (
28
+ ("KTUtil", r"\bKTUtil"),
29
+ ("KTComponents", r"\bKTComponents"),
30
+ ("KTApp", r"\bKTApp"),
31
+ ("KTMenu", r"\bKTMenu"),
32
+ ("KTStepper", r"\bKTStepper"),
33
+ ("keenthemes", r"(?i:keenthemes)"),
34
+ ("Metronic", r"(?i:\bmetronic\b)"),
35
+ ("ki-duotone", r"\bki-duotone\b"),
36
+ ("ki-outline", r"\bki-outline\b"),
37
+ ("ki-solid", r"\bki-solid\b"),
38
+ ("data-kt-", r"\bdata-kt-"),
39
+ )
40
+
41
+ _PATTERN = re.compile(
42
+ "|".join(f"(?P<p{i}>{regex})" for i, (_, regex) in enumerate(PATTERNS))
43
+ )
44
+
45
+ # Keenthemes' names in a file or folder name, for Duotune, its icon set.
46
+ _PATH_PATTERN = re.compile(r"(?i)duotune|keenicons|keenthemes|metronic")
47
+
48
+ # Never searched: tests, installed packages, caches, and hidden folders
49
+ # (.git, virtual environments).
50
+ SKIPPED_DIRS = {"tests", "node_modules", "__pycache__"}
51
+
52
+ # Also left out of the product's tree: what the app writes while it runs.
53
+ # Its features/ folder, the links product:resolve makes, is skipped at the
54
+ # top, and each feature is searched on its own.
55
+ SKIPPED_PRODUCT_DIRS = SKIPPED_DIRS | {"instance", "uploads"}
56
+
57
+ # Read as bytes first: a file with a NUL byte is binary and has no text to
58
+ # search. These are not even opened.
59
+ BINARY_SUFFIXES = (
60
+ ".png",
61
+ ".jpg",
62
+ ".jpeg",
63
+ ".gif",
64
+ ".webp",
65
+ ".ico",
66
+ ".bmp",
67
+ ".woff",
68
+ ".woff2",
69
+ ".ttf",
70
+ ".otf",
71
+ ".eot",
72
+ ".pdf",
73
+ ".zip",
74
+ ".gz",
75
+ ".tgz",
76
+ ".mp4",
77
+ ".webm",
78
+ ".mp3",
79
+ ".pyc",
80
+ )
81
+
82
+ # Files listed per place before the rest is counted, unless --all.
83
+ SHOWN_PER_PLACE = 10
84
+
85
+
86
+ def scan_file(path: str) -> dict | None:
87
+ """{"line": first line, "patterns": [...], "count": matches} for a file
88
+ with Keenthemes code, or None."""
89
+ try:
90
+ with open(path, "rb") as f:
91
+ raw = f.read()
92
+ except OSError:
93
+ return None
94
+ if b"\0" in raw[:8192]:
95
+ return None
96
+ first = None
97
+ names: set[str] = set()
98
+ count = 0
99
+ for number, line in enumerate(raw.decode("utf-8", "replace").splitlines(), 1):
100
+ for match in _PATTERN.finditer(line):
101
+ names.add(PATTERNS[int(match.lastgroup[1:])][0])
102
+ count += 1
103
+ if first is None:
104
+ first = number
105
+ if not count:
106
+ return None
107
+ order = [name for name, _ in PATTERNS]
108
+ return {"line": first, "patterns": sorted(names, key=order.index), "count": count}
109
+
110
+
111
+ def scan_tree(root: str, *, skip=SKIPPED_DIRS, skip_at_root=()) -> dict[str, dict]:
112
+ """{path relative to root: hit} for every file of the tree with
113
+ Keenthemes code, or a Keenthemes name in its path under root. A hit is
114
+ scan_file()'s, and a file found by its path alone has no line and a
115
+ count of 0. Symbolic links to folders are not followed."""
116
+ found: dict[str, dict] = {}
117
+ for current, dirs, files in os.walk(root):
118
+ dirs[:] = sorted(
119
+ d
120
+ for d in dirs
121
+ if not d.startswith(".")
122
+ and d not in skip
123
+ and not (current == root and d in skip_at_root)
124
+ )
125
+ for name in sorted(files):
126
+ path = os.path.join(current, name)
127
+ relative = os.path.relpath(path, root)
128
+ named = _PATH_PATTERN.search(relative)
129
+ hit = None
130
+ if not name.lower().endswith(BINARY_SUFFIXES):
131
+ hit = scan_file(path)
132
+ if named:
133
+ hit = hit or {"line": None, "patterns": [], "count": 0}
134
+ hit["patterns"].insert(0, f"{named.group().lower()} in its path")
135
+ if hit:
136
+ found[relative] = hit
137
+ return found
138
+
139
+
140
+ def _feature_places(workspace: str, product_dir: str, data: dict):
141
+ """(label, src folder or None) for every feature the product declares,
142
+ in every environment, at the copy the product runs."""
143
+ from splent_cli.utils.feature_utils import read_all_feature_entries
144
+ from splent_cli.utils.refinement_facts import product_source
145
+
146
+ for entry in read_all_feature_entries(data):
147
+ source = product_source(workspace, product_dir, entry)
148
+ label = f"{source.name}@{source.version}" if source.version else source.name
149
+ src = source.root / "src" if source.root is not None else None
150
+ yield label, (str(src) if src is not None and src.is_dir() else None)
151
+
152
+
153
+ def _report_place(label: str, where: str, found: dict, show_all: bool, tone):
154
+ click.echo(
155
+ click.style(f" {label}", bold=True) + click.style(f" ({where})", dim=True)
156
+ )
157
+ paths = sorted(found)
158
+ shown = paths if show_all else paths[:SHOWN_PER_PLACE]
159
+ for path in shown:
160
+ hit = found[path]
161
+ location = f"{path}:{hit['line']}" if hit["line"] else path
162
+ what = ", ".join(hit["patterns"])
163
+ if hit["count"]:
164
+ what += " (1 match)" if hit["count"] == 1 else f" ({hit['count']} matches)"
165
+ click.secho(f" {tone[0]} {location} {what}", fg=tone[1])
166
+ if len(paths) > len(shown):
167
+ click.secho(
168
+ f" … and {len(paths) - len(shown)} more files (--all lists them)",
169
+ fg=tone[1],
170
+ )
171
+
172
+
173
+ @click.command(
174
+ "check:vendor",
175
+ short_help="Find Keenthemes (Metronic) code in the product and its features.",
176
+ )
177
+ @click.option(
178
+ "--warn",
179
+ is_flag=True,
180
+ help="Report what is found without failing, while a product moves off it.",
181
+ )
182
+ @click.option("--all", "show_all", is_flag=True, help="List every file found.")
183
+ def check_vendor(warn, show_all):
184
+ """
185
+ Search the active product and the features it declares for Keenthemes
186
+ code: Metronic's globals (KTUtil, KTApp, KTMenu, KTStepper,
187
+ KTComponents), its icon classes (ki-duotone, ki-outline, ki-solid), its
188
+ data-kt- attributes and the names Keenthemes and Metronic, and files
189
+ with Duotune or Keenthemes in their path.
190
+
191
+ \b
192
+ It reads the product's tree and the src/ of each feature, at the
193
+ release the product pins (or the working copy of an editable one),
194
+ and leaves out tests/, node_modules/ and hidden folders. A feature with
195
+ no local copy is said and not searched.
196
+
197
+ \b
198
+ Exits 1 when anything is found. --warn reports it and exits 0.
199
+
200
+ \b
201
+ Examples:
202
+ splent check:vendor
203
+ splent check:vendor --warn
204
+ splent check:vendor --all
205
+ """
206
+ from splent_cli.commands.uvl.uvl_utils import load_pyproject
207
+
208
+ workspace = str(context.workspace())
209
+ product = context.require_app()
210
+ product_dir = os.path.join(workspace, product)
211
+ data = load_pyproject(os.path.join(product_dir, "pyproject.toml"))
212
+ tone = ("⚠️ ", "yellow") if warn else ("✗", "red")
213
+
214
+ click.echo()
215
+ click.echo(click.style(f" check:vendor {product}", bold=True))
216
+ click.echo(click.style(f" {'-' * 60}", fg="bright_black"))
217
+
218
+ places: list[tuple[str, int]] = []
219
+ found = scan_tree(
220
+ product_dir, skip=SKIPPED_PRODUCT_DIRS, skip_at_root=("features",)
221
+ )
222
+ if found:
223
+ _report_place(product, "the product's tree", found, show_all, tone)
224
+ places.append((product, len(found)))
225
+
226
+ clean = 0
227
+ unchecked: list[str] = []
228
+ for label, src in _feature_places(workspace, product_dir, data):
229
+ if src is None:
230
+ unchecked.append(label)
231
+ continue
232
+ found = scan_tree(src)
233
+ if found:
234
+ _report_place(label, "src/", found, show_all, tone)
235
+ places.append((label, len(found)))
236
+ else:
237
+ clean += 1
238
+
239
+ click.echo()
240
+ if unchecked:
241
+ click.secho(
242
+ f" ℹ️ Not searched, no local copy: {', '.join(unchecked)}",
243
+ fg="bright_black",
244
+ )
245
+ if not places:
246
+ click.secho(
247
+ f" ✅ No Keenthemes code in {product} or its "
248
+ f"{clean} feature{'s' if clean != 1 else ''}",
249
+ fg="green",
250
+ )
251
+ click.echo()
252
+ return
253
+
254
+ files = sum(n for _, n in places)
255
+ features = [label for label, _ in places if label != product]
256
+ summary = f"{files} file{'s' if files != 1 else ''} with Keenthemes code"
257
+ if features:
258
+ summary += f", in {len(features)} feature{'s' if len(features) != 1 else ''}"
259
+ if places[0][0] == product:
260
+ summary += " and the product"
261
+ click.secho(f" {tone[0]} {summary}", fg=tone[1], bold=True)
262
+ click.echo(
263
+ " Replace it with Bootstrap, feather icons or code of your own. In a "
264
+ "feature, that takes a release the product then pins."
265
+ )
266
+ click.echo()
267
+ if not warn:
268
+ raise SystemExit(1)
269
+
270
+
271
+ cli_command = check_vendor
@@ -0,0 +1,298 @@
1
+ import importlib.util
2
+ import os
3
+ import shutil
4
+
5
+ import click
6
+
7
+ from splent_cli.utils.path_utils import PathUtils
8
+ from splent_cli.services import context
9
+
10
+
11
+ class UploadsDirError(Exception):
12
+ """The uploads directory of the active product cannot be resolved safely."""
13
+
14
+
15
+ # Top-level directories of a system or a container. No product keeps its
16
+ # uploads in one of them, and emptying one would take the machine with it.
17
+ _SYSTEM_DIRS = {
18
+ "/bin",
19
+ "/boot",
20
+ "/dev",
21
+ "/etc",
22
+ "/home",
23
+ "/lib",
24
+ "/lib64",
25
+ "/opt",
26
+ "/proc",
27
+ "/root",
28
+ "/run",
29
+ "/sbin",
30
+ "/srv",
31
+ "/sys",
32
+ "/tmp",
33
+ "/usr",
34
+ "/var",
35
+ }
36
+
37
+ # What a directory of code has at its top and a directory of uploads never
38
+ # has. A PROTECTED_UPLOADS_DIR pointing at a checkout is refused on sight.
39
+ _CODE_MARKERS = ("pyproject.toml", "setup.py", ".git", "__init__.py")
40
+
41
+
42
+ def _instance_path(product: str, product_dir: str) -> str:
43
+ """Flask's instance folder for the product's app.
44
+
45
+ A product running from its source tree, as in development, gets
46
+ ``<product>/src/instance``. One installed outside it, as the production
47
+ image installs it, gets the folder Flask picks for an installed package.
48
+ """
49
+ try:
50
+ spec = importlib.util.find_spec(product)
51
+ except (ImportError, ValueError):
52
+ spec = None
53
+ # A namespace match (the product directory itself on sys.path) has no
54
+ # origin and says nothing about where the app runs from.
55
+ origin = os.path.realpath(spec.origin) if spec and spec.origin else ""
56
+ if origin and not origin.startswith(product_dir + os.sep):
57
+ try:
58
+ from flask.sansio.scaffold import find_package
59
+ except ImportError:
60
+ find_package = None
61
+ if find_package is not None:
62
+ prefix, package_path = find_package(product)
63
+ if prefix is not None:
64
+ return os.path.join(prefix, "var", f"{product}-instance")
65
+ return os.path.join(package_path, "instance")
66
+ return os.path.join(product_dir, "src", "instance")
67
+
68
+
69
+ def _configured_dir(app) -> str:
70
+ """PROTECTED_UPLOADS_DIR as the app reads it: its config, then the
71
+ environment. Every command starts with the product's env files loaded
72
+ (utils/dynamic_imports), so the environment here is the app's."""
73
+ if app is not None:
74
+ configured = app.config.get("PROTECTED_UPLOADS_DIR")
75
+ if configured:
76
+ return str(configured)
77
+ return os.getenv("PROTECTED_UPLOADS_DIR", "").strip()
78
+
79
+
80
+ def _refuse(path: str, real: str, why: str):
81
+ shown = path if path == real else f"{path} (resolves to {real})"
82
+ raise UploadsDirError(f"{shown} {why}. Nothing was deleted.")
83
+
84
+
85
+ def resolve_uploads_dir(app=None) -> str:
86
+ """Return the directory where the product's app keeps protected uploads.
87
+
88
+ That is where the files of the hub live (hubfile and dataset write under
89
+ it, media keeps its restricted files there): PROTECTED_UPLOADS_DIR, which
90
+ the production compose mounts as a named volume, or, when it is empty,
91
+ ``<instance_path>/protected_uploads``, the framework's fallback
92
+ (splent_framework.utils.path_utils.protected_uploads_dir). With ``app``
93
+ the path comes from the app itself; without it, from the environment and
94
+ the product on disk, so the product does not need to be importable.
95
+
96
+ Raises UploadsDirError when no product is selected, the product directory
97
+ does not exist, or the path is one no uploads directory can be: relative,
98
+ the root, a system directory, the home directory, the workspace, the
99
+ product or its package, anything holding one of them, or a directory with
100
+ code at its top.
101
+ """
102
+ product = os.getenv("SPLENT_APP", "")
103
+ if not product or product in (".", "..") or os.sep in product or "/" in product:
104
+ raise UploadsDirError(f"'{product}' is not a valid product name.")
105
+
106
+ product_dir = os.path.realpath(PathUtils.get_app_base_dir())
107
+ if not os.path.isdir(product_dir):
108
+ raise UploadsDirError(f"Product directory not found: {product_dir}")
109
+
110
+ configured = _configured_dir(app)
111
+ if configured:
112
+ uploads_dir = configured
113
+ elif app is not None:
114
+ uploads_dir = os.path.join(app.instance_path, "protected_uploads")
115
+ else:
116
+ uploads_dir = os.path.join(
117
+ _instance_path(product, product_dir), "protected_uploads"
118
+ )
119
+
120
+ if not os.path.isabs(uploads_dir):
121
+ raise UploadsDirError(
122
+ f"PROTECTED_UPLOADS_DIR={uploads_dir} is not an absolute path, so it "
123
+ "depends on where the app was started. Nothing was deleted."
124
+ )
125
+
126
+ real = os.path.realpath(uploads_dir)
127
+ if real == os.sep or real in _SYSTEM_DIRS:
128
+ _refuse(uploads_dir, real, "is a system directory")
129
+ if real == os.path.realpath(os.path.expanduser("~")):
130
+ _refuse(uploads_dir, real, "is the home directory")
131
+
132
+ package_dir = os.path.join(product_dir, "src", product)
133
+ guarded = [product_dir, package_dir]
134
+ workspace = PathUtils.get_working_dir()
135
+ if workspace:
136
+ guarded.append(os.path.realpath(workspace))
137
+ for kept in guarded:
138
+ if real == kept or kept.startswith(real + os.sep):
139
+ _refuse(uploads_dir, real, f"holds {kept}")
140
+ if real.startswith(package_dir + os.sep):
141
+ _refuse(uploads_dir, real, f"is inside the product's package {package_dir}")
142
+ for marker in _CODE_MARKERS:
143
+ if os.path.lexists(os.path.join(real, marker)):
144
+ _refuse(uploads_dir, real, f"has a {marker}, so it holds code, not uploads")
145
+ return real
146
+
147
+
148
+ def clear_product_uploads(app=None) -> list[str]:
149
+ """Delete the contents of the active product's protected uploads.
150
+
151
+ Prints the outcome and returns the errors (empty when everything was
152
+ cleared or there was nothing to clear). It never raises SystemExit, so
153
+ db:reset can call it after dropping the schema and still re-apply the
154
+ migrations. It does not ask: its callers do.
155
+ """
156
+ try:
157
+ uploads_dir = resolve_uploads_dir(app)
158
+ except UploadsDirError as e:
159
+ click.echo(click.style(f"Cannot clear the uploads directory. {e}", fg="red"))
160
+ return [str(e)]
161
+
162
+ if not os.path.isdir(uploads_dir):
163
+ click.echo(
164
+ click.style(
165
+ f"The uploads directory {uploads_dir} does not exist.", fg="yellow"
166
+ )
167
+ )
168
+ return []
169
+
170
+ errors = []
171
+ try:
172
+ entries = sorted(os.listdir(uploads_dir))
173
+ except OSError as e:
174
+ entries = []
175
+ errors.append(f"{uploads_dir}: {e}")
176
+
177
+ for filename in entries:
178
+ file_path = os.path.join(uploads_dir, filename)
179
+ try:
180
+ # A symlink is removed itself, never the directory it points to
181
+ if os.path.islink(file_path) or not os.path.isdir(file_path):
182
+ os.remove(file_path)
183
+ else:
184
+ shutil.rmtree(file_path)
185
+ except OSError as e:
186
+ errors.append(f"{filename}: {e}")
187
+
188
+ if errors:
189
+ click.echo(
190
+ click.style(
191
+ f"Error clearing the uploads directory {uploads_dir}. "
192
+ f"Could not delete {len(errors)} "
193
+ f"{'entry' if len(errors) == 1 else 'entries'}.",
194
+ fg="red",
195
+ )
196
+ )
197
+ for err in errors:
198
+ click.echo(click.style(f" {err}", fg="red"))
199
+ else:
200
+ click.echo(
201
+ click.style(
202
+ f"The contents of the uploads directory {uploads_dir} have been "
203
+ "successfully cleared.",
204
+ fg="green",
205
+ )
206
+ )
207
+ return errors
208
+
209
+
210
+ def _run_where_the_uploads_are(product: str, env: str, yes: bool) -> None:
211
+ """Run this command in the product's web container when it is up there.
212
+
213
+ PROTECTED_UPLOADS_DIR is a path inside that container, and in production
214
+ a named volume only that container mounts: emptying the same path from
215
+ the CLI container would empty a directory the app never reads, and say
216
+ it worked. That container is ``<product>_web`` in development and
217
+ ``<product>_web_deploy`` in production; only the first was looked for,
218
+ so a production deployment was never reached.
219
+
220
+ Returns when this already is that container, or when it is down in
221
+ development, where the CLI container sees the same files. In production
222
+ with it down, nothing here is the app's: it says so and exits.
223
+ """
224
+ from splent_cli.services import web_container
225
+
226
+ if web_container.is_product_container():
227
+ return
228
+ container = web_container.name(product, env)
229
+ if web_container.running(container):
230
+ # No --prod: the container runs as what it is, and the CLI of an
231
+ # older image would not know the flag.
232
+ argv = ["clear:uploads", *(["--yes"] if yes else [])]
233
+ raise SystemExit(web_container.delegate(product, argv, container=container))
234
+
235
+ if env == "prod":
236
+ # product:clean --prod gets here after 'down -v', which deleted the
237
+ # volume with everything in it.
238
+ click.echo(
239
+ click.style(
240
+ f"{container} is not running, and its uploads are in a volume "
241
+ "only it mounts. Nothing was deleted from here.",
242
+ fg="yellow",
243
+ )
244
+ )
245
+ raise SystemExit(0)
246
+ deployed = web_container.name(product, "prod")
247
+ if web_container.running(deployed):
248
+ click.echo(
249
+ click.style(
250
+ f"These are the development uploads. {deployed} is running: "
251
+ "'splent clear:uploads --prod' empties its own.",
252
+ fg="yellow",
253
+ )
254
+ )
255
+
256
+
257
+ @click.command(
258
+ "clear:uploads",
259
+ short_help="Empty the product's protected uploads (PROTECTED_UPLOADS_DIR).",
260
+ )
261
+ @click.option("--dev", "env_dev", is_flag=True, help="Use development environment.")
262
+ @click.option("--prod", "env_prod", is_flag=True, help="Use production environment.")
263
+ @click.option("-y", "--yes", is_flag=True, help="Confirm without prompting.")
264
+ @context.requires_product
265
+ def clear_uploads(env_dev, env_prod, yes):
266
+ """
267
+ Delete every file in the directory where the product's app keeps the
268
+ uploads it serves behind an access check: PROTECTED_UPLOADS_DIR, or the
269
+ app's instance/protected_uploads when it is empty. The directory itself
270
+ stays. Public files under static/ are not touched.
271
+
272
+ When the product's web container is up, the command runs there, where
273
+ the directory is mounted: <product>_web in development, and
274
+ <product>_web_deploy with --prod or SPLENT_ENV=prod. In production
275
+ nothing is deleted while that container is down, since its uploads are
276
+ a volume only it mounts.
277
+ """
278
+ if env_dev and env_prod:
279
+ click.echo(click.style("Cannot specify both --dev and --prod.", fg="red"))
280
+ raise SystemExit(1)
281
+ env = context.resolve_env(env_dev, env_prod)
282
+ _run_where_the_uploads_are(os.getenv("SPLENT_APP", ""), env, yes)
283
+
284
+ try:
285
+ uploads_dir = resolve_uploads_dir()
286
+ except UploadsDirError as e:
287
+ click.echo(click.style(f"Cannot clear the uploads directory. {e}", fg="red"))
288
+ raise SystemExit(1)
289
+
290
+ if not yes:
291
+ click.confirm(
292
+ f"Delete every file in {uploads_dir}? The datasets and documents "
293
+ "the app serves from it cannot be recovered.",
294
+ abort=True,
295
+ )
296
+
297
+ if clear_product_uploads():
298
+ raise SystemExit(1)
@@ -305,7 +305,7 @@ def _reset_one_feature(app, feature_name, yes, product_path, product_name):
305
305
 
306
306
  def _reset_everything(app, yes, product_path, product_name):
307
307
  if not yes and not click.confirm(
308
- "⚠️ WARNING: This will DROP all tables and clear uploads. Are you sure?",
308
+ "⚠️ WARNING: This will DROP all tables and empty the protected uploads. Are you sure?",
309
309
  abort=True,
310
310
  ):
311
311
  return
@@ -367,7 +367,9 @@ def _reset_everything(app, yes, product_path, product_name):
367
367
  raise SystemExit(1)
368
368
 
369
369
  # The schema is already gone: a failure here must not stop the migrations.
370
- if clear_product_uploads():
370
+ # The app says where its protected uploads are, the rows of which were
371
+ # just dropped.
372
+ if clear_product_uploads(app):
371
373
  click.echo(
372
374
  click.style(
373
375
  "⚠️ Uploads were not cleared. The reset continues.", fg="yellow"