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.
- {splent_cli-1.29.0/src/splent_cli.egg-info → splent_cli-1.30.0}/PKG-INFO +1 -1
- {splent_cli-1.29.0 → splent_cli-1.30.0}/pyproject.toml +1 -1
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_release.py +96 -1
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_release.py +70 -41
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_publish.py +145 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/marketplace.py +69 -1
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/marketplace_api.py +31 -3
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/release.py +54 -4
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/io_utils.py +10 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/toml_edit.py +48 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
- {splent_cli-1.29.0 → splent_cli-1.30.0}/LICENSE +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/README.md +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/setup.cfg +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/cli.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_deps.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_env.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_features.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_infra.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_product.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/check/check_releases.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/clear/__init__.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/command/__init__.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/command/command_create.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/coverage.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_console.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_dump.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_reset.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_restore.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_seed.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_status.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/doctor.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/env/env_set.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/export/__init__.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/export/export_puml.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_test.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/lint.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/locust.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/marketplace/login.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_build.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_clean.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_commands.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_config.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_configure.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_console.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_containers.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_create.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_derive.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_down.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_drift.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_env.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_missing.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_port.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_restart.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_routes.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_run.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_select.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_signals.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_test.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_up.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/product/product_validate.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/release/release_core.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/release/release_resume.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/selenium.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/__init__.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/tokens_setup.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/commands/version.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/__init__.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/compose.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/context.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/credentials.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/image_registry.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/index_refresh.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/marketplace_url.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/preflight.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/pypi_quota.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/registry.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/release_dry_run.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/release_gate.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/services/spl_store.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/__init__.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/archetype.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/cache_utils.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/command_loader.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/contract_freshness.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/db_client.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/feature_installer.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/feature_utils.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/forge.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/git_url.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/integrity.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/lifecycle.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/manifest.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/proc.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/redact.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/refinement_facts.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/template_drift.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli.egg-info/SOURCES.txt +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.29.0 → splent_cli-1.30.0}/src/splent_cli.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
146
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
655
|
-
"
|
|
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")
|