splent-cli 1.29.0__tar.gz → 1.30.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 (201) hide show
  1. {splent_cli-1.29.0/src/splent_cli.egg-info → splent_cli-1.30.0}/PKG-INFO +1 -1
  2. {splent_cli-1.29.0 → splent_cli-1.30.0}/pyproject.toml +1 -1
  3. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_release.py +96 -1
  4. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_release.py +70 -41
  5. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_publish.py +145 -0
  6. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/marketplace.py +69 -1
  7. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/marketplace_api.py +31 -3
  8. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/release.py +54 -4
  9. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/io_utils.py +10 -0
  10. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/toml_edit.py +48 -0
  11. {splent_cli-1.29.0 → splent_cli-1.30.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  12. {splent_cli-1.29.0 → splent_cli-1.30.0}/LICENSE +0 -0
  13. {splent_cli-1.29.0 → splent_cli-1.30.0}/README.md +0 -0
  14. {splent_cli-1.29.0 → splent_cli-1.30.0}/setup.cfg +0 -0
  15. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/__init__.py +0 -0
  16. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/__main__.py +0 -0
  17. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/cli.py +0 -0
  18. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/__init__.py +0 -0
  19. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  20. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  21. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  22. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  23. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  24. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  25. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  26. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  27. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  28. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/__init__.py +0 -0
  29. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  30. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  31. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  32. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_env.py +0 -0
  33. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_features.py +0 -0
  34. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_github.py +0 -0
  35. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  36. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_product.py +0 -0
  37. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  38. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  39. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_releases.py +0 -0
  40. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  41. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  42. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  43. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  44. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/command/__init__.py +0 -0
  45. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/command/command_create.py +0 -0
  46. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/coverage.py +0 -0
  47. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_console.py +0 -0
  48. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  49. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  50. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  51. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  52. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  53. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  54. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_status.py +0 -0
  55. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  56. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/doctor.py +0 -0
  57. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/env/env_list.py +0 -0
  58. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/env/env_set.py +0 -0
  59. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/env/env_show.py +0 -0
  60. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/export/__init__.py +0 -0
  61. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  62. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  63. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
  64. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  65. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  66. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
  67. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  68. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
  69. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
  70. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  71. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
  72. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  73. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  74. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  75. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  76. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  77. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  78. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  79. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  80. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  81. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
  82. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  83. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  84. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  85. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  86. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  87. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  88. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  89. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  90. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
  91. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
  92. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  93. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  94. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  95. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  96. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_test.py +0 -0
  97. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  98. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
  99. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  100. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  101. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  102. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/lint.py +0 -0
  103. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/locust.py +0 -0
  104. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  105. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  106. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  107. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  108. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  109. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/__init__.py +0 -0
  110. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  111. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_build.py +0 -0
  112. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  113. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  114. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_config.py +0 -0
  115. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_configure.py +0 -0
  116. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_console.py +0 -0
  117. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  118. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_create.py +0 -0
  119. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
  120. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  121. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  122. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_down.py +0 -0
  123. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  124. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_env.py +0 -0
  125. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_list.py +0 -0
  126. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  127. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  128. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_port.py +0 -0
  129. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  130. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  131. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  132. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_run.py +0 -0
  133. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_select.py +0 -0
  134. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  135. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  136. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  137. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_test.py +0 -0
  138. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_up.py +0 -0
  139. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_validate.py +0 -0
  140. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/release/__init__.py +0 -0
  141. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/release/release_core.py +0 -0
  142. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/release/release_resume.py +0 -0
  143. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/selenium.py +0 -0
  144. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  145. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  146. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  147. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  148. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  149. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  150. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  151. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  152. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  153. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  154. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  155. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  156. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  157. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
  158. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  159. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  160. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  161. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/version.py +0 -0
  162. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/__init__.py +0 -0
  163. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/compose.py +0 -0
  164. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/context.py +0 -0
  165. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/credentials.py +0 -0
  166. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/image_registry.py +0 -0
  167. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/index_refresh.py +0 -0
  168. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/marketplace_url.py +0 -0
  169. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/preflight.py +0 -0
  170. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/pypi_quota.py +0 -0
  171. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/registry.py +0 -0
  172. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/release_dry_run.py +0 -0
  173. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/release_gate.py +0 -0
  174. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/spl_store.py +0 -0
  175. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/__init__.py +0 -0
  176. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/archetype.py +0 -0
  177. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/cache_utils.py +0 -0
  178. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/command_loader.py +0 -0
  179. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  180. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/db_client.py +0 -0
  181. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/db_utils.py +0 -0
  182. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/decorators.py +0 -0
  183. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  184. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/feature_installer.py +0 -0
  185. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/feature_utils.py +0 -0
  186. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/forge.py +0 -0
  187. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/git_url.py +0 -0
  188. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/integrity.py +0 -0
  189. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/lifecycle.py +0 -0
  190. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/manifest.py +0 -0
  191. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/path_utils.py +0 -0
  192. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/proc.py +0 -0
  193. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/redact.py +0 -0
  194. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/refinement_facts.py +0 -0
  195. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/template_drift.py +0 -0
  196. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
  197. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli.egg-info/SOURCES.txt +0 -0
  198. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  199. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  200. {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli.egg-info/requires.txt +0 -0
  201. {splent_cli-1.29.0 → splent_cli-1.30.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.29.0
3
+ Version: 1.30.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.29.0"
7
+ version = "1.30.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,6 +3,7 @@ feature:release — Release a feature: bump version, tag, publish to the git for
3
3
  """
4
4
 
5
5
  import ast
6
+ import json
6
7
  import os
7
8
  import re
8
9
  import shutil
@@ -363,6 +364,83 @@ def _extract_commands(commands_path: Path) -> list[str]:
363
364
  return sorted(set(re.findall(r"""@click\.command\s*\(\s*['"]([^'"]+)['"]""", text)))
364
365
 
365
366
 
367
+ def _call_name(node) -> str:
368
+ """``click.command`` for the decorator call ``@click.command(...)``."""
369
+ func = node.func if isinstance(node, ast.Call) else node
370
+ parts = []
371
+ while isinstance(func, ast.Attribute):
372
+ parts.append(func.attr)
373
+ func = func.value
374
+ if isinstance(func, ast.Name):
375
+ parts.append(func.id)
376
+ return ".".join(reversed(parts))
377
+
378
+
379
+ def _literal(node):
380
+ try:
381
+ return ast.literal_eval(node)
382
+ except (ValueError, SyntaxError, TypeError):
383
+ return None
384
+
385
+
386
+ def _first_line(text) -> str:
387
+ for line in (text or "").strip().splitlines():
388
+ if line.strip():
389
+ return line.strip()
390
+ return ""
391
+
392
+
393
+ def _extract_command_help(commands_path: Path) -> dict:
394
+ """What each command a feature adds is for, and how it is called.
395
+
396
+ ``{name: {"usage": "EMAIL ROLE [OPTIONS]", "help": "..."}}``, read from
397
+ the source without importing it: the arguments in the order they are
398
+ declared, in click's uppercase, ``[OPTIONS]`` when there are any, and the
399
+ first line of ``short_help``, ``help`` or the docstring. The marketplace
400
+ shows it next to each command, which used to be a bare name.
401
+ """
402
+ if not commands_path.exists():
403
+ return {}
404
+ try:
405
+ tree = ast.parse(commands_path.read_text())
406
+ except SyntaxError:
407
+ return {}
408
+ found = {}
409
+ for node in ast.walk(tree):
410
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
411
+ continue
412
+ name, help_text, usage, has_options = None, "", [], False
413
+ for deco in node.decorator_list:
414
+ if not isinstance(deco, ast.Call):
415
+ continue
416
+ kind = _call_name(deco)
417
+ if kind.endswith("command") and deco.args:
418
+ name = _literal(deco.args[0])
419
+ kwargs = {k.arg: _literal(k.value) for k in deco.keywords}
420
+ help_text = kwargs.get("short_help") or kwargs.get("help") or ""
421
+ elif kind.endswith("argument") and deco.args:
422
+ arg = _literal(deco.args[0])
423
+ if not isinstance(arg, str):
424
+ continue
425
+ kwargs = {k.arg: _literal(k.value) for k in deco.keywords}
426
+ metavar = arg.upper().replace("-", "_")
427
+ if kwargs.get("nargs") == -1:
428
+ metavar += "..."
429
+ optional = kwargs.get("required") is False or "default" in kwargs
430
+ usage.append(f"[{metavar}]" if optional else metavar)
431
+ elif kind.endswith("option"):
432
+ has_options = True
433
+ if not isinstance(name, str):
434
+ continue
435
+ if has_options:
436
+ usage.append("[OPTIONS]")
437
+ found[name] = {
438
+ "usage": " ".join(usage),
439
+ "help": _first_line(help_text) or _first_line(ast.get_docstring(node)),
440
+ }
441
+ return dict(sorted(found.items()))
442
+
443
+
366
444
  def _feature_sources(src_dir: Path):
367
445
  """The feature's own Python files, without its tests.
368
446
 
@@ -693,6 +771,7 @@ def infer_contract(
693
771
  templates = _extract_templates(src_dir)
694
772
  template_hook_slots = _extract_template_hook_slots(src_dir)
695
773
  commands = _extract_commands(src_dir / "commands.py")
774
+ commands_help = _extract_command_help(src_dir / "commands.py")
696
775
  docker = _extract_docker(feature_root)
697
776
  docker_contract = _extract_docker_contract(feature_root)
698
777
  req_features, env_vars = _scan_dependencies(src_dir, feature_name)
@@ -751,6 +830,7 @@ def infer_contract(
751
830
  "blueprints": blueprints,
752
831
  "models": models,
753
832
  "commands": commands,
833
+ "commands_help": commands_help,
754
834
  "hooks": hooks,
755
835
  "services": services,
756
836
  "signals": signals_provided,
@@ -945,7 +1025,8 @@ def write_contract(pyproject_path: str, contract: dict, feature_name: str) -> No
945
1025
  f"signals = {_toml_value(contract.get('signals', []))}\n"
946
1026
  f"translations = {_toml_value(contract.get('translations', []))}\n"
947
1027
  f"docker = {_toml_value(contract['docker'])}\n"
948
- "\n"
1028
+ + _commands_help_block(contract.get("commands_help") or {})
1029
+ + "\n"
949
1030
  "[tool.splent.contract.requires]\n"
950
1031
  f"features = {_toml_value(contract['requires_features'])}\n"
951
1032
  + (
@@ -989,6 +1070,20 @@ def write_contract(pyproject_path: str, contract: dict, feature_name: str) -> No
989
1070
  path.write_text(text + contract_block + refinement_block)
990
1071
 
991
1072
 
1073
+ def _commands_help_block(commands_help: dict) -> str:
1074
+ """The ``[tool.splent.contract.provides.commands_help]`` table, or nothing."""
1075
+ if not commands_help:
1076
+ return ""
1077
+ lines = ["\n[tool.splent.contract.provides.commands_help]\n"]
1078
+ for name, entry in commands_help.items():
1079
+ key = name if re.fullmatch(r"[A-Za-z0-9_-]+", name) else json.dumps(name)
1080
+ lines.append(
1081
+ f"{key} = {{ usage = {json.dumps(entry.get('usage', ''))}, "
1082
+ f"help = {json.dumps(entry.get('help', ''))} }}\n"
1083
+ )
1084
+ return "".join(lines)
1085
+
1086
+
992
1087
  # ── Compile assets ────────────────────────────────────────────────────
993
1088
 
994
1089
 
@@ -2,8 +2,11 @@
2
2
  product:release, a product release: version bump, tag, forge, PyPI and the image.
3
3
  """
4
4
 
5
+ import contextlib
6
+ import json
5
7
  import os
6
8
  import subprocess
9
+ import tempfile
7
10
 
8
11
  import click
9
12
  from splent_cli.commands.product.product_build import (
@@ -63,7 +66,32 @@ def release_targets(product: str, version: str, product_path: str, org: str):
63
66
  return image_registry.image_targets(product, version, org, registries, mirrors)
64
67
 
65
68
 
66
- def _login(target) -> bool:
69
+ #: Docker Hub answers to these names in a client's configuration.
70
+ _DOCKER_HUB_HOSTS = ("index.docker.io", "registry-1.docker.io", "docker.io")
71
+
72
+
73
+ @contextlib.contextmanager
74
+ def registry_session(hosts=()):
75
+ """A docker client configuration of its own for one release's logins and pushes.
76
+
77
+ The host's configuration may hand credentials to a helper, the macOS
78
+ keychain under Docker Desktop, and that helper refuses to store a login
79
+ for a host the keychain already holds a git password for ("The specified
80
+ item already exists in the keychain"), which stopped the marketplace's
81
+ first release. A configuration of its own also keeps the release tokens
82
+ off the machine: it is deleted when the release is done.
83
+ """
84
+ # An empty credsStore is not enough: the docker client then detects the
85
+ # platform's helper again. An empty helper per host is what makes it keep
86
+ # the login in this file.
87
+ helpers = {host: "" for host in (*hosts, *_DOCKER_HUB_HOSTS) if host}
88
+ with tempfile.TemporaryDirectory(prefix="splent-docker-") as config_dir:
89
+ with open(os.path.join(config_dir, "config.json"), "w") as handle:
90
+ json.dump({"auths": {}, "credHelpers": helpers}, handle)
91
+ yield {**os.environ, "DOCKER_CONFIG": config_dir}
92
+
93
+
94
+ def _login(target, env=None) -> bool:
67
95
  user, secret = target.credentials()
68
96
  if not user or not secret:
69
97
  return False
@@ -71,7 +99,7 @@ def _login(target) -> bool:
71
99
  if target.host:
72
100
  command.insert(2, target.host)
73
101
  try:
74
- run(command, input=secret, capture=True)
102
+ run(command, input=secret, capture=True, env=env)
75
103
  except subprocess.CalledProcessError:
76
104
  return False
77
105
  return True
@@ -105,51 +133,52 @@ def release_docker_image(product: str, version: str, docker_dir: str, targets):
105
133
  )
106
134
  raise SystemExit(1)
107
135
 
108
- usable = []
109
- for target in targets:
110
- click.echo(f" docker logging into {target.label}...")
111
- if _login(target):
112
- usable.append(target)
113
- continue
114
- if target.mirror:
115
- click.secho(
116
- f" docker the {target.label} mirror is left out, its login failed "
117
- f"or {' / '.join(target.credential_names())} are not set",
118
- fg="yellow",
119
- )
120
- continue
121
- user_env, secret_env = target.credential_names()
122
- click.secho(
123
- f" error: could not log into {target.label}. Check {user_env} and "
124
- f"{secret_env}.",
125
- fg="red",
126
- )
127
- raise SystemExit(1)
128
-
129
- refs = [ref for target in usable for ref in target.refs]
130
- click.echo(" docker building image...")
131
- try:
132
- build_production_image(product, str(context.workspace()), docker_dir, refs)
133
- except subprocess.CalledProcessError:
134
- click.secho(" error: Docker image build failed.", fg="red")
135
- raise SystemExit(1)
136
-
137
- for target in usable:
138
- click.echo(f" docker pushing to {target.label}...")
139
- try:
140
- for ref in target.refs:
141
- run(["docker", "push", ref])
142
- except subprocess.CalledProcessError:
136
+ with registry_session([t.host for t in targets]) as docker_env:
137
+ usable = []
138
+ for target in targets:
139
+ click.echo(f" docker logging into {target.label}...")
140
+ if _login(target, docker_env):
141
+ usable.append(target)
142
+ continue
143
143
  if target.mirror:
144
144
  click.secho(
145
- f" docker the push to the {target.label} mirror failed and is "
146
- "left out",
145
+ f" docker the {target.label} mirror is left out, its login "
146
+ f"failed or {' / '.join(target.credential_names())} are not set",
147
147
  fg="yellow",
148
148
  )
149
149
  continue
150
- click.secho(f" error: the push to {target.label} failed.", fg="red")
150
+ user_env, secret_env = target.credential_names()
151
+ click.secho(
152
+ f" error: could not log into {target.label}. Check {user_env} and "
153
+ f"{secret_env}.",
154
+ fg="red",
155
+ )
156
+ raise SystemExit(1)
157
+
158
+ refs = [ref for target in usable for ref in target.refs]
159
+ click.echo(" docker building image...")
160
+ try:
161
+ build_production_image(product, str(context.workspace()), docker_dir, refs)
162
+ except subprocess.CalledProcessError:
163
+ click.secho(" error: Docker image build failed.", fg="red")
151
164
  raise SystemExit(1)
152
- click.echo(f" docker {', '.join(target.refs)}")
165
+
166
+ for target in usable:
167
+ click.echo(f" docker pushing to {target.label}...")
168
+ try:
169
+ for ref in target.refs:
170
+ run(["docker", "push", ref], env=docker_env)
171
+ except subprocess.CalledProcessError:
172
+ if target.mirror:
173
+ click.secho(
174
+ f" docker the push to the {target.label} mirror failed "
175
+ "and is left out",
176
+ fg="yellow",
177
+ )
178
+ continue
179
+ click.secho(f" error: the push to {target.label} failed.", fg="red")
180
+ raise SystemExit(1)
181
+ click.echo(f" docker {', '.join(target.refs)}")
153
182
 
154
183
  click.echo(" docker push complete")
155
184
 
@@ -21,6 +21,7 @@ saying what came back.
21
21
 
22
22
  import os
23
23
  import re
24
+ import time
24
25
  import tomllib
25
26
 
26
27
  import click
@@ -225,6 +226,119 @@ def _concept_doi(result: dict) -> str:
225
226
  return ""
226
227
 
227
228
 
229
+ # ---------------------------------------------------------------------------
230
+ # Waiting for the DOI
231
+ # ---------------------------------------------------------------------------
232
+
233
+ #: Seconds between two questions about a release that is still being minted.
234
+ POLL_INTERVAL = 5
235
+ #: How long to keep asking about a release that is still being minted. Longer
236
+ #: than the marketplace waits on a mint before it goes looking for the DOI.
237
+ WAIT_LIMIT = 20 * 60
238
+ #: How long to keep asking once the marketplace says it lost track of the
239
+ #: mint. It looks for the DOI about once a minute, so this is a few looks.
240
+ UNKNOWN_GRACE = 5 * 60
241
+ #: How often to say it is still going, so a quiet terminal does not read as
242
+ #: a hung one.
243
+ PROGRESS_EVERY = 30
244
+
245
+ #: Error codes that waiting longer cannot fix.
246
+ _FINAL_CODES = frozenset(
247
+ {
248
+ marketplace_api.CODE_FORBIDDEN,
249
+ marketplace_api.CODE_NOT_FOUND,
250
+ marketplace_api.CODE_INVALID_REQUEST,
251
+ }
252
+ )
253
+
254
+ # Swapped out by the tests, so nothing there really waits.
255
+ _sleep = time.sleep
256
+ _clock = time.monotonic
257
+
258
+
259
+ def _duration(seconds: float) -> str:
260
+ minutes, rest = divmod(int(seconds), 60)
261
+ return f"{minutes}m {rest:02d}s" if minutes else f"{rest}s"
262
+
263
+
264
+ def _wait_for_release(client, spl_name: str, accepted: dict) -> dict:
265
+ """Ask about an accepted release until the marketplace settles it.
266
+
267
+ Returns the last answer: published or failed, or still reserved or
268
+ unknown once the limits above run out. Nothing is written down until it
269
+ says published, so stopping early, by limit or by Ctrl+C, loses nothing.
270
+ """
271
+ version = accepted.get("version")
272
+ if not version:
273
+ raise marketplace_api.MarketplaceError(
274
+ "The marketplace accepted the release but did not say which version it is.",
275
+ status=None,
276
+ code=marketplace_api.CODE_BAD_RESPONSE,
277
+ )
278
+ click.echo(
279
+ f" Release {version} accepted. Minting its DOI, which usually takes a "
280
+ "minute or two."
281
+ )
282
+
283
+ started = last_report = _clock()
284
+ unknown_since = None
285
+ status = accepted
286
+ warned = False
287
+ while True:
288
+ state = str(status.get("state") or "")
289
+ if state not in ("reserved", "unknown"):
290
+ return status
291
+ now = _clock()
292
+ if state == "unknown" and unknown_since is None:
293
+ unknown_since = now
294
+ if now - started >= WAIT_LIMIT or (
295
+ unknown_since is not None and now - unknown_since >= UNKNOWN_GRACE
296
+ ):
297
+ return status
298
+ if now - last_report >= PROGRESS_EVERY:
299
+ click.echo(f" Still minting ({_duration(now - started)}).")
300
+ last_report = now
301
+ _sleep(POLL_INTERVAL)
302
+ try:
303
+ status = client.release_status(spl_name, version)
304
+ except marketplace_api.MarketplaceError as e:
305
+ if e.dead_token or e.code in _FINAL_CODES:
306
+ raise
307
+ # A blip between two questions says nothing about the release,
308
+ # which carries on in the marketplace either way.
309
+ if not warned:
310
+ click.secho(
311
+ f" The marketplace did not answer ({e.reason or e}). "
312
+ "Asking again.",
313
+ fg="yellow",
314
+ )
315
+ warned = True
316
+
317
+
318
+ def _come_back_later(spl_name: str, version, state: str) -> None:
319
+ """What to do about a release that has not settled yet. Always exits."""
320
+ click.echo()
321
+ if state == "unknown":
322
+ click.secho(
323
+ f" Release {version} did not report back, so the marketplace is "
324
+ "looking for its DOI.",
325
+ fg="yellow",
326
+ )
327
+ else:
328
+ click.secho(
329
+ f" Release {version} is still being minted. The marketplace carries "
330
+ "on with it.",
331
+ fg="yellow",
332
+ )
333
+ click.echo(
334
+ f" Run splent spl:publish {spl_name} again later. The same model is "
335
+ "answered with its DOI once it exists, and is never minted twice."
336
+ )
337
+ click.echo(" Nothing was written down locally yet.")
338
+ click.echo()
339
+ raise SystemExit(1)
340
+
341
+
228
342
  # ---------------------------------------------------------------------------
229
343
  # Command
230
344
  # ---------------------------------------------------------------------------
@@ -315,6 +429,7 @@ def spl_publish(spl_name, registry_option, dry_run):
315
429
  click.echo()
316
430
  click.echo(" Would POST the model to /api/v1/spls/<name>/releases and:")
317
431
  click.echo(" - let the marketplace publish or version it on its own key")
432
+ click.echo(" - wait while it mints the DOI")
318
433
  click.echo(" - let it verify the published file byte for byte")
319
434
  click.echo(" - record the DOI it reports")
320
435
  if metadata_path:
@@ -360,6 +475,36 @@ def spl_publish(spl_name, registry_option, dry_run):
360
475
  click.echo()
361
476
  raise SystemExit(1)
362
477
 
478
+ if result.get("accepted"):
479
+ try:
480
+ result = _wait_for_release(client, spl_name, result)
481
+ except marketplace_api.MarketplaceError as e:
482
+ handle_error(e, url=target.url, forget=not cred.from_environment)
483
+ click.echo()
484
+ raise SystemExit(1)
485
+ except KeyboardInterrupt:
486
+ click.echo()
487
+ click.secho(" Stopped waiting.", fg="yellow")
488
+ click.echo(
489
+ f" The marketplace carries on with release {result.get('version')}. "
490
+ f"Run splent spl:publish {spl_name} again to pick up its DOI."
491
+ )
492
+ raise SystemExit(130)
493
+ state = str(result.get("state") or "")
494
+ if state == "failed":
495
+ click.secho(
496
+ f" Release {result.get('version')} was refused. Nothing was minted.",
497
+ fg="red",
498
+ )
499
+ detail = str(result.get("detail") or "").strip()
500
+ if detail:
501
+ click.echo(f" {detail}")
502
+ click.echo(" Fix the model and publish it again.")
503
+ click.echo()
504
+ raise SystemExit(1)
505
+ if state != "published":
506
+ _come_back_later(spl_name, result.get("version"), state)
507
+
363
508
  doi, remote_file = _pointer(result)
364
509
  concept_doi = _concept_doi(result)
365
510
  if not doi:
@@ -19,7 +19,8 @@ Layout of the index document (schema 1)::
19
19
  }
20
20
 
21
21
  Feature entries carry the full contract (provides / requires / extensible /
22
- docker), the derived archetype, presentation metadata (description,
22
+ docker), the settings the feature declares under ``[tool.splent.config]``
23
+ (``config``), the derived archetype, presentation metadata (description,
23
24
  category, tags), distribution state (latest tag, PyPI presence) and the
24
25
  computed reverse dependencies (``used_by``).
25
26
  """
@@ -36,6 +37,11 @@ import click
36
37
  INDEX_SCHEMA = 1
37
38
  FEATURE_PREFIX = "splent_feature_"
38
39
 
40
+ # Where a feature declares the variables it reads, and the value a setting
41
+ # holds when only the deployment can supply it (a secret).
42
+ CONFIG_TABLE = ("tool", "splent", "config")
43
+ REQUIRED_VALUE = "<SET>"
44
+
39
45
  # Feeds feature:search / feature:info when no explicit path is given.
40
46
  INDEX_URL_ENV = "SPLENT_INDEX_URL"
41
47
 
@@ -72,6 +78,67 @@ def index_cache_path(workspace: str) -> Path:
72
78
  # ── Feature entries ───────────────────────────────────────────────────
73
79
 
74
80
 
81
+ # The value types product:env writes into an env file. A table or a date
82
+ # under [tool.splent.config] never reaches the environment, so it is no
83
+ # setting a product can change.
84
+ _CONFIG_TYPES = (bool, int, float, str, list)
85
+
86
+
87
+ def _config_text(value) -> str:
88
+ """A declared value as the environment receives it from ``product:env``.
89
+
90
+ Booleans are ``true``/``false``, numbers and strings their plain text, and
91
+ a list its items joined by commas.
92
+ """
93
+ if isinstance(value, bool):
94
+ return "true" if value else "false"
95
+ if isinstance(value, list):
96
+ return ",".join(_config_text(item) for item in value)
97
+ return str(value)
98
+
99
+
100
+ def config_declarations(pyproject_text: str, data: dict | None = None) -> list[dict]:
101
+ """The settings a feature declares under ``[tool.splent.config]``.
102
+
103
+ One ``{"key", "default", "required", "help"}`` per key, in the order they
104
+ are declared (tomllib keeps it), for every key whose value reaches the
105
+ environment. ``default`` is the value as text (see :func:`_config_text`).
106
+ A value of ``<SET>`` means only the deployment can supply it, so it gives
107
+ ``required`` true and an empty ``default``. ``help`` is the comment above
108
+ the key, or trailing its value (see :func:`toml_edit.key_comments`), and
109
+ ``""`` when there is none.
110
+
111
+ The values come from tomllib (*data*, parsed from *pyproject_text* when
112
+ not given) and only the comments from the text, so an unusual layout
113
+ costs the help and never a key or a value.
114
+ """
115
+ from splent_cli.utils.toml_edit import TomlEditError, key_comments
116
+
117
+ if data is None:
118
+ data = tomllib.loads(pyproject_text)
119
+ declared = data.get("tool", {}).get("splent", {}).get("config", {})
120
+ if not isinstance(declared, dict) or not declared:
121
+ return []
122
+ try:
123
+ comments = key_comments(pyproject_text, CONFIG_TABLE)
124
+ except TomlEditError:
125
+ comments = {}
126
+ settings = []
127
+ for key, value in declared.items():
128
+ if not isinstance(value, _CONFIG_TYPES):
129
+ continue
130
+ required = isinstance(value, str) and value.strip() == REQUIRED_VALUE
131
+ settings.append(
132
+ {
133
+ "key": key,
134
+ "default": "" if required else _config_text(value),
135
+ "required": required,
136
+ "help": comments.get(key, ""),
137
+ }
138
+ )
139
+ return settings
140
+
141
+
75
142
  def feature_entry_from_pyproject(
76
143
  pyproject_text: str,
77
144
  *,
@@ -109,6 +176,7 @@ def feature_entry_from_pyproject(
109
176
  "provides": contract.get("provides", {}),
110
177
  "requires": contract.get("requires", {}),
111
178
  "extensible": contract.get("extensible", {}),
179
+ "config": config_declarations(pyproject_text, data),
112
180
  "docker": contract.get("docker") or None,
113
181
  "refinement": splent.get("refinement") or None,
114
182
  "cli_version": splent.get("cli_version"),
@@ -17,6 +17,10 @@ extra fields and loud about missing ones)::
17
17
  GET /api/v1/spls public
18
18
  GET /api/v1/spls/<name> public -> {name, description, uvl{...}}
19
19
  POST /api/v1/spls/<name>/releases Bearer, multipart with the uvl file
20
+ Prefer: respond-async -> 202 {accepted,
21
+ version, state} while the DOI is minted
22
+ GET /api/v1/spls/<name>/releases/<version>
23
+ Bearer -> {state, doi, uvl{...}, ...}
20
24
 
21
25
  Nothing here prints, and nothing here ever puts a token in a message: an error
22
26
  string from this module is safe to show to the user verbatim.
@@ -36,6 +40,11 @@ USER_AGENT = "splent-cli"
36
40
  DEFAULT_TIMEOUT = 15
37
41
  UPLOAD_TIMEOUT = 60
38
42
 
43
+ #: RFC 7240. Asks the marketplace to answer a release as soon as it is
44
+ #: reserved and mint the DOI in the background. A marketplace that predates
45
+ #: it ignores the header and answers when the DOI exists, as it always did.
46
+ PREFER_ASYNC = "respond-async"
47
+
39
48
  #: Lifetime requested for a token created by ``splent login``.
40
49
  DEFAULT_TTL_DAYS = 90
41
50
 
@@ -451,6 +460,7 @@ class MarketplaceClient:
451
460
  timeout: int | None = None,
452
461
  unauthenticated_code: str | None = None,
453
462
  sent_but_unanswered: str | None = None,
463
+ extra_headers: dict | None = None,
454
464
  ):
455
465
  """Perform the request. Returns parsed JSON, or None for an empty body.
456
466
 
@@ -465,7 +475,7 @@ class MarketplaceClient:
465
475
  response = requests.request(
466
476
  method,
467
477
  url,
468
- headers=self._headers(auth=auth),
478
+ headers={**self._headers(auth=auth), **(extra_headers or {})},
469
479
  json=json_body,
470
480
  files=files,
471
481
  data=data,
@@ -637,6 +647,10 @@ class MarketplaceClient:
637
647
  spends anything: on the far side the marketplace relays it to UVLHub
638
648
  with its own key and a DOI may be minted. ``splent spl:publish`` is its
639
649
  only caller and there is meant to be exactly one.
650
+
651
+ It asks for the answer as soon as the release is reserved. A body with
652
+ ``accepted`` set means the DOI is still being minted, and
653
+ :meth:`release_status` says how it ends.
640
654
  """
641
655
  files = {
642
656
  UVL_FIELD_NAME: (filename, uvl_bytes, "text/plain"),
@@ -651,10 +665,24 @@ class MarketplaceClient:
651
665
  sent_but_unanswered=(
652
666
  f"The upload of {name} was sent to the marketplace at {self.base_url} "
653
667
  f"but no answer came back within {UPLOAD_TIMEOUT} seconds. The "
654
- "release may have completed, so check the product line page on "
655
- "the marketplace before publishing again."
668
+ "release may have completed. Publishing the same model again is "
669
+ "safe. The marketplace answers it with the DOI it minted and "
670
+ "never mints a second one."
656
671
  ),
672
+ extra_headers={"Prefer": PREFER_ASYNC},
657
673
  )
658
674
  if payload is None:
659
675
  return {}
660
676
  return self._expect_dict(payload, "release response")
677
+
678
+ def release_status(self, name: str, version) -> dict:
679
+ """How one release of *name* stands: reserved, published, failed or unknown.
680
+
681
+ The path is built here from the name and the version, never taken from
682
+ the server's ``status_url``: the token goes with this request, and it
683
+ only ever goes to the marketplace it was issued by.
684
+ """
685
+ payload = self._request(
686
+ "GET", f"/api/v1/spls/{name}/releases/{int(version)}", auth=True
687
+ )
688
+ return self._expect_dict(payload, "release status")