splent-cli 1.14.0__tar.gz → 1.15.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 (177) hide show
  1. {splent_cli-1.14.0/src/splent_cli.egg-info → splent_cli-1.15.0}/PKG-INFO +1 -1
  2. {splent_cli-1.14.0 → splent_cli-1.15.0}/pyproject.toml +1 -1
  3. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/cli.py +5 -2
  4. splent_cli-1.15.0/src/splent_cli/commands/spl/spl_publish.py +434 -0
  5. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/uvl/uvl_utils.py +13 -2
  6. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/services/marketplace.py +16 -0
  7. {splent_cli-1.14.0 → splent_cli-1.15.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  8. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli.egg-info/SOURCES.txt +1 -0
  9. {splent_cli-1.14.0 → splent_cli-1.15.0}/LICENSE +0 -0
  10. {splent_cli-1.14.0 → splent_cli-1.15.0}/README.md +0 -0
  11. {splent_cli-1.14.0 → splent_cli-1.15.0}/setup.cfg +0 -0
  12. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/__init__.py +0 -0
  13. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/__main__.py +0 -0
  14. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/__init__.py +0 -0
  15. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  16. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  17. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  18. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  19. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  20. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  21. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  22. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  23. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  24. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/check/__init__.py +0 -0
  25. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  26. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  27. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  28. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/check/check_env.py +0 -0
  29. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/check/check_features.py +0 -0
  30. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/check/check_github.py +0 -0
  31. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  32. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/check/check_product.py +0 -0
  33. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  34. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  35. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  36. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  37. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  38. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  39. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/command/__init__.py +0 -0
  40. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/command/command_create.py +0 -0
  41. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/coverage.py +0 -0
  42. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/database/db_console.py +0 -0
  43. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  44. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  45. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  46. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  47. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  48. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  49. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/database/db_status.py +0 -0
  50. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  51. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/doctor.py +0 -0
  52. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/env/env_list.py +0 -0
  53. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/env/env_set.py +0 -0
  54. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/env/env_show.py +0 -0
  55. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/export/__init__.py +0 -0
  56. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  57. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  58. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
  59. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  60. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  61. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
  62. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  63. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
  64. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
  65. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  66. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
  67. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  68. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  69. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  70. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  71. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  72. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  73. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  74. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  75. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  76. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
  77. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  78. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  79. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  80. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  81. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  82. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  83. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  84. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  85. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
  86. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_release.py +0 -0
  87. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
  88. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  89. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  90. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  91. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  92. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_test.py +0 -0
  93. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  94. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
  95. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  96. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  97. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  98. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/lint.py +0 -0
  99. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/locust.py +0 -0
  100. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  101. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/__init__.py +0 -0
  102. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  103. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_build.py +0 -0
  104. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  105. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  106. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_config.py +0 -0
  107. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_configure.py +0 -0
  108. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_console.py +0 -0
  109. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  110. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_create.py +0 -0
  111. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
  112. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  113. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  114. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_down.py +0 -0
  115. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  116. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_env.py +0 -0
  117. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_list.py +0 -0
  118. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  119. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  120. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_port.py +0 -0
  121. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_release.py +0 -0
  122. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  123. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  124. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  125. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_run.py +0 -0
  126. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_select.py +0 -0
  127. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  128. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  129. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  130. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_test.py +0 -0
  131. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_up.py +0 -0
  132. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/product/product_validate.py +0 -0
  133. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/release/__init__.py +0 -0
  134. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/release/release_core.py +0 -0
  135. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/selenium.py +0 -0
  136. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  137. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  138. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  139. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  140. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  141. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  142. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  143. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  144. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  145. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  146. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  147. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  148. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  149. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/commands/version.py +0 -0
  150. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/services/__init__.py +0 -0
  151. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/services/compose.py +0 -0
  152. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/services/context.py +0 -0
  153. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/services/preflight.py +0 -0
  154. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/services/registry.py +0 -0
  155. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/services/release.py +0 -0
  156. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/__init__.py +0 -0
  157. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/archetype.py +0 -0
  158. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/cache_utils.py +0 -0
  159. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/command_loader.py +0 -0
  160. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  161. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/db_utils.py +0 -0
  162. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/decorators.py +0 -0
  163. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  164. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/feature_installer.py +0 -0
  165. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/feature_utils.py +0 -0
  166. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/git_url.py +0 -0
  167. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/integrity.py +0 -0
  168. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/io_utils.py +0 -0
  169. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/lifecycle.py +0 -0
  170. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/manifest.py +0 -0
  171. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/path_utils.py +0 -0
  172. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/proc.py +0 -0
  173. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli/utils/template_drift.py +0 -0
  174. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  175. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  176. {splent_cli-1.14.0 → splent_cli-1.15.0}/src/splent_cli.egg-info/requires.txt +0 -0
  177. {splent_cli-1.14.0 → splent_cli-1.15.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.14.0
3
+ Version: 1.15.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.14.0"
7
+ version = "1.15.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"
@@ -3,9 +3,7 @@ import os
3
3
  import click
4
4
  from dotenv import load_dotenv
5
5
 
6
- from splent_cli.utils.dynamic_imports import get_app
7
6
  from splent_cli.utils.command_loader import load_commands
8
- from splent_cli.utils.db_utils import check_db_connection
9
7
 
10
8
  load_dotenv()
11
9
 
@@ -40,6 +38,8 @@ class SPLENTCLI(click.Group):
40
38
 
41
39
  self._feature_cmds_cache: dict[str, click.BaseCommand] = {}
42
40
  try:
41
+ from splent_cli.utils.dynamic_imports import get_app
42
+
43
43
  app = get_app()
44
44
  with app.app_context():
45
45
  registry = app.extensions.get("splent_feature_commands", {})
@@ -84,6 +84,9 @@ class SPLENTCLI(click.Group):
84
84
  command = self.get_command(ctx, cmd_name)
85
85
 
86
86
  if command and getattr(command, "requires_app", False):
87
+ from splent_cli.utils.dynamic_imports import get_app
88
+ from splent_cli.utils.db_utils import check_db_connection
89
+
87
90
  app = get_app()
88
91
  if getattr(command, "requires_db", False):
89
92
  if not check_db_connection(app):
@@ -0,0 +1,434 @@
1
+ """
2
+ spl:publish — Publish the local UVL model of an SPL to UVLHub.
3
+
4
+ Closes the UVL → UVLHub → spl:fetch cycle: the .uvl files are not tracked in
5
+ git (UVLHub is the remote source of truth), so after editing a model locally
6
+ this command uploads it, publishes it (new dataset or new version of the
7
+ existing DOI), verifies the published raw file matches the local one
8
+ byte-for-byte, and only then records the new doi/file in metadata.toml.
9
+ """
10
+
11
+ import os
12
+ import re
13
+ import tomllib
14
+
15
+ import click
16
+ import requests
17
+
18
+ from splent_cli.commands.spl.spl_utils import _resolve_spl_metadata
19
+ from splent_cli.commands.uvl.uvl_utils import (
20
+ resolve_uvlhub_raw_url,
21
+ uvlhub_base_url,
22
+ )
23
+ from splent_cli.services import context
24
+ from splent_cli.utils.io_utils import atomic_write, backup_file
25
+
26
+ _TIMEOUT = 60
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # UVLHub API boundary (all network I/O goes through these helpers)
31
+ # ---------------------------------------------------------------------------
32
+
33
+
34
+ def _api_base() -> str:
35
+ """Base URL of the UVLHub instance (UVLHUB_URL env, default uvlhub.io).
36
+
37
+ Shared with resolve_uvlhub_raw_url so upload/publish and verification
38
+ always target the SAME instance.
39
+ """
40
+ return uvlhub_base_url()
41
+
42
+
43
+ def _server_message(response) -> str:
44
+ """Extract a human-readable error message from an API response."""
45
+ try:
46
+ data = response.json()
47
+ except Exception: # noqa: BLE001 - any body that is not JSON
48
+ data = None
49
+ if isinstance(data, dict):
50
+ for key in ("error", "message", "detail"):
51
+ if data.get(key):
52
+ return str(data[key])
53
+ text = (getattr(response, "text", "") or "").strip()
54
+ return text[:300] if text else f"HTTP {response.status_code}"
55
+
56
+
57
+ def _check_response(response, action: str) -> dict:
58
+ """Fail with the server's message on non-2xx; return the JSON body."""
59
+ if response.status_code not in (200, 201):
60
+ raise click.ClickException(
61
+ f"UVLHub rejected the {action} ({response.status_code}): "
62
+ f"{_server_message(response)}"
63
+ )
64
+ try:
65
+ data = response.json()
66
+ except Exception: # noqa: BLE001
67
+ raise click.ClickException(
68
+ f"UVLHub returned an invalid response for the {action} (not JSON)"
69
+ )
70
+ if not isinstance(data, dict):
71
+ raise click.ClickException(
72
+ f"UVLHub returned an unexpected response for the {action}"
73
+ )
74
+ return data
75
+
76
+
77
+ def _api_upload(base: str, api_key: str, uvl_path: str, title: str, description: str):
78
+ """POST /api/v1/datasets/upload — create a draft dataset with the UVL."""
79
+ try:
80
+ with open(uvl_path, "rb") as fh:
81
+ r = requests.post(
82
+ f"{base}/api/v1/datasets/upload",
83
+ headers={"X-API-Key": api_key},
84
+ data={"title": title, "description": description},
85
+ files={"uvl_file": (os.path.basename(uvl_path), fh)},
86
+ timeout=_TIMEOUT,
87
+ )
88
+ except requests.RequestException as e:
89
+ raise click.ClickException(f"Could not reach UVLHub at {base}: {e}")
90
+ return _check_response(r, "upload")
91
+
92
+
93
+ def _api_publish(base: str, api_key: str, dataset_id) -> dict:
94
+ """POST /api/v1/datasets/<id>/publish — publish the draft (mints a DOI)."""
95
+ try:
96
+ r = requests.post(
97
+ f"{base}/api/v1/datasets/{dataset_id}/publish",
98
+ headers={"X-API-Key": api_key},
99
+ timeout=_TIMEOUT,
100
+ )
101
+ except requests.RequestException as e:
102
+ raise click.ClickException(f"Could not reach UVLHub at {base}: {e}")
103
+ return _check_response(r, "publish")
104
+
105
+
106
+ def _api_lookup_doi(base: str, api_key: str, doi: str) -> dict:
107
+ """GET /api/v1/datasets/doi/<doi> — resolve a DOI to its dataset."""
108
+ try:
109
+ r = requests.get(
110
+ f"{base}/api/v1/datasets/doi/{doi}",
111
+ headers={"X-API-Key": api_key},
112
+ timeout=_TIMEOUT,
113
+ )
114
+ except requests.RequestException as e:
115
+ raise click.ClickException(f"Could not reach UVLHub at {base}: {e}")
116
+ return _check_response(r, "DOI lookup")
117
+
118
+
119
+ def _api_new_version(base: str, api_key: str, dataset_id, uvl_path: str) -> dict:
120
+ """POST /api/v1/datasets/<id>/new-version — publish a new version."""
121
+ try:
122
+ with open(uvl_path, "rb") as fh:
123
+ r = requests.post(
124
+ f"{base}/api/v1/datasets/{dataset_id}/new-version",
125
+ headers={"X-API-Key": api_key},
126
+ files={"file": (os.path.basename(uvl_path), fh)},
127
+ timeout=_TIMEOUT,
128
+ )
129
+ except requests.RequestException as e:
130
+ raise click.ClickException(f"Could not reach UVLHub at {base}: {e}")
131
+ return _check_response(r, "new-version")
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # Local helpers
136
+ # ---------------------------------------------------------------------------
137
+
138
+
139
+ def _pick_remote_file(files, local_name: str) -> tuple[str, str | None]:
140
+ """Return (name, raw_url) of the REAL remote file from the API's files list.
141
+
142
+ UVLHub may rename the upload (secure_filename / collision suffixes), so
143
+ the name the server reports is the one metadata.toml must point at. The
144
+ raw_url the server reports (built with the host that actually served the
145
+ publish) is the authoritative download location for verification.
146
+ """
147
+ if not isinstance(files, list) or not files:
148
+ raise click.ClickException(
149
+ "UVLHub response did not include the published files"
150
+ )
151
+ entries = [f for f in files if isinstance(f, dict) and f.get("name")]
152
+ if not entries:
153
+ raise click.ClickException(
154
+ "UVLHub response did not include the published file name"
155
+ )
156
+ chosen = next(
157
+ (f for f in entries if f["name"] == local_name),
158
+ next((f for f in entries if f["name"].endswith(".uvl")), entries[0]),
159
+ )
160
+ raw_url = chosen.get("raw_url")
161
+ return chosen["name"], raw_url if isinstance(raw_url, str) else None
162
+
163
+
164
+ def _toml_str(value: str) -> str:
165
+ """Render a string as a quoted TOML value."""
166
+ return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
167
+
168
+
169
+ def _update_metadata_uvl(metadata_path: str, *, doi: str, file: str) -> None:
170
+ """Rewrite doi/file inside [spl.uvl] preserving the rest of metadata.toml.
171
+
172
+ Text-level edit (comments/formatting survive), backed by a .bak copy and
173
+ an atomic write, then re-parsed to confirm the values landed. On any
174
+ failure the original file is restored — metadata.toml is never left
175
+ corrupted or half-written.
176
+ """
177
+ with open(metadata_path, encoding="utf-8") as f:
178
+ lines = f.read().splitlines()
179
+
180
+ section = None
181
+ uvl_header_idx = None
182
+ doi_done = file_done = False
183
+ for i, line in enumerate(lines):
184
+ stripped = line.strip()
185
+ if stripped.startswith("[") and stripped.endswith("]"):
186
+ section = stripped
187
+ if section == "[spl.uvl]":
188
+ uvl_header_idx = i
189
+ continue
190
+ if section != "[spl.uvl]":
191
+ continue
192
+ m = re.match(r"^(\s*)(doi|file)\s*=", line)
193
+ if not m:
194
+ continue
195
+ key = m.group(2)
196
+ value = doi if key == "doi" else file
197
+ lines[i] = f"{m.group(1)}{key} = {_toml_str(value)}"
198
+ if key == "doi":
199
+ doi_done = True
200
+ else:
201
+ file_done = True
202
+
203
+ if uvl_header_idx is None:
204
+ if lines and lines[-1].strip():
205
+ lines.append("")
206
+ lines.append("[spl.uvl]")
207
+ lines.append('mirror = "uvlhub.io"')
208
+ lines.append(f"doi = {_toml_str(doi)}")
209
+ lines.append(f"file = {_toml_str(file)}")
210
+ else:
211
+ missing = []
212
+ if not doi_done:
213
+ missing.append(f"doi = {_toml_str(doi)}")
214
+ if not file_done:
215
+ missing.append(f"file = {_toml_str(file)}")
216
+ for offset, entry in enumerate(missing):
217
+ lines.insert(uvl_header_idx + 1 + offset, entry)
218
+
219
+ bak = backup_file(metadata_path, ".bak")
220
+ try:
221
+ atomic_write(metadata_path, "\n".join(lines) + "\n")
222
+ with open(metadata_path, "rb") as f:
223
+ written = tomllib.load(f)
224
+ uvl_cfg = written.get("spl", {}).get("uvl", {})
225
+ if uvl_cfg.get("doi") != doi or uvl_cfg.get("file") != file:
226
+ raise click.ClickException(
227
+ "metadata.toml did not validate after editing "
228
+ "(doi/file not found on re-read)"
229
+ )
230
+ except BaseException:
231
+ if bak is not None:
232
+ atomic_write(metadata_path, bak.read_text(encoding="utf-8"))
233
+ raise
234
+ finally:
235
+ if bak is not None and bak.exists():
236
+ bak.unlink()
237
+
238
+
239
+ def _verify_published(
240
+ mirror: str,
241
+ doi: str,
242
+ remote_file: str,
243
+ local_bytes: bytes,
244
+ raw_url: str | None = None,
245
+ ):
246
+ """Download the published raw file and compare it byte-for-byte.
247
+
248
+ Prefers the absolute raw_url reported by the server (it carries the host
249
+ that actually served the publish); only falls back to the URL built from
250
+ mirror/doi/file. Follows redirects (the DOI mapping may 302 to the
251
+ versioned dataset). Returns (matches: bool, url: str).
252
+ """
253
+ if raw_url and raw_url.startswith(("http://", "https://")):
254
+ url = raw_url
255
+ else:
256
+ url = resolve_uvlhub_raw_url(mirror, doi, remote_file)
257
+ try:
258
+ r = requests.get(url, timeout=_TIMEOUT, allow_redirects=True)
259
+ except requests.RequestException as e:
260
+ raise click.ClickException(f"Verification download failed: {e}")
261
+ if r.status_code != 200:
262
+ raise click.ClickException(
263
+ f"Verification failed: UVLHub returned {r.status_code} for {url}"
264
+ )
265
+ return r.content == local_bytes, url
266
+
267
+
268
+ # ---------------------------------------------------------------------------
269
+ # Command
270
+ # ---------------------------------------------------------------------------
271
+
272
+
273
+ @click.command(
274
+ "spl:publish",
275
+ short_help="Publish the local UVL model to UVLHub and update metadata.toml",
276
+ )
277
+ @click.argument("spl_name")
278
+ @click.option(
279
+ "--dry-run",
280
+ is_flag=True,
281
+ help="Show what would be done without touching the network or metadata.",
282
+ )
283
+ @context.requires_detached
284
+ def spl_publish(spl_name, dry_run):
285
+ """Publish the local UVL model of the SPL to UVLHub.
286
+
287
+ \b
288
+ - Empty doi in metadata.toml → uploads a NEW dataset and publishes it.
289
+ - Existing doi → publishes a NEW VERSION of that dataset.
290
+
291
+ After publishing, the raw file on UVLHub is downloaded and compared
292
+ byte-for-byte with the local UVL; only if they match are doi/file
293
+ written to splent_catalog/{spl}/metadata.toml.
294
+
295
+ Requires UVLHUB_API_KEY (generate one at {UVLHUB_URL}/developer/api-keys).
296
+ """
297
+ workspace = str(context.workspace())
298
+ base = _api_base()
299
+
300
+ metadata = _resolve_spl_metadata(spl_name)
301
+ spl_cfg = metadata.get("spl", {})
302
+ uvl_cfg = spl_cfg.get("uvl", {})
303
+ mirror = uvl_cfg.get("mirror") or "uvlhub.io"
304
+ doi = (uvl_cfg.get("doi") or "").strip()
305
+
306
+ spl_dir = os.path.join(workspace, "splent_catalog", spl_name)
307
+ metadata_path = os.path.join(spl_dir, "metadata.toml")
308
+ uvl_path = os.path.join(spl_dir, f"{spl_name}.uvl")
309
+
310
+ if mirror != "uvlhub.io":
311
+ click.secho(
312
+ f" ❌ Unsupported mirror '{mirror}' in metadata.toml "
313
+ f"(only 'uvlhub.io' can be published to).",
314
+ fg="red",
315
+ )
316
+ raise SystemExit(1)
317
+
318
+ if not os.path.isfile(uvl_path):
319
+ click.secho(f" ❌ Local UVL not found: {uvl_path}", fg="red")
320
+ click.echo(
321
+ " Nothing to publish. Build the model first "
322
+ "(spl:create / spl:add-feature)"
323
+ )
324
+ click.echo(
325
+ f" or download the current version with: splent spl:fetch {spl_name}"
326
+ )
327
+ raise SystemExit(1)
328
+
329
+ title = spl_cfg.get("name") or spl_name
330
+ description = (spl_cfg.get("description") or "").strip() or (
331
+ f"UVL variability model for the '{spl_name}' SPLENT product line."
332
+ )
333
+
334
+ if dry_run:
335
+ click.echo()
336
+ click.echo(click.style(f" spl:publish {spl_name} (dry run)", bold=True))
337
+ click.echo()
338
+ click.echo(f" Local UVL : {uvl_path}")
339
+ click.echo(f" API base : {base}")
340
+ click.echo()
341
+ if doi:
342
+ click.echo(f" Would publish a NEW VERSION of the existing DOI {doi}:")
343
+ click.echo(" 1. GET /api/v1/datasets/doi/… (resolve dataset id)")
344
+ click.echo(" 2. POST /api/v1/datasets/<id>/new-version (upload UVL)")
345
+ else:
346
+ click.echo(" Would upload a NEW dataset and publish it:")
347
+ click.echo(f" 1. POST /api/v1/datasets/upload (title: {title})")
348
+ click.echo(" 2. POST /api/v1/datasets/<id>/publish (mints the DOI)")
349
+ click.echo(
350
+ " 3. Verify the published raw file matches the local UVL "
351
+ "byte-for-byte"
352
+ )
353
+ click.echo(f" 4. Write doi/file into {metadata_path}")
354
+ if not os.getenv("UVLHUB_API_KEY"):
355
+ click.echo()
356
+ click.secho(
357
+ " ⚠ UVLHUB_API_KEY is not set — a real run would fail.",
358
+ fg="yellow",
359
+ )
360
+ click.echo()
361
+ click.secho(" ✅ Dry run: nothing was changed.", fg="green")
362
+ return
363
+
364
+ api_key = os.getenv("UVLHUB_API_KEY")
365
+ if not api_key:
366
+ click.secho(" ❌ UVLHUB_API_KEY is not set.", fg="red")
367
+ click.echo(f" Generate an API key at {base}/developer/api-keys")
368
+ click.echo(" and export it before publishing:")
369
+ click.echo(" export UVLHUB_API_KEY=<your-key>")
370
+ raise SystemExit(1)
371
+
372
+ with open(uvl_path, "rb") as f:
373
+ local_bytes = f.read()
374
+ local_name = os.path.basename(uvl_path)
375
+
376
+ click.echo()
377
+ click.echo(click.style(f" Publishing {spl_name} to {base}", bold=True))
378
+ click.echo()
379
+
380
+ if doi:
381
+ click.echo(f" Existing DOI {doi} — creating a new version...")
382
+ dataset = _api_lookup_doi(base, api_key, doi)
383
+ dataset_id = dataset.get("dataset_id")
384
+ if not dataset_id:
385
+ raise click.ClickException(
386
+ f"UVLHub DOI lookup for {doi} did not return a dataset_id"
387
+ )
388
+ result = _api_new_version(base, api_key, dataset_id, uvl_path)
389
+ else:
390
+ click.echo(" No DOI in metadata.toml — uploading a new dataset...")
391
+ uploaded = _api_upload(base, api_key, uvl_path, title, description)
392
+ dataset_id = uploaded.get("dataset_id")
393
+ if not dataset_id:
394
+ raise click.ClickException(
395
+ "UVLHub upload did not return a dataset_id"
396
+ )
397
+ click.echo(f" 📤 Draft dataset created (id {dataset_id}) — publishing...")
398
+ result = _api_publish(base, api_key, dataset_id)
399
+
400
+ new_doi = (result.get("doi") or "").strip()
401
+ if not new_doi:
402
+ raise click.ClickException("UVLHub did not return a DOI after publishing")
403
+ remote_file, raw_url = _pick_remote_file(result.get("files"), local_name)
404
+ click.echo(f" 🌐 Published as DOI {new_doi} (file: {remote_file})")
405
+
406
+ click.echo(" Verifying published content against the local UVL...")
407
+ matches, url = _verify_published(
408
+ mirror, new_doi, remote_file, local_bytes, raw_url
409
+ )
410
+ if not matches:
411
+ click.secho(
412
+ " ❌ Verification failed: the published content differs from "
413
+ "the local UVL.",
414
+ fg="red",
415
+ )
416
+ click.echo(f" URL: {url}")
417
+ click.echo(
418
+ " metadata.toml was NOT modified — it still points to the "
419
+ "previous version."
420
+ )
421
+ raise SystemExit(1)
422
+
423
+ _update_metadata_uvl(metadata_path, doi=new_doi, file=remote_file)
424
+
425
+ click.echo()
426
+ click.secho(
427
+ f" ✅ '{spl_name}' published to UVLHub (DOI {new_doi}).", fg="green"
428
+ )
429
+ if doi and new_doi != doi:
430
+ click.echo(f" metadata.toml updated: doi {doi} → {new_doi}")
431
+ click.echo()
432
+
433
+
434
+ cli_command = spl_publish
@@ -13,6 +13,13 @@ import tomllib
13
13
 
14
14
  from splent_cli.utils.feature_utils import read_features_from_data
15
15
 
16
+ DEFAULT_UVLHUB_URL = "https://www.uvlhub.io"
17
+
18
+
19
+ def uvlhub_base_url() -> str:
20
+ """Base URL of the UVLHub instance (UVLHUB_URL env, default uvlhub.io)."""
21
+ return os.getenv("UVLHUB_URL", DEFAULT_UVLHUB_URL).rstrip("/")
22
+
16
23
 
17
24
  def _require_flamapy():
18
25
  try:
@@ -131,12 +138,16 @@ def normalize_feature_name(dep: str) -> str:
131
138
 
132
139
 
133
140
  def resolve_uvlhub_raw_url(mirror: str, doi: str, file: str) -> str:
134
- """Resolve a uvlhub.io DOI + filename to a raw download URL."""
141
+ """Resolve a uvlhub.io DOI + filename to a raw download URL.
142
+
143
+ The base honors UVLHUB_URL so fetch/info/publish all target the same
144
+ instance (custom deployments included), defaulting to production.
145
+ """
135
146
  if mirror != "uvlhub.io":
136
147
  raise click.ClickException(
137
148
  f"Unsupported mirror '{mirror}' (only 'uvlhub.io' implemented)"
138
149
  )
139
- return f"https://www.uvlhub.io/doi/{doi}/files/raw/{file}/"
150
+ return f"{uvlhub_base_url()}/doi/{doi}/files/raw/{file}/"
140
151
 
141
152
 
142
153
  def iter_children(node):
@@ -30,6 +30,8 @@ import tomllib
30
30
  from datetime import datetime, timezone
31
31
  from pathlib import Path
32
32
 
33
+ import click
34
+
33
35
  INDEX_SCHEMA = 1
34
36
  FEATURE_PREFIX = "splent_feature_"
35
37
 
@@ -370,6 +372,20 @@ def build_spls(workspace: str) -> list[dict]:
370
372
  "model": None,
371
373
  }
372
374
  uvl_path = spl_dir / f"{spl_dir.name}.uvl"
375
+ if not uvl_path.is_file():
376
+ # UVL files are not tracked in git — fetch from UVLHub (spl:fetch
377
+ # logic) when the metadata has a DOI. Best-effort: an SPL without
378
+ # a published model is indexed without one, never fails the build
379
+ # — but the failure is warned about, never swallowed silently.
380
+ try:
381
+ from splent_cli.commands.spl.spl_utils import _fetch_uvl
382
+
383
+ _fetch_uvl(spl_dir.name, metadata, str(uvl_path))
384
+ except Exception as e: # noqa: BLE001 - best-effort indexing
385
+ click.secho(
386
+ f" ⚠ could not fetch UVL for {spl_dir.name}: {e}",
387
+ fg="yellow",
388
+ )
373
389
  if uvl_path.is_file():
374
390
  try:
375
391
  entry["model"] = parse_uvl_structure(uvl_path.read_text())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splent_cli
3
- Version: 1.14.0
3
+ Version: 1.15.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
@@ -145,6 +145,7 @@ src/splent_cli/commands/spl/spl_features.py
145
145
  src/splent_cli/commands/spl/spl_fetch.py
146
146
  src/splent_cli/commands/spl/spl_info.py
147
147
  src/splent_cli/commands/spl/spl_list.py
148
+ src/splent_cli/commands/spl/spl_publish.py
148
149
  src/splent_cli/commands/spl/spl_utils.py
149
150
  src/splent_cli/commands/uvl/uvl_utils.py
150
151
  src/splent_cli/services/__init__.py
File without changes
File without changes
File without changes