splent-cli 1.26.1__tar.gz → 1.27.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.26.1/src/splent_cli.egg-info → splent_cli-1.27.0}/PKG-INFO +2 -2
- {splent_cli-1.26.1 → splent_cli-1.27.0}/pyproject.toml +2 -2
- splent_cli-1.27.0/src/splent_cli/__init__.py +15 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/cli.py +2 -0
- splent_cli-1.27.0/src/splent_cli/commands/cache/cache_clear.py +208 -0
- splent_cli-1.27.0/src/splent_cli/commands/cache/cache_orphans.py +58 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/cache/cache_outdated.py +35 -41
- splent_cli-1.27.0/src/splent_cli/commands/cache/cache_prune.py +148 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/cache/cache_status.py +5 -19
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/cache/cache_usage.py +14 -21
- splent_cli-1.27.0/src/splent_cli/commands/cache/cache_versions.py +100 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/check/check_contracts.py +3 -5
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/check/check_deps.py +15 -2
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/check/check_env.py +7 -3
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/check/check_features.py +2 -2
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/check/check_github.py +28 -5
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/check/check_infra.py +134 -60
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/check/check_product.py +32 -12
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/check/check_pypi.py +6 -6
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/check/check_pyproject.py +47 -17
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/check/check_releases.py +12 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/clear/clear_log.py +1 -0
- splent_cli-1.27.0/src/splent_cli/commands/clear/clear_uploads.py +110 -0
- splent_cli-1.27.0/src/splent_cli/commands/command/command_create.py +97 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/coverage.py +11 -2
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/database/db_console.py +3 -1
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/database/db_dump.py +38 -9
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/database/db_migrate.py +69 -23
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/database/db_reset.py +169 -43
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/database/db_restore.py +40 -10
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/database/db_rollback.py +81 -18
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/database/db_seed.py +148 -44
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/database/db_status.py +36 -20
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/database/db_upgrade.py +111 -12
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/doctor.py +12 -1
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/env/env_list.py +16 -26
- splent_cli-1.27.0/src/splent_cli/commands/env/env_set.py +265 -0
- splent_cli-1.27.0/src/splent_cli/commands/env/env_show.py +52 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/export/export_puml.py +264 -122
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_add.py +21 -10
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_attach.py +53 -14
- splent_cli-1.27.0/src/splent_cli/commands/feature/feature_clean.py +583 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_clone.py +26 -14
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_compat.py +37 -4
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_compile.py +23 -5
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_contract.py +46 -5
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_create.py +8 -1
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_delete.py +24 -7
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_detach.py +29 -4
- splent_cli-1.27.0/src/splent_cli/commands/feature/feature_drift.py +285 -0
- splent_cli-1.27.0/src/splent_cli/commands/feature/feature_hook_add.py +406 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_hook_remove.py +66 -81
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_info.py +3 -2
- splent_cli-1.27.0/src/splent_cli/commands/feature/feature_inject_config.py +374 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_install.py +135 -75
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_list.py +7 -4
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_order.py +27 -6
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_outdated.py +15 -11
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_pin.py +24 -7
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_pip_install.py +14 -4
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_refine.py +225 -46
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_release.py +299 -105
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_remove.py +22 -3
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_rename.py +87 -41
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_search.py +11 -8
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_sync_template.py +52 -4
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_test.py +135 -26
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_translate.py +40 -11
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_unlock.py +73 -2
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_upgrade.py +62 -18
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_versions.py +89 -32
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_xray.py +19 -22
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/lint.py +5 -1
- splent_cli-1.27.0/src/splent_cli/commands/locust.py +305 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/marketplace/login.py +22 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/marketplace/logout.py +42 -2
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/marketplace/marketplace_index.py +24 -10
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_auto_require.py +55 -9
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_build.py +296 -90
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_clean.py +45 -36
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_config.py +53 -7
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_containers.py +34 -17
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_create.py +80 -17
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_deploy.py +106 -50
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_derive.py +44 -21
- splent_cli-1.27.0/src/splent_cli/commands/product/product_down.py +211 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_drift.py +4 -4
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_env.py +93 -130
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_missing.py +6 -4
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_port.py +37 -24
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_release.py +75 -20
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_resolve.py +42 -4
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_restart.py +72 -21
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_run.py +28 -24
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_select.py +35 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_shell.py +35 -15
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_sync_template.py +7 -6
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_test.py +7 -3
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_up.py +65 -19
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/release/release_core.py +4 -0
- splent_cli-1.27.0/src/splent_cli/commands/release/release_resume.py +760 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/selenium.py +34 -12
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/spl/spl_add_constraints.py +19 -5
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/spl/spl_add_feature.py +40 -2
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/spl/spl_compare.py +6 -3
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/spl/spl_create.py +43 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/spl/spl_deps.py +21 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/spl/spl_outdated.py +64 -19
- splent_cli-1.27.0/src/splent_cli/commands/spl/spl_pin.py +158 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/spl/spl_publish.py +73 -12
- splent_cli-1.27.0/src/splent_cli/commands/spl/spl_remove_feature.py +254 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/uvl/uvl_utils.py +4 -7
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/version.py +33 -8
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/services/compose.py +127 -40
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/services/context.py +63 -4
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/services/credentials.py +10 -1
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/services/marketplace.py +69 -8
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/services/marketplace_api.py +26 -1
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/services/marketplace_url.py +31 -4
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/services/preflight.py +55 -10
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/services/registry.py +17 -2
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/services/release.py +274 -55
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/services/release_dry_run.py +20 -6
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/services/release_gate.py +48 -12
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/services/spl_store.py +353 -61
- splent_cli-1.27.0/src/splent_cli/utils/archetype.py +245 -0
- splent_cli-1.27.0/src/splent_cli/utils/cache_utils.py +203 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/command_loader.py +14 -2
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/contract_freshness.py +61 -4
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/dynamic_imports.py +29 -57
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/feature_utils.py +124 -17
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/forge.py +90 -5
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/git_url.py +117 -10
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/integrity.py +43 -11
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/io_utils.py +140 -14
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/lifecycle.py +85 -21
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/manifest.py +47 -14
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/proc.py +48 -3
- splent_cli-1.27.0/src/splent_cli/utils/redact.py +62 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/template_drift.py +75 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0/src/splent_cli.egg-info}/PKG-INFO +2 -2
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli.egg-info/SOURCES.txt +1 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli.egg-info/requires.txt +1 -1
- splent_cli-1.26.1/src/splent_cli/commands/cache/cache_clear.py +0 -88
- splent_cli-1.26.1/src/splent_cli/commands/cache/cache_orphans.py +0 -98
- splent_cli-1.26.1/src/splent_cli/commands/cache/cache_prune.py +0 -123
- splent_cli-1.26.1/src/splent_cli/commands/cache/cache_versions.py +0 -47
- splent_cli-1.26.1/src/splent_cli/commands/clear/clear_uploads.py +0 -42
- splent_cli-1.26.1/src/splent_cli/commands/command/command_create.py +0 -60
- splent_cli-1.26.1/src/splent_cli/commands/env/env_set.py +0 -174
- splent_cli-1.26.1/src/splent_cli/commands/env/env_show.py +0 -62
- splent_cli-1.26.1/src/splent_cli/commands/feature/feature_clean.py +0 -347
- splent_cli-1.26.1/src/splent_cli/commands/feature/feature_drift.py +0 -145
- splent_cli-1.26.1/src/splent_cli/commands/feature/feature_hook_add.py +0 -186
- splent_cli-1.26.1/src/splent_cli/commands/feature/feature_inject_config.py +0 -216
- splent_cli-1.26.1/src/splent_cli/commands/locust.py +0 -140
- splent_cli-1.26.1/src/splent_cli/commands/product/product_down.py +0 -140
- splent_cli-1.26.1/src/splent_cli/commands/release/release_resume.py +0 -436
- splent_cli-1.26.1/src/splent_cli/commands/spl/spl_pin.py +0 -89
- splent_cli-1.26.1/src/splent_cli/commands/spl/spl_remove_feature.py +0 -184
- splent_cli-1.26.1/src/splent_cli/utils/__init__.py +0 -0
- splent_cli-1.26.1/src/splent_cli/utils/archetype.py +0 -56
- splent_cli-1.26.1/src/splent_cli/utils/cache_utils.py +0 -115
- {splent_cli-1.26.1 → splent_cli-1.27.0}/LICENSE +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/README.md +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/setup.cfg +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.26.1/src/splent_cli → splent_cli-1.27.0/src/splent_cli/commands}/__init__.py +0 -0
- {splent_cli-1.26.1/src/splent_cli/commands → splent_cli-1.27.0/src/splent_cli/commands/cache}/__init__.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.26.1/src/splent_cli/commands/cache → splent_cli-1.27.0/src/splent_cli/commands/check}/__init__.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.26.1/src/splent_cli/commands/check → splent_cli-1.27.0/src/splent_cli/commands/clear}/__init__.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
- {splent_cli-1.26.1/src/splent_cli/commands/clear → splent_cli-1.27.0/src/splent_cli/commands/command}/__init__.py +0 -0
- {splent_cli-1.26.1/src/splent_cli/commands/command → splent_cli-1.27.0/src/splent_cli/commands/export}/__init__.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
- {splent_cli-1.26.1/src/splent_cli/commands/export → splent_cli-1.27.0/src/splent_cli/commands/product}/__init__.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_commands.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_configure.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_console.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_routes.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_signals.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/product/product_validate.py +0 -0
- {splent_cli-1.26.1/src/splent_cli/commands/product → splent_cli-1.27.0/src/splent_cli/commands/release}/__init__.py +0 -0
- {splent_cli-1.26.1/src/splent_cli/commands/release → splent_cli-1.27.0/src/splent_cli/commands/spl}/__init__.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/commands/tokens_setup.py +0 -0
- {splent_cli-1.26.1/src/splent_cli/commands/spl → splent_cli-1.27.0/src/splent_cli/services}/__init__.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/services/index_refresh.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/services/pypi_quota.py +0 -0
- {splent_cli-1.26.1/src/splent_cli/services → splent_cli-1.27.0/src/splent_cli/utils}/__init__.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/feature_installer.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.26.1 → splent_cli-1.27.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.27.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
|
|
@@ -14,7 +14,7 @@ Requires-Dist: packaging>=24.0
|
|
|
14
14
|
Requires-Dist: python-dotenv>=1.0
|
|
15
15
|
Requires-Dist: pyyaml>=6.0
|
|
16
16
|
Requires-Dist: requests>=2.31.0
|
|
17
|
-
Requires-Dist: splent_framework<2,>=1.
|
|
17
|
+
Requires-Dist: splent_framework<2,>=1.16.0
|
|
18
18
|
Requires-Dist: tomli_w>=1.0.0
|
|
19
19
|
Provides-Extra: dev
|
|
20
20
|
Requires-Dist: setuptools==80.3.1; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "splent_cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.27.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"
|
|
@@ -24,7 +24,7 @@ dependencies = [
|
|
|
24
24
|
"requests>=2.31.0",
|
|
25
25
|
# The CLI drives the framework (paths, feature order, migrations, the
|
|
26
26
|
# app factory) and only works with the same major version of it.
|
|
27
|
-
"splent_framework>=1.
|
|
27
|
+
"splent_framework>=1.16.0,<2",
|
|
28
28
|
"tomli_w>=1.0.0",
|
|
29
29
|
]
|
|
30
30
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""The SPLENT command-line interface."""
|
|
2
|
+
|
|
3
|
+
import os as _os
|
|
4
|
+
from types import MappingProxyType as _MappingProxyType
|
|
5
|
+
|
|
6
|
+
# What the operator actually exported, taken before anything reads a .env.
|
|
7
|
+
#
|
|
8
|
+
# This must stay the first thing the package does. cli.py runs load_dotenv(),
|
|
9
|
+
# and so do framework modules it imports (path_utils, configuration), which
|
|
10
|
+
# puts every workspace .env key into os.environ before any command runs. Read
|
|
11
|
+
# afterwards, the shell and the file look identical: env:show could never say
|
|
12
|
+
# "not loaded", env:list never showed "·", and the workspace .env passed for
|
|
13
|
+
# shell values and beat the product's docker/.env. Compare against this
|
|
14
|
+
# instead of os.environ whenever the question is "what did the shell export".
|
|
15
|
+
SHELL_ENV = _MappingProxyType(dict(_os.environ))
|
|
@@ -6,6 +6,8 @@ from dotenv import load_dotenv
|
|
|
6
6
|
|
|
7
7
|
from splent_cli.utils.command_loader import load_commands
|
|
8
8
|
|
|
9
|
+
# From here on os.environ holds the workspace .env as well. What the shell
|
|
10
|
+
# itself exported is splent_cli.SHELL_ENV, taken before this line ran.
|
|
9
11
|
load_dotenv()
|
|
10
12
|
|
|
11
13
|
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from splent_cli.services import context, compose
|
|
2
|
+
import re
|
|
3
|
+
import click
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from splent_cli.utils.cache_utils import rmtree_force
|
|
7
|
+
from splent_cli.utils.feature_utils import normalize_namespace, parse_feature_entry
|
|
8
|
+
|
|
9
|
+
# A namespace, feature name or version is a single path component: it can
|
|
10
|
+
# never be empty, a path, "." or "..", so a scope always stays inside
|
|
11
|
+
# .splent_cache/features.
|
|
12
|
+
_NAME_RE = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.-]*$")
|
|
13
|
+
_VERSION_RE = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.+-]*$")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _plain(value: str, pattern=_NAME_RE) -> bool:
|
|
17
|
+
return bool(pattern.match(value)) and ".." not in value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _validate_namespace(ctx, param, value):
|
|
21
|
+
if value is None:
|
|
22
|
+
return None
|
|
23
|
+
if not _plain(value):
|
|
24
|
+
raise click.BadParameter(
|
|
25
|
+
"must be a namespace name such as splent_io or splent-io,"
|
|
26
|
+
" not empty and not a path."
|
|
27
|
+
)
|
|
28
|
+
return normalize_namespace(value)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _validate_feature(ctx, param, value):
|
|
32
|
+
if value is None:
|
|
33
|
+
return None
|
|
34
|
+
base, at, version = value.partition("@")
|
|
35
|
+
explicit_ns, slash, name = base.rpartition("/")
|
|
36
|
+
if (
|
|
37
|
+
not _plain(name)
|
|
38
|
+
or (slash and not _plain(explicit_ns))
|
|
39
|
+
or (at and not _plain(version, _VERSION_RE))
|
|
40
|
+
):
|
|
41
|
+
raise click.BadParameter(
|
|
42
|
+
"must be a feature name such as splent_feature_auth, optionally"
|
|
43
|
+
" with a namespace and a version (splent-io/splent_feature_auth@v1.2.0)."
|
|
44
|
+
)
|
|
45
|
+
ns, name, version = parse_feature_entry(value)
|
|
46
|
+
return {
|
|
47
|
+
"raw": value,
|
|
48
|
+
"namespace": ns if slash else None,
|
|
49
|
+
"name": name,
|
|
50
|
+
"version": version,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _matches(entry_name: str, name: str, version: str | None) -> bool:
|
|
55
|
+
if version:
|
|
56
|
+
return entry_name == f"{name}@{version}"
|
|
57
|
+
return entry_name.split("@", 1)[0] == name
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _delete(target: Path, cache_root: Path) -> None:
|
|
61
|
+
"""Delete one cache entry or namespace, refusing anything that is not a
|
|
62
|
+
real directory directly under ``cache_root`` or one of its namespaces.
|
|
63
|
+
|
|
64
|
+
Raises ``OSError`` when the target is refused or cannot be removed.
|
|
65
|
+
"""
|
|
66
|
+
root = cache_root.resolve()
|
|
67
|
+
parent = target.parent.resolve()
|
|
68
|
+
if (
|
|
69
|
+
target.is_symlink()
|
|
70
|
+
or not target.is_dir()
|
|
71
|
+
or target.resolve().parent != parent
|
|
72
|
+
or (parent != root and parent.parent != root)
|
|
73
|
+
):
|
|
74
|
+
raise OSError(
|
|
75
|
+
f"refusing to delete {target}, it is not a directory inside {cache_root}"
|
|
76
|
+
)
|
|
77
|
+
rmtree_force(target)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@click.command(
|
|
81
|
+
"cache:clear",
|
|
82
|
+
short_help="Clear the feature version cache (.splent_cache), totally or partially.",
|
|
83
|
+
)
|
|
84
|
+
@click.option(
|
|
85
|
+
"--namespace",
|
|
86
|
+
default=None,
|
|
87
|
+
callback=_validate_namespace,
|
|
88
|
+
help="Clear only a specific namespace (e.g. splent_io or splent-io).",
|
|
89
|
+
)
|
|
90
|
+
@click.option(
|
|
91
|
+
"--feature",
|
|
92
|
+
default=None,
|
|
93
|
+
callback=_validate_feature,
|
|
94
|
+
help=(
|
|
95
|
+
"Clear the entries of one feature, given as a bare name (all versions,"
|
|
96
|
+
" editable included), name@version or namespace/name[@version]."
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
@click.option("--yes", is_flag=True, help="Skip confirmation prompt.")
|
|
100
|
+
def cache_clear(namespace, feature, yes):
|
|
101
|
+
"""
|
|
102
|
+
Deletes entries from the feature cache and removes broken symlinks in products.
|
|
103
|
+
|
|
104
|
+
\b
|
|
105
|
+
Scope (most to least specific):
|
|
106
|
+
--feature splent_feature_auth → every entry of that feature
|
|
107
|
+
--feature splent_feature_auth@v1.2.0 → only that version
|
|
108
|
+
--feature splent-io/splent_feature_auth → that feature in one namespace
|
|
109
|
+
--namespace splent_io → the entire namespace folder
|
|
110
|
+
(no options) → the entire feature cache
|
|
111
|
+
"""
|
|
112
|
+
if feature is not None and feature["namespace"] is not None:
|
|
113
|
+
if namespace is not None and namespace != feature["namespace"]:
|
|
114
|
+
raise click.BadParameter(
|
|
115
|
+
f"names namespace '{feature['namespace']}' but --namespace"
|
|
116
|
+
f" is '{namespace}'.",
|
|
117
|
+
param_hint="'--feature'",
|
|
118
|
+
)
|
|
119
|
+
namespace = feature["namespace"]
|
|
120
|
+
|
|
121
|
+
workspace = context.workspace()
|
|
122
|
+
cache_root = workspace / ".splent_cache" / "features"
|
|
123
|
+
|
|
124
|
+
if not cache_root.is_dir():
|
|
125
|
+
click.secho("⚠️ No .splent_cache/features directory found.", fg="yellow")
|
|
126
|
+
raise SystemExit(0)
|
|
127
|
+
|
|
128
|
+
# Resolve targets
|
|
129
|
+
if feature is not None:
|
|
130
|
+
targets = []
|
|
131
|
+
if namespace is not None:
|
|
132
|
+
search_in = [cache_root / namespace]
|
|
133
|
+
else:
|
|
134
|
+
search_in = sorted(cache_root.iterdir())
|
|
135
|
+
for ns_dir in search_in:
|
|
136
|
+
if ns_dir.is_symlink() or not ns_dir.is_dir():
|
|
137
|
+
continue
|
|
138
|
+
for feat_dir in sorted(ns_dir.iterdir()):
|
|
139
|
+
if feat_dir.is_dir() and _matches(
|
|
140
|
+
feat_dir.name, feature["name"], feature["version"]
|
|
141
|
+
):
|
|
142
|
+
targets.append(feat_dir)
|
|
143
|
+
if not targets:
|
|
144
|
+
label = f"'{feature['raw']}'" + (
|
|
145
|
+
f" in namespace '{namespace}'" if namespace is not None else ""
|
|
146
|
+
)
|
|
147
|
+
click.secho(f"⚠️ No cache entries found for {label}.", fg="yellow")
|
|
148
|
+
raise SystemExit(0)
|
|
149
|
+
description = f"{len(targets)} entry/entries for feature '{feature['raw']}'"
|
|
150
|
+
elif namespace is not None:
|
|
151
|
+
targets = [cache_root / namespace]
|
|
152
|
+
if not targets[0].is_dir():
|
|
153
|
+
click.secho(f"⚠️ Namespace '{namespace}' not found in cache.", fg="yellow")
|
|
154
|
+
raise SystemExit(0)
|
|
155
|
+
description = f"namespace '{namespace}'"
|
|
156
|
+
else:
|
|
157
|
+
targets = sorted(cache_root.iterdir())
|
|
158
|
+
description = "entire feature cache"
|
|
159
|
+
|
|
160
|
+
if not yes and not click.confirm(
|
|
161
|
+
f"⚠️ This will permanently delete the {description}. Continue?"
|
|
162
|
+
):
|
|
163
|
+
click.echo("❎ Cancelled.")
|
|
164
|
+
raise SystemExit(0)
|
|
165
|
+
|
|
166
|
+
# Delete, one entry at a time so a failure never stops the cleanup.
|
|
167
|
+
failures = []
|
|
168
|
+
for t in targets:
|
|
169
|
+
try:
|
|
170
|
+
if (
|
|
171
|
+
feature is None
|
|
172
|
+
and namespace is None
|
|
173
|
+
and (t.is_symlink() or not t.is_dir())
|
|
174
|
+
):
|
|
175
|
+
# Stray file or link directly in features/: unlink, never follow.
|
|
176
|
+
t.unlink()
|
|
177
|
+
else:
|
|
178
|
+
_delete(t, cache_root)
|
|
179
|
+
except OSError as exc:
|
|
180
|
+
failures.append(t)
|
|
181
|
+
click.secho(
|
|
182
|
+
f" Could not delete {t.relative_to(cache_root)}: {exc}",
|
|
183
|
+
fg="red",
|
|
184
|
+
err=True,
|
|
185
|
+
)
|
|
186
|
+
cache_root.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
if not failures:
|
|
188
|
+
click.secho(f"🧹 Cleared: {description}.", fg="green")
|
|
189
|
+
|
|
190
|
+
# Clean broken symlinks
|
|
191
|
+
removed = compose.remove_broken_symlinks(workspace)
|
|
192
|
+
if removed:
|
|
193
|
+
click.secho(f"🔗 Removed {removed} broken feature symlink(s).", fg="yellow")
|
|
194
|
+
else:
|
|
195
|
+
click.secho("✅ No broken symlinks found.", fg="green")
|
|
196
|
+
|
|
197
|
+
if failures:
|
|
198
|
+
click.secho(
|
|
199
|
+
f"{len(failures)} cache entry/entries could not be deleted. Check the"
|
|
200
|
+
" filesystem permissions (files written by a container may belong"
|
|
201
|
+
" to root) and remove them by hand.",
|
|
202
|
+
fg="red",
|
|
203
|
+
err=True,
|
|
204
|
+
)
|
|
205
|
+
raise SystemExit(1)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
cli_command = cache_clear
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from splent_cli.services import context
|
|
2
|
+
import click
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from splent_cli.utils.cache_utils import (
|
|
6
|
+
cache_entries,
|
|
7
|
+
cache_entry_key,
|
|
8
|
+
product_ref_keys,
|
|
9
|
+
)
|
|
10
|
+
from splent_cli.utils.feature_utils import workspace_feature_entries
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_cache_entries(cache_root: Path) -> list:
|
|
14
|
+
"""Returns list of {namespace, name, version, is_versioned, path} dicts."""
|
|
15
|
+
return cache_entries(cache_root)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_all_product_refs(workspace: Path) -> set:
|
|
19
|
+
"""Every ``(namespace, name, version)`` any product declares, in any
|
|
20
|
+
environment list.
|
|
21
|
+
|
|
22
|
+
The namespace is normalised and a bare entry belongs to the default
|
|
23
|
+
namespace, so the keys compare directly with :func:`cache_entry_key`.
|
|
24
|
+
"""
|
|
25
|
+
return product_ref_keys(workspace_feature_entries(workspace))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@click.command(
|
|
29
|
+
"cache:orphans", short_help="Show cached features not referenced by any product."
|
|
30
|
+
)
|
|
31
|
+
def cache_orphans():
|
|
32
|
+
"""Lists features in the cache that no product references in its pyproject.toml."""
|
|
33
|
+
workspace = context.workspace()
|
|
34
|
+
cache_root = workspace / ".splent_cache" / "features"
|
|
35
|
+
|
|
36
|
+
entries = _get_cache_entries(cache_root)
|
|
37
|
+
if not entries:
|
|
38
|
+
click.secho("ℹ️ Feature cache is empty.", fg="yellow")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
refs = _get_all_product_refs(workspace)
|
|
42
|
+
|
|
43
|
+
orphans = []
|
|
44
|
+
for e in entries:
|
|
45
|
+
if cache_entry_key(e) not in refs:
|
|
46
|
+
ref_key = f"{e['name']}@{e['version']}" if e["is_versioned"] else e["name"]
|
|
47
|
+
orphans.append(f"{e['namespace']}/{ref_key}")
|
|
48
|
+
|
|
49
|
+
if not orphans:
|
|
50
|
+
click.secho("✅ No orphaned features in cache.", fg="green")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
click.secho(f"🗑 Orphaned cache entries ({len(orphans)}):", fg="yellow")
|
|
54
|
+
for o in orphans:
|
|
55
|
+
click.echo(f" - {o}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
cli_command = cache_orphans
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
from splent_cli.services import context
|
|
2
|
-
import re
|
|
3
2
|
import click
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
from collections import defaultdict
|
|
6
|
-
from packaging.version import Version
|
|
5
|
+
from packaging.version import Version
|
|
6
|
+
|
|
7
|
+
from splent_cli.commands.cache.cache_versions import parse_version, sort_versions
|
|
8
|
+
from splent_cli.utils.feature_utils import (
|
|
9
|
+
normalize_namespace,
|
|
10
|
+
parse_feature_entry,
|
|
11
|
+
workspace_feature_entries,
|
|
12
|
+
)
|
|
7
13
|
|
|
8
14
|
|
|
9
15
|
def _get_cache_versions(cache_root: Path) -> dict:
|
|
10
|
-
"""Returns {name: [version, ...]} with all versioned snapshots in cache."""
|
|
16
|
+
"""Returns {(namespace, name): [version, ...]} with all versioned snapshots in cache."""
|
|
11
17
|
versions = defaultdict(list)
|
|
12
18
|
if not cache_root.exists():
|
|
13
19
|
return versions
|
|
@@ -18,51 +24,36 @@ def _get_cache_versions(cache_root: Path) -> dict:
|
|
|
18
24
|
if not feat_dir.is_dir() or "@" not in feat_dir.name:
|
|
19
25
|
continue
|
|
20
26
|
name, version = feat_dir.name.split("@", 1)
|
|
21
|
-
versions[name].append(version)
|
|
27
|
+
versions[(normalize_namespace(ns_dir.name), name)].append(version)
|
|
22
28
|
return versions
|
|
23
29
|
|
|
24
30
|
|
|
25
31
|
def _latest(versions: list) -> str:
|
|
26
|
-
"""Returns the latest version string, using semver if possible.
|
|
32
|
+
"""Returns the latest version string, using semver if possible.
|
|
33
|
+
|
|
34
|
+
Valid versions beat unparseable ones; ties are broken by the raw string,
|
|
35
|
+
so the result never depends on the order of ``versions``.
|
|
36
|
+
"""
|
|
27
37
|
|
|
28
38
|
def sort_key(v):
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
except InvalidVersion:
|
|
32
|
-
return Version("0")
|
|
39
|
+
parsed = parse_version(v)
|
|
40
|
+
return (parsed is not None, parsed or Version("0"), v)
|
|
33
41
|
|
|
34
42
|
return max(versions, key=sort_key)
|
|
35
43
|
|
|
36
44
|
|
|
37
45
|
def _get_product_features(workspace: Path) -> dict:
|
|
38
|
-
"""Returns {product_name: {feature_name: version_or_None}}.
|
|
46
|
+
"""Returns {product_name: {(namespace, feature_name): version_or_None}}.
|
|
47
|
+
|
|
48
|
+
Every environment list counts (features, features_dev, features_prod).
|
|
49
|
+
"""
|
|
39
50
|
products = {}
|
|
40
|
-
for
|
|
41
|
-
if not product_dir.is_dir() or product_dir.name.startswith("."):
|
|
42
|
-
continue
|
|
43
|
-
pyproject = product_dir / "pyproject.toml"
|
|
44
|
-
if not pyproject.exists():
|
|
45
|
-
continue
|
|
46
|
-
content = pyproject.read_text()
|
|
47
|
-
m = re.search(
|
|
48
|
-
r"\[project\.optional-dependencies\].*?features\s*=\s*\[(.*?)\]",
|
|
49
|
-
content,
|
|
50
|
-
re.DOTALL,
|
|
51
|
-
)
|
|
52
|
-
if not m:
|
|
53
|
-
continue
|
|
51
|
+
for product, entries in workspace_feature_entries(workspace).items():
|
|
54
52
|
features = {}
|
|
55
|
-
for
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if "@" in ref:
|
|
60
|
-
name, version = ref.split("@", 1)
|
|
61
|
-
features[name] = version
|
|
62
|
-
else:
|
|
63
|
-
features[ref] = None
|
|
64
|
-
if features:
|
|
65
|
-
products[product_dir.name] = features
|
|
53
|
+
for entry in entries:
|
|
54
|
+
ns, name, version = parse_feature_entry(entry)
|
|
55
|
+
features[(ns, name)] = version
|
|
56
|
+
products[product] = features
|
|
66
57
|
return products
|
|
67
58
|
|
|
68
59
|
|
|
@@ -90,8 +81,9 @@ def cache_outdated():
|
|
|
90
81
|
|
|
91
82
|
outdated = []
|
|
92
83
|
for product, features in sorted(products.items()):
|
|
93
|
-
for
|
|
94
|
-
|
|
84
|
+
for (ns, feat), current_ver in features.items():
|
|
85
|
+
name = f"{ns}/{feat}"
|
|
86
|
+
available = cache_versions.get((ns, feat))
|
|
95
87
|
if not available:
|
|
96
88
|
continue
|
|
97
89
|
if current_ver is None:
|
|
@@ -101,9 +93,11 @@ def cache_outdated():
|
|
|
101
93
|
)
|
|
102
94
|
continue
|
|
103
95
|
latest = _latest(available)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
96
|
+
latest_parsed = parse_version(latest)
|
|
97
|
+
current_parsed = parse_version(current_ver)
|
|
98
|
+
if latest_parsed is not None and current_parsed is not None:
|
|
99
|
+
is_old = latest_parsed > current_parsed
|
|
100
|
+
else:
|
|
107
101
|
is_old = latest != current_ver
|
|
108
102
|
if is_old:
|
|
109
103
|
outdated.append((product, name, current_ver, latest, available))
|
|
@@ -118,7 +112,7 @@ def cache_outdated():
|
|
|
118
112
|
f" {click.style(product, bold=True)} {name} "
|
|
119
113
|
f"{click.style(current, fg='red')} → {click.style(latest, fg='green')}"
|
|
120
114
|
)
|
|
121
|
-
others =
|
|
115
|
+
others = sort_versions(set(available) - {latest})
|
|
122
116
|
if others:
|
|
123
117
|
click.echo(f" also cached: {', '.join(others)}")
|
|
124
118
|
click.echo()
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from splent_cli.services import context, compose
|
|
2
|
+
import click
|
|
3
|
+
import os
|
|
4
|
+
import tomllib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from splent_cli.utils.cache_utils import (
|
|
8
|
+
cache_entries,
|
|
9
|
+
cache_entry_key,
|
|
10
|
+
product_ref_keys,
|
|
11
|
+
rmtree_force,
|
|
12
|
+
)
|
|
13
|
+
from splent_cli.utils.feature_utils import (
|
|
14
|
+
load_product_pyproject,
|
|
15
|
+
read_all_feature_entries,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_cache_entries(cache_root: Path) -> list:
|
|
20
|
+
return cache_entries(cache_root)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _scan_product_entries(workspace: Path) -> tuple[dict, list]:
|
|
24
|
+
"""Strict variant of ``workspace_feature_entries`` for a destructive command.
|
|
25
|
+
|
|
26
|
+
Returns ``({product: entries}, [(directory, error), ...])``. A non-hidden
|
|
27
|
+
directory holding a pyproject.toml that cannot be read or parsed is a
|
|
28
|
+
failure, not a product to skip: it may pin entries that would otherwise
|
|
29
|
+
look orphaned. Directories without a pyproject.toml are ignored.
|
|
30
|
+
"""
|
|
31
|
+
products: dict[str, list[str]] = {}
|
|
32
|
+
failures: list[tuple[str, str]] = []
|
|
33
|
+
try:
|
|
34
|
+
children = sorted(os.scandir(workspace), key=lambda e: e.name)
|
|
35
|
+
except OSError as e:
|
|
36
|
+
return products, [(str(workspace), str(e))]
|
|
37
|
+
for child in children:
|
|
38
|
+
if not child.is_dir() or child.name.startswith("."):
|
|
39
|
+
continue
|
|
40
|
+
try:
|
|
41
|
+
data = load_product_pyproject(child.path)
|
|
42
|
+
except FileNotFoundError:
|
|
43
|
+
continue
|
|
44
|
+
except (OSError, tomllib.TOMLDecodeError) as e:
|
|
45
|
+
failures.append((child.name, str(e)))
|
|
46
|
+
continue
|
|
47
|
+
entries = read_all_feature_entries(data)
|
|
48
|
+
if entries:
|
|
49
|
+
products[child.name] = entries
|
|
50
|
+
return products, failures
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_all_product_refs(workspace: Path) -> set:
|
|
54
|
+
"""Every ``(namespace, name, version)`` any product declares.
|
|
55
|
+
|
|
56
|
+
Raises ``SystemExit(1)`` when a product's pyproject.toml cannot be read.
|
|
57
|
+
"""
|
|
58
|
+
products, failures = _scan_product_entries(workspace)
|
|
59
|
+
if failures:
|
|
60
|
+
click.secho(
|
|
61
|
+
"cache:prune stopped because these pyproject.toml files could not be read.",
|
|
62
|
+
fg="red",
|
|
63
|
+
err=True,
|
|
64
|
+
)
|
|
65
|
+
for directory, error in failures:
|
|
66
|
+
click.secho(f" - {directory}/pyproject.toml ({error})", fg="red", err=True)
|
|
67
|
+
click.secho(
|
|
68
|
+
" Every entry they pin would look orphaned. Fix them and run"
|
|
69
|
+
" cache:prune again. Nothing was deleted.",
|
|
70
|
+
fg="red",
|
|
71
|
+
err=True,
|
|
72
|
+
)
|
|
73
|
+
raise SystemExit(1)
|
|
74
|
+
return product_ref_keys(products)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@click.command(
|
|
78
|
+
"cache:prune", short_help="Remove orphaned cache entries not used by any product."
|
|
79
|
+
)
|
|
80
|
+
@click.option("--yes", is_flag=True, help="Skip confirmation prompt.")
|
|
81
|
+
def cache_prune(yes):
|
|
82
|
+
"""
|
|
83
|
+
Removes cache entries that no product references in its pyproject.toml,
|
|
84
|
+
then cleans up broken symlinks in products.
|
|
85
|
+
|
|
86
|
+
Stops without deleting anything when a product's pyproject.toml cannot
|
|
87
|
+
be parsed.
|
|
88
|
+
"""
|
|
89
|
+
workspace = context.workspace()
|
|
90
|
+
cache_root = workspace / ".splent_cache" / "features"
|
|
91
|
+
|
|
92
|
+
entries = _get_cache_entries(cache_root)
|
|
93
|
+
if not entries:
|
|
94
|
+
click.secho("ℹ️ Feature cache is empty.", fg="yellow")
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
refs = _get_all_product_refs(workspace)
|
|
98
|
+
|
|
99
|
+
orphans = [e for e in entries if cache_entry_key(e) not in refs]
|
|
100
|
+
|
|
101
|
+
if not orphans:
|
|
102
|
+
click.secho("✅ Nothing to prune — no orphaned entries.", fg="green")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
click.secho(f"Orphaned entries to remove ({len(orphans)}):", fg="yellow")
|
|
106
|
+
for e in orphans:
|
|
107
|
+
click.echo(f" - {_label(e)}")
|
|
108
|
+
|
|
109
|
+
click.echo()
|
|
110
|
+
if not yes and not click.confirm("Remove all of the above?"):
|
|
111
|
+
click.echo("❎ Cancelled.")
|
|
112
|
+
raise SystemExit(0)
|
|
113
|
+
|
|
114
|
+
failures = []
|
|
115
|
+
for e in orphans:
|
|
116
|
+
try:
|
|
117
|
+
rmtree_force(e["path"])
|
|
118
|
+
except OSError as exc:
|
|
119
|
+
failures.append((e, exc))
|
|
120
|
+
click.secho(f" Could not delete {_label(e)}: {exc}", fg="red", err=True)
|
|
121
|
+
|
|
122
|
+
pruned = len(orphans) - len(failures)
|
|
123
|
+
if pruned:
|
|
124
|
+
click.secho(f"🧹 Pruned {pruned} orphaned cache entry/entries.", fg="green")
|
|
125
|
+
|
|
126
|
+
removed = compose.remove_broken_symlinks(workspace)
|
|
127
|
+
if removed:
|
|
128
|
+
click.secho(f"🔗 Removed {removed} broken feature symlink(s).", fg="yellow")
|
|
129
|
+
else:
|
|
130
|
+
click.secho("✅ No broken symlinks found.", fg="green")
|
|
131
|
+
|
|
132
|
+
if failures:
|
|
133
|
+
click.secho(
|
|
134
|
+
f"{len(failures)} cache entry/entries could not be deleted. Check the"
|
|
135
|
+
" filesystem permissions (files written by a container may belong"
|
|
136
|
+
" to root) and remove them by hand.",
|
|
137
|
+
fg="red",
|
|
138
|
+
err=True,
|
|
139
|
+
)
|
|
140
|
+
raise SystemExit(1)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _label(e: dict) -> str:
|
|
144
|
+
version = f"@{e['version']}" if e["is_versioned"] else " (editable)"
|
|
145
|
+
return f"{e['namespace']}/{e['name']}{version}"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
cli_command = cache_prune
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
from splent_cli.services import context
|
|
2
|
+
from splent_cli.commands.cache.cache_versions import sort_versions
|
|
3
|
+
from splent_cli.utils.feature_utils import editable_feature_dirs
|
|
2
4
|
import click
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from collections import defaultdict
|
|
@@ -27,24 +29,8 @@ def _get_cache_grouped(cache_root: Path) -> dict:
|
|
|
27
29
|
def _get_workspace_root_features(workspace: Path) -> dict:
|
|
28
30
|
"""Returns {namespace/name: ['workspace']} for editable features at workspace root."""
|
|
29
31
|
grouped = defaultdict(list)
|
|
30
|
-
for
|
|
31
|
-
|
|
32
|
-
continue
|
|
33
|
-
# Must have pyproject.toml to be a real feature
|
|
34
|
-
if not (entry / "pyproject.toml").exists():
|
|
35
|
-
continue
|
|
36
|
-
src = entry / "src"
|
|
37
|
-
if not src.is_dir():
|
|
38
|
-
continue
|
|
39
|
-
for ns_dir in src.iterdir():
|
|
40
|
-
if (
|
|
41
|
-
ns_dir.is_dir()
|
|
42
|
-
and not ns_dir.name.startswith(("_", "."))
|
|
43
|
-
and "." not in ns_dir.name
|
|
44
|
-
and (ns_dir / entry.name).is_dir()
|
|
45
|
-
):
|
|
46
|
-
grouped[f"{ns_dir.name}/{entry.name}"].append("workspace")
|
|
47
|
-
break
|
|
32
|
+
for ns, name, _path in editable_feature_dirs(workspace):
|
|
33
|
+
grouped[f"{ns}/{name}"].append("workspace")
|
|
48
34
|
return grouped
|
|
49
35
|
|
|
50
36
|
|
|
@@ -73,7 +59,7 @@ def cache_status():
|
|
|
73
59
|
|
|
74
60
|
for feature, versions in sorted(grouped.items()):
|
|
75
61
|
click.secho(f" {feature}", bold=True)
|
|
76
|
-
sorted_versions =
|
|
62
|
+
sorted_versions = sort_versions(versions)
|
|
77
63
|
for i, v in enumerate(sorted_versions):
|
|
78
64
|
connector = "└──" if i == len(sorted_versions) - 1 else "├──"
|
|
79
65
|
if v == "workspace":
|