splent-cli 1.9.2__tar.gz → 1.11.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 (173) hide show
  1. {splent_cli-1.9.2/src/splent_cli.egg-info → splent_cli-1.11.0}/PKG-INFO +2 -1
  2. {splent_cli-1.9.2 → splent_cli-1.11.0}/pyproject.toml +7 -3
  3. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/check/check_features.py +9 -2
  4. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/check/check_infra.py +51 -49
  5. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/coverage.py +7 -4
  6. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/database/db_console.py +28 -13
  7. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/database/db_dump.py +25 -10
  8. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/database/db_migrate.py +25 -7
  9. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/database/db_reset.py +27 -4
  10. splent_cli-1.11.0/src/splent_cli/commands/database/db_restore.py +117 -0
  11. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/database/db_rollback.py +44 -3
  12. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/database/db_seed.py +16 -16
  13. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/database/db_upgrade.py +8 -0
  14. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/doctor.py +49 -0
  15. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_add.py +4 -7
  16. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_attach.py +4 -9
  17. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_clean.py +13 -1
  18. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_clone.py +52 -53
  19. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_compat.py +2 -3
  20. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_compile.py +24 -9
  21. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_contract.py +3 -4
  22. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_create.py +6 -6
  23. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_detach.py +31 -10
  24. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_discard.py +13 -3
  25. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_env.py +6 -2
  26. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_fork.py +41 -6
  27. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_git.py +2 -2
  28. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_hook_add.py +17 -32
  29. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_install.py +78 -49
  30. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_order.py +2 -3
  31. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_outdated.py +9 -11
  32. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_pin.py +3 -5
  33. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_pip_install.py +7 -7
  34. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_pull.py +6 -2
  35. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_refine.py +91 -13
  36. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_release.py +44 -5
  37. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_remove.py +3 -5
  38. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_rename.py +81 -37
  39. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_search.py +19 -1
  40. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_status.py +32 -4
  41. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_test.py +33 -1
  42. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_translate.py +32 -12
  43. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_unlock.py +82 -18
  44. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_upgrade.py +43 -15
  45. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_versions.py +19 -8
  46. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_xray.py +4 -7
  47. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/lint.py +18 -6
  48. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/locust.py +85 -14
  49. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_auto_require.py +24 -90
  50. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_build.py +15 -11
  51. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_clean.py +20 -9
  52. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_config.py +4 -1
  53. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_configure.py +41 -38
  54. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_console.py +4 -1
  55. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_create.py +33 -27
  56. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_deploy.py +13 -2
  57. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_derive.py +3 -3
  58. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_deselect.py +2 -2
  59. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_env.py +56 -16
  60. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_list.py +6 -1
  61. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_release.py +56 -36
  62. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_resolve.py +66 -3
  63. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_restart.py +55 -36
  64. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_routes.py +4 -1
  65. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_run.py +26 -8
  66. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_select.py +9 -2
  67. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_signals.py +4 -1
  68. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_test.py +4 -3
  69. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_up.py +57 -35
  70. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/release/release_core.py +26 -4
  71. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/selenium.py +16 -12
  72. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/spl/spl_add_constraints.py +36 -3
  73. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/spl/spl_add_feature.py +49 -2
  74. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/uvl/uvl_utils.py +7 -1
  75. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/services/preflight.py +43 -27
  76. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/services/release.py +155 -17
  77. splent_cli-1.11.0/src/splent_cli/utils/cache_utils.py +115 -0
  78. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/utils/command_loader.py +6 -3
  79. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/utils/contract_freshness.py +10 -2
  80. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/utils/dynamic_imports.py +24 -12
  81. splent_cli-1.11.0/src/splent_cli/utils/feature_installer.py +86 -0
  82. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/utils/feature_utils.py +66 -9
  83. splent_cli-1.11.0/src/splent_cli/utils/git_url.py +221 -0
  84. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/utils/integrity.py +26 -12
  85. splent_cli-1.11.0/src/splent_cli/utils/io_utils.py +89 -0
  86. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/utils/manifest.py +12 -2
  87. splent_cli-1.11.0/src/splent_cli/utils/proc.py +109 -0
  88. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/utils/template_drift.py +12 -5
  89. {splent_cli-1.9.2 → splent_cli-1.11.0/src/splent_cli.egg-info}/PKG-INFO +2 -1
  90. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli.egg-info/SOURCES.txt +2 -0
  91. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli.egg-info/requires.txt +1 -0
  92. splent_cli-1.9.2/src/splent_cli/commands/database/db_restore.py +0 -77
  93. splent_cli-1.9.2/src/splent_cli/utils/cache_utils.py +0 -54
  94. splent_cli-1.9.2/src/splent_cli/utils/feature_installer.py +0 -54
  95. splent_cli-1.9.2/src/splent_cli/utils/git_url.py +0 -67
  96. {splent_cli-1.9.2 → splent_cli-1.11.0}/LICENSE +0 -0
  97. {splent_cli-1.9.2 → splent_cli-1.11.0}/README.md +0 -0
  98. {splent_cli-1.9.2 → splent_cli-1.11.0}/setup.cfg +0 -0
  99. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/__init__.py +0 -0
  100. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/__main__.py +0 -0
  101. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/cli.py +0 -0
  102. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/__init__.py +0 -0
  103. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  104. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  105. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  106. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  107. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  108. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  109. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  110. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  111. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  112. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/check/__init__.py +0 -0
  113. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  114. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  115. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/check/check_env.py +0 -0
  116. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/check/check_github.py +0 -0
  117. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/check/check_product.py +0 -0
  118. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  119. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  120. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  121. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  122. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  123. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  124. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/command/__init__.py +0 -0
  125. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/command/command_create.py +0 -0
  126. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/database/db_status.py +0 -0
  127. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/env/env_list.py +0 -0
  128. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/env/env_set.py +0 -0
  129. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/env/env_show.py +0 -0
  130. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/export/__init__.py +0 -0
  131. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  132. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  133. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  134. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  135. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  136. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  137. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  138. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  139. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  140. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/__init__.py +0 -0
  141. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  142. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  143. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_down.py +0 -0
  144. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  145. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  146. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  147. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_port.py +0 -0
  148. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  149. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  150. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/product/product_validate.py +0 -0
  151. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/release/__init__.py +0 -0
  152. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  153. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  154. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  155. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  156. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  157. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  158. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  159. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  160. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  161. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  162. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/commands/version.py +0 -0
  163. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/services/__init__.py +0 -0
  164. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/services/compose.py +0 -0
  165. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/services/context.py +0 -0
  166. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/utils/__init__.py +0 -0
  167. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/utils/db_utils.py +0 -0
  168. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/utils/decorators.py +0 -0
  169. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/utils/lifecycle.py +0 -0
  170. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli/utils/path_utils.py +0 -0
  171. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  172. {splent_cli-1.9.2 → splent_cli-1.11.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  173. {splent_cli-1.9.2 → splent_cli-1.11.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.9.2
3
+ Version: 1.11.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
  Project-URL: Homepage, https://github.com/diverso-lab/splent_cli
@@ -11,6 +11,7 @@ License-File: LICENSE
11
11
  Requires-Dist: click==8.1.8
12
12
  Requires-Dist: packaging>=24.0
13
13
  Requires-Dist: requests>=2.31.0
14
+ Requires-Dist: tomli_w>=1.0.0
14
15
  Provides-Extra: dev
15
16
  Requires-Dist: setuptools==80.3.1; extra == "dev"
16
17
  Requires-Dist: pytest==8.3.4; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.9.2"
7
+ version = "1.11.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"
@@ -15,6 +15,7 @@ dependencies = [
15
15
  "click==8.1.8",
16
16
  "packaging>=24.0",
17
17
  "requests>=2.31.0",
18
+ "tomli_w>=1.0.0",
18
19
  ]
19
20
 
20
21
  [project.optional-dependencies]
@@ -62,8 +63,11 @@ where = ["src"]
62
63
  testpaths = ["tests"]
63
64
  pythonpath = ["src"]
64
65
 
65
- [tool.black]
66
- line-length = 79
66
+ # Canonical formatter/linter is Ruff (`splent lint` runs `ruff check` +
67
+ # `ruff format`). Ruff's default line length (88) is the project standard;
68
+ # set explicitly here so it is unambiguous.
69
+ [tool.ruff]
70
+ line-length = 88
67
71
 
68
72
  [project.urls]
69
73
  Homepage = "https://github.com/diverso-lab/splent_cli"
@@ -54,8 +54,15 @@ def check_features():
54
54
  _fail("pyproject.toml not found")
55
55
  raise SystemExit(1)
56
56
 
57
- with open(pyproject_path, "rb") as f:
58
- data = tomllib.load(f)
57
+ try:
58
+ with open(pyproject_path, "rb") as f:
59
+ data = tomllib.load(f)
60
+ except tomllib.TOMLDecodeError as e:
61
+ _fail(f"pyproject.toml is not valid TOML: {e}")
62
+ raise SystemExit(1)
63
+ except OSError as e:
64
+ _fail(f"Could not read pyproject.toml: {e}")
65
+ raise SystemExit(1)
59
66
 
60
67
  env = os.getenv("SPLENT_ENV")
61
68
  features = read_features_from_data(data, env)
@@ -7,18 +7,43 @@ import subprocess
7
7
  import json
8
8
 
9
9
  import click
10
- import tomllib
11
10
 
12
11
  from splent_cli.services import context, compose
13
12
  from splent_cli.utils.feature_utils import read_features_from_data
13
+ from splent_cli.utils.io_utils import load_toml
14
+
15
+
16
+ class _Result:
17
+ """Minimal stand-in for CompletedProcess used by the guarded ``_run``."""
18
+
19
+ __slots__ = ("returncode", "stdout", "stderr")
20
+
21
+ def __init__(self, returncode: int, stdout: str = "", stderr: str = ""):
22
+ self.returncode = returncode
23
+ self.stdout = stdout
24
+ self.stderr = stderr
25
+
26
+
27
+ def _run(cmd: list) -> _Result:
28
+ """Run a docker command, never crashing the diagnostic.
29
+
30
+ Missing tool (FileNotFoundError) and timeouts are mapped to a non-zero
31
+ returncode so callers treat them as a normal FAIL/skip, exactly like
32
+ check_docker._run.
33
+ """
34
+ try:
35
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
36
+ return _Result(r.returncode, r.stdout, r.stderr)
37
+ except FileNotFoundError:
38
+ return _Result(1, "", "command not found")
39
+ except subprocess.TimeoutExpired:
40
+ return _Result(1, "", "timed out")
14
41
 
15
42
 
16
43
  def _parse_compose_ports(compose_file: str) -> list[tuple[int, str, str]]:
17
44
  """Return [(host_port, service_name, source_label)] from a compose file."""
18
- result = subprocess.run(
19
- ["docker", "compose", "-f", compose_file, "config", "--format", "json"],
20
- capture_output=True,
21
- text=True,
45
+ result = _run(
46
+ ["docker", "compose", "-f", compose_file, "config", "--format", "json"]
22
47
  )
23
48
  if result.returncode != 0:
24
49
  return []
@@ -41,10 +66,8 @@ def _parse_compose_ports(compose_file: str) -> list[tuple[int, str, str]]:
41
66
 
42
67
  def _parse_compose_services(compose_file: str) -> list[tuple[str, str, str]]:
43
68
  """Return [(service_name, container_name_or_None, source_label)]."""
44
- result = subprocess.run(
45
- ["docker", "compose", "-f", compose_file, "config", "--format", "json"],
46
- capture_output=True,
47
- text=True,
69
+ result = _run(
70
+ ["docker", "compose", "-f", compose_file, "config", "--format", "json"]
48
71
  )
49
72
  if result.returncode != 0:
50
73
  return []
@@ -97,8 +120,7 @@ def check_infra():
97
120
  _fail("pyproject.toml not found")
98
121
  raise SystemExit(1)
99
122
 
100
- with open(pyproject_path, "rb") as f:
101
- data = tomllib.load(f)
123
+ data = load_toml(pyproject_path, what="pyproject.toml")
102
124
 
103
125
  env = os.getenv("SPLENT_ENV", "dev")
104
126
  features = read_features_from_data(data, env)
@@ -132,21 +154,19 @@ def check_infra():
132
154
  else:
133
155
  _ok(f"No port conflicts ({len(all_ports)} ports declared)")
134
156
 
135
- # Check against running containers
157
+ # Check against running containers (query docker ps once, not per-port)
136
158
  running_conflicts = []
137
- for port in all_ports:
138
- result = subprocess.run(
139
- ["docker", "ps", "--format", "{{.ID}}\t{{.Names}}\t{{.Ports}}"],
140
- capture_output=True,
141
- text=True,
142
- )
143
- for line in result.stdout.splitlines():
144
- parts = line.split("\t", 2)
145
- if len(parts) < 3:
146
- continue
147
- cid, name, ports_str = parts
148
- if f":{port}->" in ports_str:
149
- running_conflicts.append((port, name))
159
+ ps_result = _run(["docker", "ps", "--format", "{{.ID}}\t{{.Names}}\t{{.Ports}}"])
160
+ if ps_result.returncode == 0:
161
+ ps_lines = ps_result.stdout.splitlines()
162
+ for port in all_ports:
163
+ for line in ps_lines:
164
+ parts = line.split("\t", 2)
165
+ if len(parts) < 3:
166
+ continue
167
+ cid, name, ports_str = parts
168
+ if f":{port}->" in ports_str:
169
+ running_conflicts.append((port, name))
150
170
 
151
171
  if running_conflicts:
152
172
  for port, cname in running_conflicts:
@@ -189,11 +209,7 @@ def check_infra():
189
209
  click.echo(click.style(" Networks", bold=True))
190
210
  required_networks: set[str] = set()
191
211
  for label, cf in compose_files:
192
- result = subprocess.run(
193
- ["docker", "compose", "-f", cf, "config", "--format", "json"],
194
- capture_output=True,
195
- text=True,
196
- )
212
+ result = _run(["docker", "compose", "-f", cf, "config", "--format", "json"])
197
213
  if result.returncode != 0:
198
214
  continue
199
215
  try:
@@ -205,10 +221,8 @@ def check_infra():
205
221
  required_networks.add(net_name)
206
222
 
207
223
  if required_networks:
208
- existing_networks = subprocess.run(
209
- ["docker", "network", "ls", "--format", "{{.Name}}"],
210
- capture_output=True,
211
- text=True,
224
+ existing_networks = _run(
225
+ ["docker", "network", "ls", "--format", "{{.Name}}"]
212
226
  ).stdout.splitlines()
213
227
  for net in sorted(required_networks):
214
228
  if net in existing_networks:
@@ -226,11 +240,7 @@ def check_infra():
226
240
  build_count = 0
227
241
  for label, cf in compose_files:
228
242
  docker_dir = os.path.dirname(cf)
229
- result = subprocess.run(
230
- ["docker", "compose", "-f", cf, "config", "--format", "json"],
231
- capture_output=True,
232
- text=True,
233
- )
243
+ result = _run(["docker", "compose", "-f", cf, "config", "--format", "json"])
234
244
  if result.returncode != 0:
235
245
  continue
236
246
  try:
@@ -268,11 +278,7 @@ def check_infra():
268
278
  services_depended_on: dict[str, str] = {} # depended_svc -> by_svc
269
279
 
270
280
  for label, cf in compose_files:
271
- result = subprocess.run(
272
- ["docker", "compose", "-f", cf, "config", "--format", "json"],
273
- capture_output=True,
274
- text=True,
275
- )
281
+ result = _run(["docker", "compose", "-f", cf, "config", "--format", "json"])
276
282
  if result.returncode != 0:
277
283
  continue
278
284
  try:
@@ -309,11 +315,7 @@ def check_infra():
309
315
  click.echo(click.style(" Volumes", bold=True))
310
316
  all_volumes: dict[str, list[str]] = {} # vol_name -> [labels]
311
317
  for label, cf in compose_files:
312
- result = subprocess.run(
313
- ["docker", "compose", "-f", cf, "config", "--format", "json"],
314
- capture_output=True,
315
- text=True,
316
- )
318
+ result = _run(["docker", "compose", "-f", cf, "config", "--format", "json"])
317
319
  if result.returncode != 0:
318
320
  continue
319
321
  try:
@@ -1,8 +1,8 @@
1
1
  import click
2
- import subprocess
3
2
  import os
4
3
 
5
4
  from splent_cli.utils.path_utils import PathUtils
5
+ from splent_cli.utils.proc import run
6
6
  from splent_cli.services import context
7
7
 
8
8
 
@@ -36,9 +36,12 @@ def coverage(module_name, html):
36
36
  if html:
37
37
  coverage_cmd.extend(["--cov-report", "html"])
38
38
 
39
- try:
40
- subprocess.run(coverage_cmd, check=True)
41
- except subprocess.CalledProcessError:
39
+ result = run(
40
+ coverage_cmd,
41
+ check=False,
42
+ tool_hint="Install it with: pip install pytest pytest-cov",
43
+ )
44
+ if result.returncode != 0:
42
45
  click.echo(
43
46
  click.style("❌ Coverage run failed (tests may be failing).", fg="red")
44
47
  )
@@ -1,9 +1,9 @@
1
1
  import click
2
- import subprocess
3
2
  from dotenv import load_dotenv
4
3
  import os
5
4
 
6
5
  from splent_cli.services import context
6
+ from splent_cli.utils.proc import run, require_tool
7
7
 
8
8
 
9
9
  @click.command(
@@ -38,19 +38,34 @@ def db_console():
38
38
  )
39
39
  raise SystemExit(1)
40
40
 
41
- try:
42
- subprocess.run(
43
- [
44
- "mysql",
45
- f"-h{mariadb_hostname}",
46
- f"-u{mariadb_user}",
47
- f"-p{mariadb_password}",
48
- mariadb_database,
49
- ],
50
- check=True,
41
+ require_tool(
42
+ "mysql",
43
+ "Install the MariaDB/MySQL client (e.g. 'mariadb-client') and ensure it is on your PATH.",
44
+ )
45
+
46
+ result = run(
47
+ [
48
+ "mysql",
49
+ f"-h{mariadb_hostname}",
50
+ f"-u{mariadb_user}",
51
+ f"-p{mariadb_password}",
52
+ mariadb_database,
53
+ ],
54
+ check=False,
55
+ )
56
+ if result.returncode != 0:
57
+ click.secho(
58
+ f"❌ Could not open the MariaDB console (mysql exited {result.returncode}).",
59
+ fg="red",
60
+ )
61
+ click.secho(
62
+ " Is the database container running and reachable?\n"
63
+ f" Tried to connect to host '{mariadb_hostname}' as user "
64
+ f"'{mariadb_user}'.\n"
65
+ " Start it with: splent product:up (or check 'docker ps').",
66
+ fg="yellow",
51
67
  )
52
- except subprocess.CalledProcessError as e:
53
- click.secho(f"❌ Error opening MariaDB console: {e}", fg="red")
68
+ raise SystemExit(1)
54
69
 
55
70
 
56
71
  cli_command = db_console
@@ -1,10 +1,11 @@
1
1
  import click
2
- import subprocess
3
2
  from dotenv import load_dotenv
4
3
  import os
4
+ import tempfile
5
5
  from datetime import datetime
6
6
 
7
7
  from splent_cli.services import context
8
+ from splent_cli.utils.proc import run, require_tool
8
9
 
9
10
 
10
11
  @click.command(
@@ -44,10 +45,23 @@ def db_dump(filename):
44
45
  if not filename.endswith(".sql"):
45
46
  filename += ".sql"
46
47
 
48
+ require_tool(
49
+ "mysqldump",
50
+ "Install the MariaDB/MySQL client tools (provides 'mysqldump').",
51
+ )
52
+
53
+ env = {**os.environ, "MYSQL_PWD": mariadb_password or ""}
54
+
55
+ # Dump to a temp file first and only replace the target on success, so a
56
+ # pre-existing file with the same name is never truncated or deleted if the
57
+ # dump fails midway.
58
+ target_dir = os.path.dirname(os.path.abspath(filename)) or "."
59
+ fd, tmp_path = tempfile.mkstemp(
60
+ prefix=".db_dump_", suffix=".sql.tmp", dir=target_dir
61
+ )
47
62
  try:
48
- env = {**os.environ, "MYSQL_PWD": mariadb_password or ""}
49
- with open(filename, "wb") as out:
50
- subprocess.run(
63
+ with os.fdopen(fd, "wb") as out:
64
+ run(
51
65
  [
52
66
  "mysqldump",
53
67
  f"-h{mariadb_hostname}",
@@ -55,17 +69,18 @@ def db_dump(filename):
55
69
  mariadb_database,
56
70
  ],
57
71
  stdout=out,
58
- check=True,
72
+ text=False,
59
73
  env=env,
74
+ tool_hint="Install the MariaDB/MySQL client tools (provides 'mysqldump').",
60
75
  )
76
+ os.replace(tmp_path, filename)
61
77
  click.echo(
62
78
  click.style(f"Database dump created successfully: {filename}", fg="green")
63
79
  )
64
- except subprocess.CalledProcessError as e:
65
- click.echo(click.style(f"Error creating database dump: {e}", fg="red"))
66
- if os.path.exists(filename):
67
- os.remove(filename)
68
- click.echo(click.style(f"Partial file removed: {filename}", fg="yellow"))
80
+ except BaseException:
81
+ if os.path.exists(tmp_path):
82
+ os.remove(tmp_path)
83
+ raise
69
84
 
70
85
 
71
86
  cli_command = db_dump
@@ -75,9 +75,7 @@ def db_migrate(feature, message, empty):
75
75
  and kept so you can write ``upgrade()``/``downgrade()`` by hand.
76
76
  """
77
77
  if empty and not feature:
78
- click.echo(
79
- click.style("❌ --empty requires a FEATURE argument.", fg="red")
80
- )
78
+ click.echo(click.style("❌ --empty requires a FEATURE argument.", fg="red"))
81
79
  raise SystemExit(1)
82
80
 
83
81
  if feature:
@@ -125,19 +123,39 @@ def db_migrate(feature, message, empty):
125
123
  prev_level = alembic_logger.level
126
124
  alembic_logger.setLevel(logging.WARNING)
127
125
 
126
+ gen_error = None
128
127
  try:
129
128
  if empty:
130
129
  alembic_revision(directory=mdir, message=message or feat)
131
130
  else:
132
131
  alembic_migrate(directory=mdir, message=message or feat)
133
132
  except Exception as e:
134
- if os.getenv("SPLENT_DEBUG"):
135
- click.secho(
136
- f" ⚠️ {feat}: migration generation skipped ({e})", fg="yellow"
137
- )
133
+ gen_error = e
138
134
  finally:
139
135
  alembic_logger.setLevel(prev_level)
140
136
 
137
+ # Always surface generation failures — never report "up to date" when the
138
+ # migration actually failed to generate. Show a one-line error (full
139
+ # detail only under SPLENT_DEBUG).
140
+ if gen_error is not None:
141
+ click.echo(
142
+ click.style(
143
+ f" ❌ {feat}: migration generation failed ({gen_error})",
144
+ fg="red",
145
+ )
146
+ )
147
+ if os.getenv("SPLENT_DEBUG"):
148
+ import traceback
149
+
150
+ click.echo(
151
+ "".join(
152
+ traceback.format_exception(
153
+ type(gen_error), gen_error, gen_error.__traceback__
154
+ )
155
+ )
156
+ )
157
+ continue
158
+
141
159
  after = _count_versions(mdir)
142
160
 
143
161
  # A blank migration requested with --empty is intentional: keep it so the
@@ -41,9 +41,14 @@ def db_reset(yes):
41
41
  return
42
42
 
43
43
  # --- STEP 1: Drop ALL tables (including alembic_* and splent_migrations) ---
44
+ # Run every DROP inside a single transaction (engine.begin) so that a
45
+ # mid-operation connection failure rolls back as a unit rather than
46
+ # leaving a half-dropped schema. Note: MySQL/MariaDB issue an implicit
47
+ # COMMIT on DDL, so a full rollback is not guaranteed there; we therefore
48
+ # also surface explicit partial-state recovery guidance on failure.
44
49
  click.echo(click.style("🗑️ Dropping all tables...", fg="yellow"))
45
50
  try:
46
- with db.engine.connect() as conn:
51
+ with db.engine.begin() as conn:
47
52
  conn.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
48
53
  meta = MetaData()
49
54
  meta.reflect(bind=db.engine)
@@ -51,11 +56,21 @@ def db_reset(yes):
51
56
  conn.execute(text(f"DROP TABLE IF EXISTS `{table.name}`"))
52
57
  click.echo(click.style(f" Dropped {table.name}", fg="bright_black"))
53
58
  conn.execute(text("SET FOREIGN_KEY_CHECKS = 1"))
54
- conn.commit()
55
59
  click.echo(click.style("✅ All tables dropped.", fg="yellow"))
56
60
  except Exception as e:
57
61
  click.echo(click.style(f"❌ Error dropping tables: {e}", fg="red"))
58
- return
62
+ click.echo(
63
+ click.style(
64
+ "⚠️ The database may now be in a PARTIAL state: some tables "
65
+ "may have been dropped while others remain.\n"
66
+ " To recover, re-run this command once the database is "
67
+ "reachable again (it is safe to repeat — drops use "
68
+ "DROP TABLE IF EXISTS), or manually drop the remaining "
69
+ "tables / re-create the schema from migrations.",
70
+ fg="red",
71
+ )
72
+ )
73
+ raise SystemExit(1)
59
74
 
60
75
  # --- STEP 2: Recreate splent_migrations tracking table ---
61
76
  click.echo(click.style("📋 Recreating splent_migrations table...", fg="cyan"))
@@ -74,7 +89,15 @@ def db_reset(yes):
74
89
  )
75
90
  except Exception as e:
76
91
  click.echo(click.style(f"❌ Error creating tracking table: {e}", fg="red"))
77
- return
92
+ click.echo(
93
+ click.style(
94
+ "⚠️ All tables were dropped but the tracking table could not "
95
+ "be recreated. Re-run this command once the database is "
96
+ "reachable to finish the reset.",
97
+ fg="red",
98
+ )
99
+ )
100
+ raise SystemExit(1)
78
101
 
79
102
  # --- STEP 3: Clear uploads ---
80
103
  ctx = click.get_current_context()
@@ -0,0 +1,117 @@
1
+ import os
2
+ import click
3
+ from datetime import datetime
4
+ from dotenv import load_dotenv
5
+
6
+ from splent_cli.services import context
7
+ from splent_cli.utils.proc import run
8
+
9
+
10
+ @click.command(
11
+ "db:restore",
12
+ short_help="Restore a MariaDB database from a SQL dump file.",
13
+ )
14
+ @click.argument("filename")
15
+ @click.option("--yes", is_flag=True, help="Skip confirmation prompt.")
16
+ @context.requires_product
17
+ def db_restore(filename, yes):
18
+ """
19
+ Restore the database from FILENAME (a .sql dump).
20
+
21
+ Reads connection credentials from the workspace .env file
22
+ (MARIADB_HOSTNAME, MARIADB_USER, MARIADB_PASSWORD, MARIADB_DATABASE).
23
+
24
+ \b
25
+ Example:
26
+ splent db:restore dump_20250101_120000.sql
27
+ """
28
+ load_dotenv()
29
+
30
+ if not os.path.exists(filename):
31
+ click.secho(f"❌ File not found: {filename}", fg="red")
32
+ raise SystemExit(1)
33
+
34
+ host = os.getenv("MARIADB_HOSTNAME")
35
+ user = os.getenv("MARIADB_USER")
36
+ password = os.getenv("MARIADB_PASSWORD")
37
+ database = os.getenv("MARIADB_DATABASE")
38
+
39
+ missing = [
40
+ k
41
+ for k, v in {
42
+ "MARIADB_HOSTNAME": host,
43
+ "MARIADB_USER": user,
44
+ "MARIADB_PASSWORD": password,
45
+ "MARIADB_DATABASE": database,
46
+ }.items()
47
+ if not v
48
+ ]
49
+
50
+ if missing:
51
+ click.secho(f"❌ Missing env vars: {', '.join(missing)}", fg="red")
52
+ raise SystemExit(1)
53
+
54
+ if not yes:
55
+ click.secho(
56
+ f"⚠️ This will overwrite the database '{database}' with the contents of '{filename}'.",
57
+ fg="yellow",
58
+ )
59
+ if not click.confirm("Continue?"):
60
+ click.echo("❎ Cancelled.")
61
+ raise SystemExit(0)
62
+
63
+ env = {**os.environ, "MYSQL_PWD": password or ""}
64
+
65
+ # Take an automatic pre-restore backup so a corrupt/partial dump cannot
66
+ # leave the live database half-overwritten with no way back.
67
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
68
+ backup_filename = f"pre_restore_{database}_{timestamp}.sql"
69
+ try:
70
+ with open(backup_filename, "wb") as backup_out:
71
+ run(
72
+ ["mysqldump", f"-h{host}", f"-u{user}", database],
73
+ stdout=backup_out,
74
+ env=env,
75
+ tool_hint=(
76
+ "Install the MariaDB/MySQL client tools (provides 'mysqldump'), "
77
+ "e.g. 'mysql-client' / 'mariadb-client'."
78
+ ),
79
+ )
80
+ except click.ClickException:
81
+ # mysqldump missing/failed (often a connection failure): do not proceed
82
+ # to overwrite the live database without a safety net.
83
+ if os.path.exists(backup_filename):
84
+ os.remove(backup_filename)
85
+ click.secho(
86
+ f"❌ Could not create a pre-restore backup of '{database}' "
87
+ f"(check the database is reachable at '{host}' and credentials are correct).\n"
88
+ "Aborting before overwriting the live database.",
89
+ fg="red",
90
+ )
91
+ raise SystemExit(1)
92
+
93
+ click.secho(f"🛟 Pre-restore backup saved to: {backup_filename}", fg="cyan")
94
+
95
+ try:
96
+ with open(filename, "rb") as sql_file:
97
+ run(
98
+ ["mysql", f"-h{host}", f"-u{user}", database],
99
+ stdin=sql_file,
100
+ env=env,
101
+ tool_hint=(
102
+ "Install the MariaDB/MySQL client tools (provides 'mysql'), "
103
+ "e.g. 'mysql-client' / 'mariadb-client'."
104
+ ),
105
+ )
106
+ click.secho(f"✅ Database restored from: {filename}", fg="green")
107
+ except click.ClickException as e:
108
+ click.secho(f"❌ Error restoring database: {e.format_message()}", fg="red")
109
+ click.secho(
110
+ "The database may be in an inconsistent state. To roll back, restore "
111
+ f"the pre-restore backup:\n splent db:restore {backup_filename} --yes",
112
+ fg="yellow",
113
+ )
114
+ raise SystemExit(1)
115
+
116
+
117
+ cli_command = db_restore