splent-cli 1.11.0__tar.gz → 1.13.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.11.0/src/splent_cli.egg-info → splent_cli-1.13.0}/PKG-INFO +1 -1
- {splent_cli-1.11.0 → splent_cli-1.13.0}/pyproject.toml +1 -1
- splent_cli-1.13.0/src/splent_cli/commands/database/db_reset.py +267 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_unlock.py +10 -2
- splent_cli-1.13.0/src/splent_cli/commands/locust.py +140 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_add_feature.py +33 -36
- splent_cli-1.13.0/src/splent_cli/commands/spl/spl_compare.py +88 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_create.py +4 -1
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/dynamic_imports.py +10 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli.egg-info/SOURCES.txt +1 -0
- splent_cli-1.11.0/src/splent_cli/commands/database/db_reset.py +0 -159
- splent_cli-1.11.0/src/splent_cli/commands/locust.py +0 -271
- {splent_cli-1.11.0 → splent_cli-1.13.0}/LICENSE +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/README.md +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/setup.cfg +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/cli.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_deps.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_env.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_features.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_infra.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_product.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/clear/__init__.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/command/__init__.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/command/command_create.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/coverage.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_console.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_dump.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_restore.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_seed.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_status.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/doctor.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/env/env_set.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/export/__init__.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/export/export_puml.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_release.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_test.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/lint.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_build.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_clean.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_commands.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_config.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_configure.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_console.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_containers.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_create.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_derive.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_down.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_drift.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_env.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_missing.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_port.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_release.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_restart.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_routes.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_run.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_select.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_signals.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_test.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_up.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_validate.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/release/release_core.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/selenium.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/__init__.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/tokens_setup.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/version.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/services/__init__.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/services/compose.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/services/context.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/services/preflight.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/services/release.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/__init__.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/cache_utils.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/command_loader.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/contract_freshness.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/feature_installer.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/feature_utils.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/git_url.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/integrity.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/io_utils.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/lifecycle.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/manifest.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/proc.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/template_drift.py +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli.egg-info/requires.txt +0 -0
- {splent_cli-1.11.0 → splent_cli-1.13.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.13.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
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import importlib
|
|
3
|
+
import os
|
|
4
|
+
import tomllib
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from flask import current_app
|
|
8
|
+
from flask_migrate import upgrade as alembic_upgrade
|
|
9
|
+
from sqlalchemy import text, MetaData
|
|
10
|
+
|
|
11
|
+
from splent_cli.utils.decorators import requires_db
|
|
12
|
+
from splent_cli.services import context
|
|
13
|
+
from splent_cli.utils.lifecycle import advance_state, resolve_feature_key_from_entry
|
|
14
|
+
from splent_framework.db import db
|
|
15
|
+
from splent_framework.managers.migration_manager import (
|
|
16
|
+
MigrationManager,
|
|
17
|
+
SPLENT_MIGRATIONS_TABLE,
|
|
18
|
+
alembic_version_table,
|
|
19
|
+
)
|
|
20
|
+
from splent_framework.utils.feature_utils import get_features_from_pyproject
|
|
21
|
+
from splent_framework.utils.path_utils import PathUtils
|
|
22
|
+
from splent_cli.commands.clear.clear_uploads import clear_uploads
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# =====================================================================
|
|
26
|
+
# Helpers for the per-feature reset
|
|
27
|
+
# =====================================================================
|
|
28
|
+
def _normalize(feature_name: str) -> str:
|
|
29
|
+
"""Accept 'projects' or 'splent_feature_projects' -> full package name."""
|
|
30
|
+
short = feature_name.split("/")[-1].replace("splent_feature_", "")
|
|
31
|
+
return f"splent_feature_{short}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _tables_owned_by(feature_full: str) -> list[str]:
|
|
35
|
+
"""Tables whose model classes live in the given feature's package."""
|
|
36
|
+
import_name = f"splent_io.{feature_full}"
|
|
37
|
+
try:
|
|
38
|
+
importlib.import_module(f"{import_name}.models")
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
tables = []
|
|
42
|
+
for mapper in db.Model.registry.mappers:
|
|
43
|
+
cls = mapper.class_
|
|
44
|
+
if getattr(cls, "__module__", "").startswith(import_name):
|
|
45
|
+
tables.append(cls.__table__.name)
|
|
46
|
+
return sorted(set(tables))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _refiners_of(feature_full: str, product_path: str) -> list[str]:
|
|
50
|
+
"""Installed features whose [tool.splent.refinement].refines == feature."""
|
|
51
|
+
refiners = []
|
|
52
|
+
pattern = os.path.join(product_path, "features", "*", "*", "pyproject.toml")
|
|
53
|
+
for pp in glob.glob(pattern):
|
|
54
|
+
try:
|
|
55
|
+
with open(pp, "rb") as f:
|
|
56
|
+
data = tomllib.load(f)
|
|
57
|
+
except Exception:
|
|
58
|
+
continue
|
|
59
|
+
ref = data.get("tool", {}).get("splent", {}).get("refinement", {})
|
|
60
|
+
if ref.get("refines") == feature_full:
|
|
61
|
+
name = data.get("project", {}).get("name", "")
|
|
62
|
+
if name:
|
|
63
|
+
refiners.append(name)
|
|
64
|
+
return refiners
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _reapply(app, feats: list[str], entry_lookup: dict, product_path, product_name):
|
|
68
|
+
dirs = MigrationManager.get_all_feature_migration_dirs()
|
|
69
|
+
for feat in feats:
|
|
70
|
+
mdir = dirs.get(feat)
|
|
71
|
+
if not mdir:
|
|
72
|
+
continue
|
|
73
|
+
try:
|
|
74
|
+
alembic_upgrade(directory=mdir)
|
|
75
|
+
revision = MigrationManager.get_current_feature_revision(feat, db.engine)
|
|
76
|
+
MigrationManager.update_feature_status(app, feat, revision)
|
|
77
|
+
click.echo(click.style(f" ✅ {feat} → {revision or 'head'}", fg="green"))
|
|
78
|
+
info = entry_lookup.get(feat)
|
|
79
|
+
if info:
|
|
80
|
+
key, ns, name, version = info
|
|
81
|
+
advance_state(
|
|
82
|
+
product_path,
|
|
83
|
+
product_name,
|
|
84
|
+
key,
|
|
85
|
+
to="migrated",
|
|
86
|
+
namespace=ns,
|
|
87
|
+
name=name,
|
|
88
|
+
version=version,
|
|
89
|
+
)
|
|
90
|
+
except ImportError as e:
|
|
91
|
+
if "models" in str(e):
|
|
92
|
+
continue
|
|
93
|
+
click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
|
|
94
|
+
except Exception as e:
|
|
95
|
+
click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _entry_lookup() -> dict:
|
|
99
|
+
lookup = {}
|
|
100
|
+
for entry in get_features_from_pyproject() or []:
|
|
101
|
+
key, ns, name, version = resolve_feature_key_from_entry(entry)
|
|
102
|
+
lookup[name] = (key, ns, name, version)
|
|
103
|
+
return lookup
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# =====================================================================
|
|
107
|
+
# Command
|
|
108
|
+
# =====================================================================
|
|
109
|
+
@requires_db
|
|
110
|
+
@click.command(
|
|
111
|
+
"db:reset",
|
|
112
|
+
short_help="Drop all tables (or just one feature's) and re-apply migrations.",
|
|
113
|
+
)
|
|
114
|
+
@click.argument("feature_name", required=False)
|
|
115
|
+
@click.option("-y", "--yes", is_flag=True, help="Confirm without prompting.")
|
|
116
|
+
@context.requires_product
|
|
117
|
+
def db_reset(feature_name, yes):
|
|
118
|
+
"""Reset the database.
|
|
119
|
+
|
|
120
|
+
With no argument: drops ALL tables and re-applies every feature migration.
|
|
121
|
+
|
|
122
|
+
With FEATURE_NAME (e.g. ``db:reset projects``): drops only the tables that
|
|
123
|
+
feature owns, clears their migration tracking, and re-applies that
|
|
124
|
+
feature's migrations plus any feature that refines it — leaving every other
|
|
125
|
+
feature's data untouched. Useful after changing a model/refinement.
|
|
126
|
+
"""
|
|
127
|
+
app = current_app
|
|
128
|
+
product_path = PathUtils.get_app_base_dir()
|
|
129
|
+
product_name = os.getenv("SPLENT_APP", "")
|
|
130
|
+
|
|
131
|
+
if feature_name:
|
|
132
|
+
_reset_one_feature(app, feature_name, yes, product_path, product_name)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
_reset_everything(app, yes, product_path, product_name)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _reset_one_feature(app, feature_name, yes, product_path, product_name):
|
|
139
|
+
feature_full = _normalize(feature_name)
|
|
140
|
+
tables = _tables_owned_by(feature_full)
|
|
141
|
+
refiners = _refiners_of(feature_full, product_path)
|
|
142
|
+
feats = [feature_full] + refiners
|
|
143
|
+
|
|
144
|
+
if not tables:
|
|
145
|
+
click.secho(
|
|
146
|
+
f"⚠️ '{feature_full}' owns no tables (nothing to drop). "
|
|
147
|
+
f"If it is a refinement, reset the base feature instead.",
|
|
148
|
+
fg="yellow",
|
|
149
|
+
)
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
click.echo()
|
|
153
|
+
click.secho(f"Reset feature: {feature_full}", fg="cyan", bold=True)
|
|
154
|
+
click.echo(f" tables to drop : {', '.join(tables)}")
|
|
155
|
+
if refiners:
|
|
156
|
+
click.echo(f" also re-applies: {', '.join(refiners)} (refiners)")
|
|
157
|
+
if not yes and not click.confirm(
|
|
158
|
+
"⚠️ This drops those tables and re-applies their migrations. Continue?",
|
|
159
|
+
abort=True,
|
|
160
|
+
):
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
# Drop the feature's tables
|
|
164
|
+
with db.engine.begin() as conn:
|
|
165
|
+
conn.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
|
|
166
|
+
for t in tables:
|
|
167
|
+
conn.execute(text(f"DROP TABLE IF EXISTS `{t}`"))
|
|
168
|
+
click.echo(click.style(f" Dropped {t}", fg="bright_black"))
|
|
169
|
+
conn.execute(text("SET FOREIGN_KEY_CHECKS = 1"))
|
|
170
|
+
|
|
171
|
+
# Clear migration tracking for the feature and its refiners — both the
|
|
172
|
+
# central splent_migrations row AND each feature's alembic version table —
|
|
173
|
+
# so their migrations are re-applied from scratch onto the fresh tables.
|
|
174
|
+
with db.engine.begin() as conn:
|
|
175
|
+
conn.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
|
|
176
|
+
for feat in feats:
|
|
177
|
+
conn.execute(
|
|
178
|
+
text(f"DELETE FROM `{SPLENT_MIGRATIONS_TABLE}` WHERE feature = :f"),
|
|
179
|
+
{"f": feat},
|
|
180
|
+
)
|
|
181
|
+
conn.execute(text(f"DROP TABLE IF EXISTS `{alembic_version_table(feat)}`"))
|
|
182
|
+
conn.execute(text("SET FOREIGN_KEY_CHECKS = 1"))
|
|
183
|
+
|
|
184
|
+
click.secho("⬆️ Re-applying migrations...", fg="cyan")
|
|
185
|
+
_reapply(app, feats, _entry_lookup(), product_path, product_name)
|
|
186
|
+
click.secho(f"\n🎉 Feature '{feature_full}' reset complete.", fg="green")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _reset_everything(app, yes, product_path, product_name):
|
|
190
|
+
if not yes and not click.confirm(
|
|
191
|
+
"⚠️ WARNING: This will DROP all tables and clear uploads. Are you sure?",
|
|
192
|
+
abort=True,
|
|
193
|
+
):
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
click.echo(click.style("🗑️ Dropping all tables...", fg="yellow"))
|
|
197
|
+
try:
|
|
198
|
+
with db.engine.begin() as conn:
|
|
199
|
+
conn.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
|
|
200
|
+
meta = MetaData()
|
|
201
|
+
meta.reflect(bind=db.engine)
|
|
202
|
+
for table in meta.sorted_tables:
|
|
203
|
+
conn.execute(text(f"DROP TABLE IF EXISTS `{table.name}`"))
|
|
204
|
+
click.echo(click.style(f" Dropped {table.name}", fg="bright_black"))
|
|
205
|
+
conn.execute(text("SET FOREIGN_KEY_CHECKS = 1"))
|
|
206
|
+
click.echo(click.style("✅ All tables dropped.", fg="yellow"))
|
|
207
|
+
except Exception as e:
|
|
208
|
+
click.echo(click.style(f"❌ Error dropping tables: {e}", fg="red"))
|
|
209
|
+
click.echo(
|
|
210
|
+
click.style(
|
|
211
|
+
"⚠️ The database may now be in a PARTIAL state: some tables "
|
|
212
|
+
"may have been dropped while others remain.\n"
|
|
213
|
+
" To recover, re-run this command once the database is "
|
|
214
|
+
"reachable again (it is safe to repeat — drops use "
|
|
215
|
+
"DROP TABLE IF EXISTS), or manually drop the remaining "
|
|
216
|
+
"tables / re-create the schema from migrations.",
|
|
217
|
+
fg="red",
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
raise SystemExit(1)
|
|
221
|
+
|
|
222
|
+
click.echo(click.style("📋 Recreating splent_migrations table...", fg="cyan"))
|
|
223
|
+
try:
|
|
224
|
+
with db.engine.begin() as conn:
|
|
225
|
+
conn.execute(
|
|
226
|
+
text(
|
|
227
|
+
f"""
|
|
228
|
+
CREATE TABLE IF NOT EXISTS `{SPLENT_MIGRATIONS_TABLE}` (
|
|
229
|
+
`feature` VARCHAR(255) NOT NULL,
|
|
230
|
+
`last_migration` VARCHAR(255) DEFAULT NULL,
|
|
231
|
+
PRIMARY KEY (`feature`)
|
|
232
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
233
|
+
"""
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
except Exception as exc:
|
|
237
|
+
# The drop above succeeded, so the schema is empty but the migration
|
|
238
|
+
# tracking table is gone: surface the partial state instead of a raw
|
|
239
|
+
# traceback, and tell the operator how to converge.
|
|
240
|
+
click.echo(
|
|
241
|
+
click.style(
|
|
242
|
+
f"Failed to recreate the migration tracking table `{SPLENT_MIGRATIONS_TABLE}`: {exc}\n"
|
|
243
|
+
"The database is empty but untracked. Re-run 'splent db:reset' once the "
|
|
244
|
+
"database connection is back.",
|
|
245
|
+
fg="red",
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
raise SystemExit(1)
|
|
249
|
+
|
|
250
|
+
ctx = click.get_current_context()
|
|
251
|
+
ctx.invoke(clear_uploads)
|
|
252
|
+
|
|
253
|
+
dirs = MigrationManager.get_all_feature_migration_dirs()
|
|
254
|
+
if not dirs:
|
|
255
|
+
click.echo(click.style("⚠️ No feature migrations found.", fg="yellow"))
|
|
256
|
+
else:
|
|
257
|
+
click.echo(
|
|
258
|
+
click.style(
|
|
259
|
+
f"⬆️ Applying migrations for {len(dirs)} features...", fg="cyan"
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
_reapply(app, list(dirs.keys()), _entry_lookup(), product_path, product_name)
|
|
263
|
+
|
|
264
|
+
click.echo(click.style("\n🎉 Database reset complete.", fg="green"))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
cli_command = db_reset
|
|
@@ -4,7 +4,6 @@ import tomllib
|
|
|
4
4
|
import click
|
|
5
5
|
import requests
|
|
6
6
|
from splent_cli.services import context, compose
|
|
7
|
-
from splent_cli.utils.feature_utils import read_features_from_data
|
|
8
7
|
from splent_cli.utils.cache_utils import make_feature_writable
|
|
9
8
|
from splent_cli.utils.proc import run
|
|
10
9
|
from splent_cli.utils.io_utils import load_toml, atomic_write
|
|
@@ -343,7 +342,16 @@ def feature_edit(feature_name, edit_all, force):
|
|
|
343
342
|
with open(pyproject_path, "rb") as f:
|
|
344
343
|
data = tomllib.load(f)
|
|
345
344
|
|
|
346
|
-
features
|
|
345
|
+
# Look across all feature lists (features, features_dev, features_prod) so
|
|
346
|
+
# that dev/prod-only features (e.g. admin) can also be unlocked, not just
|
|
347
|
+
# the default `features` list.
|
|
348
|
+
splent_cfg = data.get("tool", {}).get("splent", {})
|
|
349
|
+
features = [
|
|
350
|
+
entry
|
|
351
|
+
for key in ("features", "features_dev", "features_prod")
|
|
352
|
+
for entry in (splent_cfg.get(key) or [])
|
|
353
|
+
if isinstance(entry, str)
|
|
354
|
+
]
|
|
347
355
|
|
|
348
356
|
# ── Single feature ────────────────────────────────────────────────
|
|
349
357
|
if feature_name:
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""splent locust — load testing for the active product.
|
|
2
|
+
|
|
3
|
+
Locust runs inside the product's web container, which already has
|
|
4
|
+
splent_framework and every installed feature importable, so no extra image is
|
|
5
|
+
built. Discovery is done by splent_framework's locustfile bootstrap (1.7.1+),
|
|
6
|
+
which finds each feature's locustfile on its own; a feature argument narrows it
|
|
7
|
+
through SPLENT_LOCUSTFILES.
|
|
8
|
+
|
|
9
|
+
The web interface listens on port 8089 inside the container. Products
|
|
10
|
+
scaffolded by this CLI publish that port; for an older product add
|
|
11
|
+
"8089:8089" to the web service's ports and recreate the container.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import shlex
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
from splent_cli.services import context
|
|
20
|
+
from splent_cli.utils.proc import require_docker, run
|
|
21
|
+
|
|
22
|
+
# Feature locustfile locations, relative to the workspace. Kept in sync with
|
|
23
|
+
# splent_framework.bootstraps.locustfile_bootstrap.DEFAULT_PATTERNS.
|
|
24
|
+
FEATURE_LOCUSTFILE_PATTERNS = (
|
|
25
|
+
"features/*/{feature}/tests/load/locustfile.py",
|
|
26
|
+
"features/*/{feature}/tests/locustfile.py",
|
|
27
|
+
"features/*/{feature}/src/*/*/tests/load/locustfile.py",
|
|
28
|
+
"app/features/{feature}/tests/locustfile.py",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Resolved inside the container: the framework bootstrap that discovers and
|
|
32
|
+
# re-exports every feature's HttpUser classes.
|
|
33
|
+
BOOTSTRAP_SNIPPET = (
|
|
34
|
+
"locust -f \"$(python -c 'import splent_framework.bootstraps.locustfile_bootstrap as m; "
|
|
35
|
+
"print(m.__file__)')\" --web-host 0.0.0.0"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _web_container() -> str:
|
|
40
|
+
return f"{context.require_app()}_web"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _container_running(name: str) -> bool:
|
|
44
|
+
result = run(
|
|
45
|
+
["docker", "ps", "-q", "-f", f"name=^{name}$"], check=False, capture=True
|
|
46
|
+
)
|
|
47
|
+
return result.returncode == 0 and bool(result.stdout.strip())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _locust_installed(name: str) -> bool:
|
|
51
|
+
result = run(
|
|
52
|
+
["docker", "exec", name, "python", "-c", "import locust"],
|
|
53
|
+
check=False,
|
|
54
|
+
capture=True,
|
|
55
|
+
)
|
|
56
|
+
return result.returncode == 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _feature_patterns(feature: str) -> str:
|
|
60
|
+
return ",".join(p.format(feature=feature) for p in FEATURE_LOCUSTFILE_PATTERNS)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _feature_has_locustfile(workspace: str, feature: str) -> bool:
|
|
64
|
+
import glob
|
|
65
|
+
|
|
66
|
+
for pattern in FEATURE_LOCUSTFILE_PATTERNS:
|
|
67
|
+
if glob.glob(os.path.join(workspace, pattern.format(feature=feature))):
|
|
68
|
+
return True
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@click.command(
|
|
73
|
+
"locust", short_help="Run Locust load tests inside the product web container."
|
|
74
|
+
)
|
|
75
|
+
@click.argument("feature", required=False)
|
|
76
|
+
@context.requires_product
|
|
77
|
+
def locust(feature):
|
|
78
|
+
require_docker()
|
|
79
|
+
container = _web_container()
|
|
80
|
+
|
|
81
|
+
if not _container_running(container):
|
|
82
|
+
raise click.ClickException(
|
|
83
|
+
f"Container '{container}' is not running. Start the product first: splent product:up"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if not _locust_installed(container):
|
|
87
|
+
raise click.ClickException(
|
|
88
|
+
f"locust is not installed in '{container}'.\n"
|
|
89
|
+
'Add "locust" to [project.optional-dependencies].dev in the product\'s '
|
|
90
|
+
"pyproject.toml and rebuild (splent product:derive), or install it once "
|
|
91
|
+
f"with: docker exec {container} pip install locust"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
workspace = str(context.workspace())
|
|
95
|
+
env_args = ["-e", "WORKING_DIR=/workspace"]
|
|
96
|
+
if feature:
|
|
97
|
+
if not _feature_has_locustfile(workspace, feature):
|
|
98
|
+
raise click.ClickException(
|
|
99
|
+
f"No locustfile found for feature '{feature}'. Looked under: "
|
|
100
|
+
+ ", ".join(FEATURE_LOCUSTFILE_PATTERNS)
|
|
101
|
+
)
|
|
102
|
+
env_args += ["-e", f"SPLENT_LOCUSTFILES={_feature_patterns(feature)}"]
|
|
103
|
+
|
|
104
|
+
check = run(
|
|
105
|
+
["docker", "exec", container, "sh", "-c", "pgrep -f 'locust' >/dev/null"],
|
|
106
|
+
check=False,
|
|
107
|
+
capture=True,
|
|
108
|
+
)
|
|
109
|
+
if check.returncode == 0:
|
|
110
|
+
click.echo("Locust is already running in the product container.")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
cmd = ["docker", "exec", "-d", *env_args, container, "sh", "-c", BOOTSTRAP_SNIPPET]
|
|
114
|
+
click.echo(f"Command: {' '.join(shlex.quote(c) for c in cmd)}")
|
|
115
|
+
run(cmd)
|
|
116
|
+
click.echo(click.style("Locust is running at http://localhost:8089", fg="green"))
|
|
117
|
+
click.echo("Stop it with: splent locust:stop")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@click.command(
|
|
121
|
+
"locust:stop", short_help="Stop the Locust process in the product web container."
|
|
122
|
+
)
|
|
123
|
+
@context.requires_product
|
|
124
|
+
def locust_stop():
|
|
125
|
+
require_docker()
|
|
126
|
+
container = _web_container()
|
|
127
|
+
|
|
128
|
+
if not _container_running(container):
|
|
129
|
+
click.echo(f"Container '{container}' is not running; nothing to stop.")
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
result = run(
|
|
133
|
+
["docker", "exec", container, "sh", "-c", "pkill -f 'locust' || true"],
|
|
134
|
+
check=False,
|
|
135
|
+
capture=True,
|
|
136
|
+
)
|
|
137
|
+
if result.returncode == 0:
|
|
138
|
+
click.echo("Locust stopped.")
|
|
139
|
+
else:
|
|
140
|
+
click.echo("No Locust process was running.")
|
|
@@ -219,45 +219,42 @@ def spl_add_feature(spl_name, feature_package, org):
|
|
|
219
219
|
with open(uvl_path, "r", encoding="utf-8") as f:
|
|
220
220
|
lines = f.read().splitlines()
|
|
221
221
|
|
|
222
|
-
#
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if line.strip() == "constraints"
|
|
227
|
-
constraints_idx = i
|
|
228
|
-
insert_idx = i
|
|
229
|
-
break
|
|
230
|
-
|
|
231
|
-
if insert_idx is None:
|
|
232
|
-
insert_idx = len(lines)
|
|
233
|
-
|
|
234
|
-
# The line-insertion below assumes "constraints" lives on its own line.
|
|
235
|
-
# If it doesn't (e.g. malformed or unexpected layout), bail out before
|
|
236
|
-
# touching the file rather than silently corrupting the model.
|
|
237
|
-
if detected_deps and constraints_idx is None:
|
|
238
|
-
click.secho(
|
|
239
|
-
f" ❌ No 'constraints' section found in {uvl_path}.\n"
|
|
240
|
-
f" Refusing to edit: the UVL layout is not what this command "
|
|
241
|
-
f"expects (a 'constraints' header on its own line). "
|
|
242
|
-
f"Add the feature and constraints manually.",
|
|
243
|
-
fg="red",
|
|
244
|
-
)
|
|
245
|
-
raise SystemExit(1)
|
|
246
|
-
|
|
247
|
-
# Insert feature declaration
|
|
248
|
-
feature_line = (
|
|
249
|
-
f"\t\toptional\n\t\t\t{short_name} {{org '{org}', package '{feature_package}'}}"
|
|
222
|
+
# Locate the (optional) "constraints" header and an existing "optional" group.
|
|
223
|
+
# A fresh SPL has neither. UVL rejects an EMPTY "constraints" section, so we
|
|
224
|
+
# only create that section when there is at least one constraint to add.
|
|
225
|
+
constraints_idx = next(
|
|
226
|
+
(i for i, line in enumerate(lines) if line.strip() == "constraints"), None
|
|
250
227
|
)
|
|
251
|
-
|
|
228
|
+
optional_idx = next(
|
|
229
|
+
(i for i, line in enumerate(lines) if line.strip() == "optional"), None
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Insert the feature under a SINGLE shared "optional" group, reusing it if it
|
|
233
|
+
# already exists (so repeated calls don't emit multiple 'optional' headers,
|
|
234
|
+
# which produced invalid UVL). The feature goes just before "constraints" if
|
|
235
|
+
# present, otherwise at the end of the features block. Indentation is tabs
|
|
236
|
+
# throughout, consistent with spl:create.
|
|
237
|
+
feat_at = constraints_idx if constraints_idx is not None else len(lines)
|
|
238
|
+
feature_decl = f"\t\t\t{short_name} {{org '{org}', package '{feature_package}'}}"
|
|
239
|
+
if optional_idx is not None:
|
|
240
|
+
lines.insert(feat_at, feature_decl)
|
|
241
|
+
if constraints_idx is not None:
|
|
242
|
+
constraints_idx += 1
|
|
243
|
+
else:
|
|
244
|
+
lines.insert(feat_at, "\t\toptional")
|
|
245
|
+
lines.insert(feat_at + 1, feature_decl)
|
|
246
|
+
if constraints_idx is not None:
|
|
247
|
+
constraints_idx += 2
|
|
252
248
|
|
|
253
|
-
#
|
|
254
|
-
if detected_deps
|
|
255
|
-
|
|
249
|
+
# Add constraints, creating the section on demand (never leave it empty).
|
|
250
|
+
if detected_deps:
|
|
251
|
+
if constraints_idx is None:
|
|
252
|
+
lines.append("constraints")
|
|
253
|
+
constraints_idx = len(lines) - 1
|
|
254
|
+
ci = constraints_idx
|
|
256
255
|
for dep in sorted(detected_deps):
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
)
|
|
260
|
-
lines.insert(constraints_idx, f"\t{short_name} => {dep}")
|
|
256
|
+
ci += 1
|
|
257
|
+
lines.insert(ci, f"\t{short_name} => {dep}")
|
|
261
258
|
|
|
262
259
|
# Back up the original UVL, then write atomically so a crash mid-write can
|
|
263
260
|
# never leave a half-written / truncated model.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from splent_cli.services import context
|
|
4
|
+
from splent_cli.utils.feature_utils import (
|
|
5
|
+
load_product_features,
|
|
6
|
+
parse_feature_entry,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _load_feature_map(product_name: str) -> dict[tuple[str, str], str]:
|
|
11
|
+
"""Return {(org, package): "org/package"} for a product in the workspace.
|
|
12
|
+
|
|
13
|
+
Features are keyed by (organisation, package) so that two features only
|
|
14
|
+
match when BOTH the org/user and the package coincide. The version is
|
|
15
|
+
ignored on purpose — a product on v1.2.0 and another on v1.3.0 of the same
|
|
16
|
+
feature still share that feature.
|
|
17
|
+
"""
|
|
18
|
+
product_dir = context.workspace() / product_name
|
|
19
|
+
if not (product_dir / "pyproject.toml").is_file():
|
|
20
|
+
raise click.ClickException(
|
|
21
|
+
f"Product not found: '{product_name}' (no pyproject.toml in {product_dir})"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
feature_map: dict[tuple[str, str], str] = {}
|
|
25
|
+
for entry in load_product_features(str(product_dir)):
|
|
26
|
+
org, package, _version = parse_feature_entry(entry)
|
|
27
|
+
feature_map[(org, package)] = f"{org}/{package}"
|
|
28
|
+
return feature_map
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _print_block(title: str, color: str, keys: list[tuple[str, str]]) -> None:
|
|
32
|
+
click.secho(f" {title} ({len(keys)})", fg=color, bold=True)
|
|
33
|
+
if not keys:
|
|
34
|
+
click.secho(" —", fg="bright_black")
|
|
35
|
+
for org, package in keys:
|
|
36
|
+
click.echo(f" {org}/{package}")
|
|
37
|
+
click.echo()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@click.command(
|
|
41
|
+
"spl:compare",
|
|
42
|
+
short_help="Compare the features of two products (shared vs product-specific)",
|
|
43
|
+
)
|
|
44
|
+
@click.argument("product_a")
|
|
45
|
+
@click.argument("product_b")
|
|
46
|
+
@context.requires_detached
|
|
47
|
+
def spl_compare(product_a, product_b):
|
|
48
|
+
"""Compare two products in the workspace by their feature sets.
|
|
49
|
+
|
|
50
|
+
Reports the features they SHARE and the features SPECIFIC to each one.
|
|
51
|
+
Matching is done by organisation/package — two features are "the same"
|
|
52
|
+
only when both the org (or user) and the package name coincide.
|
|
53
|
+
|
|
54
|
+
\b
|
|
55
|
+
Example:
|
|
56
|
+
splent spl:compare innosoft_app diversolab_app
|
|
57
|
+
"""
|
|
58
|
+
a = _load_feature_map(product_a)
|
|
59
|
+
b = _load_feature_map(product_b)
|
|
60
|
+
|
|
61
|
+
a_keys, b_keys = set(a), set(b)
|
|
62
|
+
shared = sorted(a_keys & b_keys)
|
|
63
|
+
only_a = sorted(a_keys - b_keys)
|
|
64
|
+
only_b = sorted(b_keys - a_keys)
|
|
65
|
+
|
|
66
|
+
union = a_keys | b_keys
|
|
67
|
+
reuse_pct = (len(shared) / len(union) * 100) if union else 0.0
|
|
68
|
+
|
|
69
|
+
click.echo()
|
|
70
|
+
click.secho(f" {product_a} ↔ {product_b}", fg="cyan", bold=True)
|
|
71
|
+
click.echo(
|
|
72
|
+
f" {len(a_keys)} feature(s) in {product_a}"
|
|
73
|
+
f" · {len(b_keys)} feature(s) in {product_b}"
|
|
74
|
+
)
|
|
75
|
+
reuse_color = "green" if reuse_pct >= 50 else "yellow"
|
|
76
|
+
click.secho(
|
|
77
|
+
f" Reuse: {len(shared)}/{len(union)} shared ({reuse_pct:.0f}%)",
|
|
78
|
+
fg=reuse_color,
|
|
79
|
+
bold=True,
|
|
80
|
+
)
|
|
81
|
+
click.echo()
|
|
82
|
+
|
|
83
|
+
_print_block("Shared", "green", shared)
|
|
84
|
+
_print_block(f"Only in {product_a}", "yellow", only_a)
|
|
85
|
+
_print_block(f"Only in {product_b}", "magenta", only_b)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
cli_command = spl_compare
|
|
@@ -35,7 +35,10 @@ def spl_create(name):
|
|
|
35
35
|
|
|
36
36
|
# {name}.uvl
|
|
37
37
|
uvl_path = os.path.join(spl_dir, f"{name}.uvl")
|
|
38
|
-
|
|
38
|
+
# A fresh SPL has no features yet — emit a minimal VALID model (just the
|
|
39
|
+
# root, no empty 'mandatory'/'optional' group, which UVL rejects). Tabs are
|
|
40
|
+
# used for indentation, consistent with spl:add-feature.
|
|
41
|
+
uvl_content = f"features\n\t{name}\n"
|
|
39
42
|
with open(uvl_path, "w") as f:
|
|
40
43
|
f.write(uvl_content)
|
|
41
44
|
|
|
@@ -10,6 +10,10 @@ from splent_framework.utils.path_utils import PathUtils
|
|
|
10
10
|
from splent_cli.utils.io_utils import load_toml
|
|
11
11
|
from splent_cli.utils.proc import run
|
|
12
12
|
|
|
13
|
+
# What the operator actually exported, captured before any .env is applied.
|
|
14
|
+
# Precedence from here on: shell environment > product .env > workspace .env.
|
|
15
|
+
_shell_env = dict(os.environ)
|
|
16
|
+
|
|
13
17
|
load_dotenv()
|
|
14
18
|
|
|
15
19
|
_app_instance = None
|
|
@@ -21,7 +25,13 @@ dotenv_path = None
|
|
|
21
25
|
if module_name:
|
|
22
26
|
dotenv_path = PathUtils.get_app_env_file()
|
|
23
27
|
if os.path.exists(dotenv_path):
|
|
28
|
+
# The product's .env beats the workspace .env, hence override=True.
|
|
29
|
+
# But it must never beat the process environment: a product .env
|
|
30
|
+
# carries container paths (WORKING_DIR=/workspace/), and blindly
|
|
31
|
+
# overriding made the CLI unusable from the host, because whatever
|
|
32
|
+
# WORKING_DIR the operator exported was clobbered on import.
|
|
24
33
|
load_dotenv(dotenv_path, override=True)
|
|
34
|
+
os.environ.update(_shell_env)
|
|
25
35
|
|
|
26
36
|
|
|
27
37
|
def install_features_if_needed():
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: splent_cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.13.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
|
|
@@ -134,6 +134,7 @@ src/splent_cli/commands/release/release_core.py
|
|
|
134
134
|
src/splent_cli/commands/spl/__init__.py
|
|
135
135
|
src/splent_cli/commands/spl/spl_add_constraints.py
|
|
136
136
|
src/splent_cli/commands/spl/spl_add_feature.py
|
|
137
|
+
src/splent_cli/commands/spl/spl_compare.py
|
|
137
138
|
src/splent_cli/commands/spl/spl_configurations.py
|
|
138
139
|
src/splent_cli/commands/spl/spl_create.py
|
|
139
140
|
src/splent_cli/commands/spl/spl_deps.py
|