splent-cli 1.9.1__tar.gz → 1.10.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.9.1/src/splent_cli.egg-info → splent_cli-1.10.0}/PKG-INFO +2 -1
- {splent_cli-1.9.1 → splent_cli-1.10.0}/pyproject.toml +2 -1
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_features.py +9 -2
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_infra.py +55 -45
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/coverage.py +7 -4
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_console.py +28 -13
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_dump.py +25 -10
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_migrate.py +24 -4
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_reset.py +27 -4
- splent_cli-1.10.0/src/splent_cli/commands/database/db_restore.py +117 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_rollback.py +44 -3
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_seed.py +17 -15
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_upgrade.py +8 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/doctor.py +47 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_add.py +4 -7
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_attach.py +4 -9
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_clean.py +13 -1
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_clone.py +85 -33
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_compat.py +2 -3
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_compile.py +26 -9
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_contract.py +3 -4
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_create.py +6 -6
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_detach.py +31 -10
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_discard.py +13 -3
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_env.py +8 -2
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_fork.py +41 -6
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_git.py +2 -2
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_hook_add.py +17 -32
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_install.py +83 -47
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_order.py +2 -3
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_outdated.py +9 -11
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_pin.py +3 -5
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_pip_install.py +7 -7
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_pull.py +6 -2
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_refine.py +120 -11
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_release.py +36 -4
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_remove.py +3 -5
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_rename.py +81 -37
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_search.py +23 -1
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_status.py +32 -4
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_test.py +33 -1
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_translate.py +32 -12
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_unlock.py +82 -18
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_upgrade.py +17 -5
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_versions.py +19 -8
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_xray.py +4 -7
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/lint.py +16 -6
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/locust.py +89 -14
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_auto_require.py +24 -90
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_build.py +15 -11
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_clean.py +18 -9
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_config.py +4 -1
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_configure.py +41 -38
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_console.py +4 -1
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_create.py +39 -27
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_deploy.py +13 -2
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_derive.py +3 -3
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_deselect.py +2 -2
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_env.py +56 -16
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_list.py +6 -1
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_release.py +48 -35
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_resolve.py +66 -3
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_restart.py +55 -36
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_routes.py +4 -1
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_run.py +24 -8
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_select.py +9 -2
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_signals.py +4 -1
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_test.py +4 -3
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_up.py +75 -34
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/selenium.py +16 -12
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_add_constraints.py +36 -3
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_add_feature.py +51 -2
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/uvl/uvl_utils.py +9 -1
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/services/preflight.py +43 -27
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/services/release.py +44 -17
- splent_cli-1.10.0/src/splent_cli/utils/cache_utils.py +115 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/command_loader.py +6 -3
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/contract_freshness.py +10 -2
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/dynamic_imports.py +26 -12
- splent_cli-1.10.0/src/splent_cli/utils/feature_installer.py +86 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/feature_utils.py +66 -9
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/git_url.py +18 -3
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/integrity.py +23 -12
- splent_cli-1.10.0/src/splent_cli/utils/io_utils.py +87 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/manifest.py +12 -2
- splent_cli-1.10.0/src/splent_cli/utils/proc.py +109 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/template_drift.py +12 -5
- {splent_cli-1.9.1 → splent_cli-1.10.0/src/splent_cli.egg-info}/PKG-INFO +2 -1
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli.egg-info/SOURCES.txt +2 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli.egg-info/requires.txt +1 -0
- splent_cli-1.9.1/src/splent_cli/commands/database/db_restore.py +0 -77
- splent_cli-1.9.1/src/splent_cli/utils/cache_utils.py +0 -54
- splent_cli-1.9.1/src/splent_cli/utils/feature_installer.py +0 -54
- {splent_cli-1.9.1 → splent_cli-1.10.0}/LICENSE +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/README.md +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/setup.cfg +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/cli.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_deps.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_env.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_product.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/clear/__init__.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/command/__init__.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/command/command_create.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_status.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/env/env_set.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/export/__init__.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/export/export_puml.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_commands.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_containers.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_down.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_drift.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_missing.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_port.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_validate.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/release/release_core.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/__init__.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/tokens_setup.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/version.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/services/__init__.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/services/compose.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/services/context.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/__init__.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/lifecycle.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.9.1 → splent_cli-1.10.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.10.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
|
|
@@ -11,6 +11,7 @@ License-File: LICENSE
|
|
|
11
11
|
Requires-Dist: click==8.1.8
|
|
12
12
|
Requires-Dist: packaging>=24.0
|
|
13
13
|
Requires-Dist: requests>=2.31.0
|
|
14
|
+
Requires-Dist: tomli_w>=1.0.0
|
|
14
15
|
Provides-Extra: dev
|
|
15
16
|
Requires-Dist: setuptools==80.3.1; extra == "dev"
|
|
16
17
|
Requires-Dist: pytest==8.3.4; 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.10.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"
|
|
@@ -15,6 +15,7 @@ dependencies = [
|
|
|
15
15
|
"click==8.1.8",
|
|
16
16
|
"packaging>=24.0",
|
|
17
17
|
"requests>=2.31.0",
|
|
18
|
+
"tomli_w>=1.0.0",
|
|
18
19
|
]
|
|
19
20
|
|
|
20
21
|
[project.optional-dependencies]
|
|
@@ -54,8 +54,15 @@ def check_features():
|
|
|
54
54
|
_fail("pyproject.toml not found")
|
|
55
55
|
raise SystemExit(1)
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
try:
|
|
58
|
+
with open(pyproject_path, "rb") as f:
|
|
59
|
+
data = tomllib.load(f)
|
|
60
|
+
except tomllib.TOMLDecodeError as e:
|
|
61
|
+
_fail(f"pyproject.toml is not valid TOML: {e}")
|
|
62
|
+
raise SystemExit(1)
|
|
63
|
+
except OSError as e:
|
|
64
|
+
_fail(f"Could not read pyproject.toml: {e}")
|
|
65
|
+
raise SystemExit(1)
|
|
59
66
|
|
|
60
67
|
env = os.getenv("SPLENT_ENV")
|
|
61
68
|
features = read_features_from_data(data, env)
|
|
@@ -7,18 +7,43 @@ import subprocess
|
|
|
7
7
|
import json
|
|
8
8
|
|
|
9
9
|
import click
|
|
10
|
-
import tomllib
|
|
11
10
|
|
|
12
11
|
from splent_cli.services import context, compose
|
|
13
12
|
from splent_cli.utils.feature_utils import read_features_from_data
|
|
13
|
+
from splent_cli.utils.io_utils import load_toml
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _Result:
|
|
17
|
+
"""Minimal stand-in for CompletedProcess used by the guarded ``_run``."""
|
|
18
|
+
|
|
19
|
+
__slots__ = ("returncode", "stdout", "stderr")
|
|
20
|
+
|
|
21
|
+
def __init__(self, returncode: int, stdout: str = "", stderr: str = ""):
|
|
22
|
+
self.returncode = returncode
|
|
23
|
+
self.stdout = stdout
|
|
24
|
+
self.stderr = stderr
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _run(cmd: list) -> _Result:
|
|
28
|
+
"""Run a docker command, never crashing the diagnostic.
|
|
29
|
+
|
|
30
|
+
Missing tool (FileNotFoundError) and timeouts are mapped to a non-zero
|
|
31
|
+
returncode so callers treat them as a normal FAIL/skip, exactly like
|
|
32
|
+
check_docker._run.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
36
|
+
return _Result(r.returncode, r.stdout, r.stderr)
|
|
37
|
+
except FileNotFoundError:
|
|
38
|
+
return _Result(1, "", "command not found")
|
|
39
|
+
except subprocess.TimeoutExpired:
|
|
40
|
+
return _Result(1, "", "timed out")
|
|
14
41
|
|
|
15
42
|
|
|
16
43
|
def _parse_compose_ports(compose_file: str) -> list[tuple[int, str, str]]:
|
|
17
44
|
"""Return [(host_port, service_name, source_label)] from a compose file."""
|
|
18
|
-
result =
|
|
19
|
-
["docker", "compose", "-f", compose_file, "config", "--format", "json"]
|
|
20
|
-
capture_output=True,
|
|
21
|
-
text=True,
|
|
45
|
+
result = _run(
|
|
46
|
+
["docker", "compose", "-f", compose_file, "config", "--format", "json"]
|
|
22
47
|
)
|
|
23
48
|
if result.returncode != 0:
|
|
24
49
|
return []
|
|
@@ -41,10 +66,8 @@ def _parse_compose_ports(compose_file: str) -> list[tuple[int, str, str]]:
|
|
|
41
66
|
|
|
42
67
|
def _parse_compose_services(compose_file: str) -> list[tuple[str, str, str]]:
|
|
43
68
|
"""Return [(service_name, container_name_or_None, source_label)]."""
|
|
44
|
-
result =
|
|
45
|
-
["docker", "compose", "-f", compose_file, "config", "--format", "json"]
|
|
46
|
-
capture_output=True,
|
|
47
|
-
text=True,
|
|
69
|
+
result = _run(
|
|
70
|
+
["docker", "compose", "-f", compose_file, "config", "--format", "json"]
|
|
48
71
|
)
|
|
49
72
|
if result.returncode != 0:
|
|
50
73
|
return []
|
|
@@ -97,8 +120,7 @@ def check_infra():
|
|
|
97
120
|
_fail("pyproject.toml not found")
|
|
98
121
|
raise SystemExit(1)
|
|
99
122
|
|
|
100
|
-
|
|
101
|
-
data = tomllib.load(f)
|
|
123
|
+
data = load_toml(pyproject_path, what="pyproject.toml")
|
|
102
124
|
|
|
103
125
|
env = os.getenv("SPLENT_ENV", "dev")
|
|
104
126
|
features = read_features_from_data(data, env)
|
|
@@ -132,21 +154,19 @@ def check_infra():
|
|
|
132
154
|
else:
|
|
133
155
|
_ok(f"No port conflicts ({len(all_ports)} ports declared)")
|
|
134
156
|
|
|
135
|
-
# Check against running containers
|
|
157
|
+
# Check against running containers (query docker ps once, not per-port)
|
|
136
158
|
running_conflicts = []
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if f":{port}->" in ports_str:
|
|
149
|
-
running_conflicts.append((port, name))
|
|
159
|
+
ps_result = _run(["docker", "ps", "--format", "{{.ID}}\t{{.Names}}\t{{.Ports}}"])
|
|
160
|
+
if ps_result.returncode == 0:
|
|
161
|
+
ps_lines = ps_result.stdout.splitlines()
|
|
162
|
+
for port in all_ports:
|
|
163
|
+
for line in ps_lines:
|
|
164
|
+
parts = line.split("\t", 2)
|
|
165
|
+
if len(parts) < 3:
|
|
166
|
+
continue
|
|
167
|
+
cid, name, ports_str = parts
|
|
168
|
+
if f":{port}->" in ports_str:
|
|
169
|
+
running_conflicts.append((port, name))
|
|
150
170
|
|
|
151
171
|
if running_conflicts:
|
|
152
172
|
for port, cname in running_conflicts:
|
|
@@ -189,10 +209,8 @@ def check_infra():
|
|
|
189
209
|
click.echo(click.style(" Networks", bold=True))
|
|
190
210
|
required_networks: set[str] = set()
|
|
191
211
|
for label, cf in compose_files:
|
|
192
|
-
result =
|
|
193
|
-
["docker", "compose", "-f", cf, "config", "--format", "json"]
|
|
194
|
-
capture_output=True,
|
|
195
|
-
text=True,
|
|
212
|
+
result = _run(
|
|
213
|
+
["docker", "compose", "-f", cf, "config", "--format", "json"]
|
|
196
214
|
)
|
|
197
215
|
if result.returncode != 0:
|
|
198
216
|
continue
|
|
@@ -205,10 +223,8 @@ def check_infra():
|
|
|
205
223
|
required_networks.add(net_name)
|
|
206
224
|
|
|
207
225
|
if required_networks:
|
|
208
|
-
existing_networks =
|
|
209
|
-
["docker", "network", "ls", "--format", "{{.Name}}"]
|
|
210
|
-
capture_output=True,
|
|
211
|
-
text=True,
|
|
226
|
+
existing_networks = _run(
|
|
227
|
+
["docker", "network", "ls", "--format", "{{.Name}}"]
|
|
212
228
|
).stdout.splitlines()
|
|
213
229
|
for net in sorted(required_networks):
|
|
214
230
|
if net in existing_networks:
|
|
@@ -226,10 +242,8 @@ def check_infra():
|
|
|
226
242
|
build_count = 0
|
|
227
243
|
for label, cf in compose_files:
|
|
228
244
|
docker_dir = os.path.dirname(cf)
|
|
229
|
-
result =
|
|
230
|
-
["docker", "compose", "-f", cf, "config", "--format", "json"]
|
|
231
|
-
capture_output=True,
|
|
232
|
-
text=True,
|
|
245
|
+
result = _run(
|
|
246
|
+
["docker", "compose", "-f", cf, "config", "--format", "json"]
|
|
233
247
|
)
|
|
234
248
|
if result.returncode != 0:
|
|
235
249
|
continue
|
|
@@ -268,10 +282,8 @@ def check_infra():
|
|
|
268
282
|
services_depended_on: dict[str, str] = {} # depended_svc -> by_svc
|
|
269
283
|
|
|
270
284
|
for label, cf in compose_files:
|
|
271
|
-
result =
|
|
272
|
-
["docker", "compose", "-f", cf, "config", "--format", "json"]
|
|
273
|
-
capture_output=True,
|
|
274
|
-
text=True,
|
|
285
|
+
result = _run(
|
|
286
|
+
["docker", "compose", "-f", cf, "config", "--format", "json"]
|
|
275
287
|
)
|
|
276
288
|
if result.returncode != 0:
|
|
277
289
|
continue
|
|
@@ -309,10 +321,8 @@ def check_infra():
|
|
|
309
321
|
click.echo(click.style(" Volumes", bold=True))
|
|
310
322
|
all_volumes: dict[str, list[str]] = {} # vol_name -> [labels]
|
|
311
323
|
for label, cf in compose_files:
|
|
312
|
-
result =
|
|
313
|
-
["docker", "compose", "-f", cf, "config", "--format", "json"]
|
|
314
|
-
capture_output=True,
|
|
315
|
-
text=True,
|
|
324
|
+
result = _run(
|
|
325
|
+
["docker", "compose", "-f", cf, "config", "--format", "json"]
|
|
316
326
|
)
|
|
317
327
|
if result.returncode != 0:
|
|
318
328
|
continue
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import click
|
|
2
|
-
import subprocess
|
|
3
2
|
import os
|
|
4
3
|
|
|
5
4
|
from splent_cli.utils.path_utils import PathUtils
|
|
5
|
+
from splent_cli.utils.proc import run
|
|
6
6
|
from splent_cli.services import context
|
|
7
7
|
|
|
8
8
|
|
|
@@ -36,9 +36,12 @@ def coverage(module_name, html):
|
|
|
36
36
|
if html:
|
|
37
37
|
coverage_cmd.extend(["--cov-report", "html"])
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
result = run(
|
|
40
|
+
coverage_cmd,
|
|
41
|
+
check=False,
|
|
42
|
+
tool_hint="Install it with: pip install pytest pytest-cov",
|
|
43
|
+
)
|
|
44
|
+
if result.returncode != 0:
|
|
42
45
|
click.echo(
|
|
43
46
|
click.style("❌ Coverage run failed (tests may be failing).", fg="red")
|
|
44
47
|
)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import click
|
|
2
|
-
import subprocess
|
|
3
2
|
from dotenv import load_dotenv
|
|
4
3
|
import os
|
|
5
4
|
|
|
6
5
|
from splent_cli.services import context
|
|
6
|
+
from splent_cli.utils.proc import run, require_tool
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
@click.command(
|
|
@@ -38,19 +38,34 @@ def db_console():
|
|
|
38
38
|
)
|
|
39
39
|
raise SystemExit(1)
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
require_tool(
|
|
42
|
+
"mysql",
|
|
43
|
+
"Install the MariaDB/MySQL client (e.g. 'mariadb-client') and ensure it is on your PATH.",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
result = run(
|
|
47
|
+
[
|
|
48
|
+
"mysql",
|
|
49
|
+
f"-h{mariadb_hostname}",
|
|
50
|
+
f"-u{mariadb_user}",
|
|
51
|
+
f"-p{mariadb_password}",
|
|
52
|
+
mariadb_database,
|
|
53
|
+
],
|
|
54
|
+
check=False,
|
|
55
|
+
)
|
|
56
|
+
if result.returncode != 0:
|
|
57
|
+
click.secho(
|
|
58
|
+
f"❌ Could not open the MariaDB console (mysql exited {result.returncode}).",
|
|
59
|
+
fg="red",
|
|
60
|
+
)
|
|
61
|
+
click.secho(
|
|
62
|
+
" Is the database container running and reachable?\n"
|
|
63
|
+
f" Tried to connect to host '{mariadb_hostname}' as user "
|
|
64
|
+
f"'{mariadb_user}'.\n"
|
|
65
|
+
" Start it with: splent product:up (or check 'docker ps').",
|
|
66
|
+
fg="yellow",
|
|
51
67
|
)
|
|
52
|
-
|
|
53
|
-
click.secho(f"❌ Error opening MariaDB console: {e}", fg="red")
|
|
68
|
+
raise SystemExit(1)
|
|
54
69
|
|
|
55
70
|
|
|
56
71
|
cli_command = db_console
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import click
|
|
2
|
-
import subprocess
|
|
3
2
|
from dotenv import load_dotenv
|
|
4
3
|
import os
|
|
4
|
+
import tempfile
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
|
|
7
7
|
from splent_cli.services import context
|
|
8
|
+
from splent_cli.utils.proc import run, require_tool
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@click.command(
|
|
@@ -44,10 +45,23 @@ def db_dump(filename):
|
|
|
44
45
|
if not filename.endswith(".sql"):
|
|
45
46
|
filename += ".sql"
|
|
46
47
|
|
|
48
|
+
require_tool(
|
|
49
|
+
"mysqldump",
|
|
50
|
+
"Install the MariaDB/MySQL client tools (provides 'mysqldump').",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
env = {**os.environ, "MYSQL_PWD": mariadb_password or ""}
|
|
54
|
+
|
|
55
|
+
# Dump to a temp file first and only replace the target on success, so a
|
|
56
|
+
# pre-existing file with the same name is never truncated or deleted if the
|
|
57
|
+
# dump fails midway.
|
|
58
|
+
target_dir = os.path.dirname(os.path.abspath(filename)) or "."
|
|
59
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
60
|
+
prefix=".db_dump_", suffix=".sql.tmp", dir=target_dir
|
|
61
|
+
)
|
|
47
62
|
try:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
subprocess.run(
|
|
63
|
+
with os.fdopen(fd, "wb") as out:
|
|
64
|
+
run(
|
|
51
65
|
[
|
|
52
66
|
"mysqldump",
|
|
53
67
|
f"-h{mariadb_hostname}",
|
|
@@ -55,17 +69,18 @@ def db_dump(filename):
|
|
|
55
69
|
mariadb_database,
|
|
56
70
|
],
|
|
57
71
|
stdout=out,
|
|
58
|
-
|
|
72
|
+
text=False,
|
|
59
73
|
env=env,
|
|
74
|
+
tool_hint="Install the MariaDB/MySQL client tools (provides 'mysqldump').",
|
|
60
75
|
)
|
|
76
|
+
os.replace(tmp_path, filename)
|
|
61
77
|
click.echo(
|
|
62
78
|
click.style(f"Database dump created successfully: {filename}", fg="green")
|
|
63
79
|
)
|
|
64
|
-
except
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
click.echo(click.style(f"Partial file removed: {filename}", fg="yellow"))
|
|
80
|
+
except BaseException:
|
|
81
|
+
if os.path.exists(tmp_path):
|
|
82
|
+
os.remove(tmp_path)
|
|
83
|
+
raise
|
|
69
84
|
|
|
70
85
|
|
|
71
86
|
cli_command = db_dump
|
|
@@ -125,19 +125,39 @@ def db_migrate(feature, message, empty):
|
|
|
125
125
|
prev_level = alembic_logger.level
|
|
126
126
|
alembic_logger.setLevel(logging.WARNING)
|
|
127
127
|
|
|
128
|
+
gen_error = None
|
|
128
129
|
try:
|
|
129
130
|
if empty:
|
|
130
131
|
alembic_revision(directory=mdir, message=message or feat)
|
|
131
132
|
else:
|
|
132
133
|
alembic_migrate(directory=mdir, message=message or feat)
|
|
133
134
|
except Exception as e:
|
|
134
|
-
|
|
135
|
-
click.secho(
|
|
136
|
-
f" ⚠️ {feat}: migration generation skipped ({e})", fg="yellow"
|
|
137
|
-
)
|
|
135
|
+
gen_error = e
|
|
138
136
|
finally:
|
|
139
137
|
alembic_logger.setLevel(prev_level)
|
|
140
138
|
|
|
139
|
+
# Always surface generation failures — never report "up to date" when the
|
|
140
|
+
# migration actually failed to generate. Show a one-line error (full
|
|
141
|
+
# detail only under SPLENT_DEBUG).
|
|
142
|
+
if gen_error is not None:
|
|
143
|
+
click.echo(
|
|
144
|
+
click.style(
|
|
145
|
+
f" ❌ {feat}: migration generation failed ({gen_error})",
|
|
146
|
+
fg="red",
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
if os.getenv("SPLENT_DEBUG"):
|
|
150
|
+
import traceback
|
|
151
|
+
|
|
152
|
+
click.echo(
|
|
153
|
+
"".join(
|
|
154
|
+
traceback.format_exception(
|
|
155
|
+
type(gen_error), gen_error, gen_error.__traceback__
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
continue
|
|
160
|
+
|
|
141
161
|
after = _count_versions(mdir)
|
|
142
162
|
|
|
143
163
|
# A blank migration requested with --empty is intentional: keep it so the
|
|
@@ -41,9 +41,14 @@ def db_reset(yes):
|
|
|
41
41
|
return
|
|
42
42
|
|
|
43
43
|
# --- STEP 1: Drop ALL tables (including alembic_* and splent_migrations) ---
|
|
44
|
+
# Run every DROP inside a single transaction (engine.begin) so that a
|
|
45
|
+
# mid-operation connection failure rolls back as a unit rather than
|
|
46
|
+
# leaving a half-dropped schema. Note: MySQL/MariaDB issue an implicit
|
|
47
|
+
# COMMIT on DDL, so a full rollback is not guaranteed there; we therefore
|
|
48
|
+
# also surface explicit partial-state recovery guidance on failure.
|
|
44
49
|
click.echo(click.style("🗑️ Dropping all tables...", fg="yellow"))
|
|
45
50
|
try:
|
|
46
|
-
with db.engine.
|
|
51
|
+
with db.engine.begin() as conn:
|
|
47
52
|
conn.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
|
|
48
53
|
meta = MetaData()
|
|
49
54
|
meta.reflect(bind=db.engine)
|
|
@@ -51,11 +56,21 @@ def db_reset(yes):
|
|
|
51
56
|
conn.execute(text(f"DROP TABLE IF EXISTS `{table.name}`"))
|
|
52
57
|
click.echo(click.style(f" Dropped {table.name}", fg="bright_black"))
|
|
53
58
|
conn.execute(text("SET FOREIGN_KEY_CHECKS = 1"))
|
|
54
|
-
conn.commit()
|
|
55
59
|
click.echo(click.style("✅ All tables dropped.", fg="yellow"))
|
|
56
60
|
except Exception as e:
|
|
57
61
|
click.echo(click.style(f"❌ Error dropping tables: {e}", fg="red"))
|
|
58
|
-
|
|
62
|
+
click.echo(
|
|
63
|
+
click.style(
|
|
64
|
+
"⚠️ The database may now be in a PARTIAL state: some tables "
|
|
65
|
+
"may have been dropped while others remain.\n"
|
|
66
|
+
" To recover, re-run this command once the database is "
|
|
67
|
+
"reachable again (it is safe to repeat — drops use "
|
|
68
|
+
"DROP TABLE IF EXISTS), or manually drop the remaining "
|
|
69
|
+
"tables / re-create the schema from migrations.",
|
|
70
|
+
fg="red",
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
raise SystemExit(1)
|
|
59
74
|
|
|
60
75
|
# --- STEP 2: Recreate splent_migrations tracking table ---
|
|
61
76
|
click.echo(click.style("📋 Recreating splent_migrations table...", fg="cyan"))
|
|
@@ -74,7 +89,15 @@ def db_reset(yes):
|
|
|
74
89
|
)
|
|
75
90
|
except Exception as e:
|
|
76
91
|
click.echo(click.style(f"❌ Error creating tracking table: {e}", fg="red"))
|
|
77
|
-
|
|
92
|
+
click.echo(
|
|
93
|
+
click.style(
|
|
94
|
+
"⚠️ All tables were dropped but the tracking table could not "
|
|
95
|
+
"be recreated. Re-run this command once the database is "
|
|
96
|
+
"reachable to finish the reset.",
|
|
97
|
+
fg="red",
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
raise SystemExit(1)
|
|
78
101
|
|
|
79
102
|
# --- STEP 3: Clear uploads ---
|
|
80
103
|
ctx = click.get_current_context()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import click
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
from splent_cli.services import context
|
|
7
|
+
from splent_cli.utils.proc import run
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command(
|
|
11
|
+
"db:restore",
|
|
12
|
+
short_help="Restore a MariaDB database from a SQL dump file.",
|
|
13
|
+
)
|
|
14
|
+
@click.argument("filename")
|
|
15
|
+
@click.option("--yes", is_flag=True, help="Skip confirmation prompt.")
|
|
16
|
+
@context.requires_product
|
|
17
|
+
def db_restore(filename, yes):
|
|
18
|
+
"""
|
|
19
|
+
Restore the database from FILENAME (a .sql dump).
|
|
20
|
+
|
|
21
|
+
Reads connection credentials from the workspace .env file
|
|
22
|
+
(MARIADB_HOSTNAME, MARIADB_USER, MARIADB_PASSWORD, MARIADB_DATABASE).
|
|
23
|
+
|
|
24
|
+
\b
|
|
25
|
+
Example:
|
|
26
|
+
splent db:restore dump_20250101_120000.sql
|
|
27
|
+
"""
|
|
28
|
+
load_dotenv()
|
|
29
|
+
|
|
30
|
+
if not os.path.exists(filename):
|
|
31
|
+
click.secho(f"❌ File not found: {filename}", fg="red")
|
|
32
|
+
raise SystemExit(1)
|
|
33
|
+
|
|
34
|
+
host = os.getenv("MARIADB_HOSTNAME")
|
|
35
|
+
user = os.getenv("MARIADB_USER")
|
|
36
|
+
password = os.getenv("MARIADB_PASSWORD")
|
|
37
|
+
database = os.getenv("MARIADB_DATABASE")
|
|
38
|
+
|
|
39
|
+
missing = [
|
|
40
|
+
k
|
|
41
|
+
for k, v in {
|
|
42
|
+
"MARIADB_HOSTNAME": host,
|
|
43
|
+
"MARIADB_USER": user,
|
|
44
|
+
"MARIADB_PASSWORD": password,
|
|
45
|
+
"MARIADB_DATABASE": database,
|
|
46
|
+
}.items()
|
|
47
|
+
if not v
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
if missing:
|
|
51
|
+
click.secho(f"❌ Missing env vars: {', '.join(missing)}", fg="red")
|
|
52
|
+
raise SystemExit(1)
|
|
53
|
+
|
|
54
|
+
if not yes:
|
|
55
|
+
click.secho(
|
|
56
|
+
f"⚠️ This will overwrite the database '{database}' with the contents of '{filename}'.",
|
|
57
|
+
fg="yellow",
|
|
58
|
+
)
|
|
59
|
+
if not click.confirm("Continue?"):
|
|
60
|
+
click.echo("❎ Cancelled.")
|
|
61
|
+
raise SystemExit(0)
|
|
62
|
+
|
|
63
|
+
env = {**os.environ, "MYSQL_PWD": password or ""}
|
|
64
|
+
|
|
65
|
+
# Take an automatic pre-restore backup so a corrupt/partial dump cannot
|
|
66
|
+
# leave the live database half-overwritten with no way back.
|
|
67
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
68
|
+
backup_filename = f"pre_restore_{database}_{timestamp}.sql"
|
|
69
|
+
try:
|
|
70
|
+
with open(backup_filename, "wb") as backup_out:
|
|
71
|
+
run(
|
|
72
|
+
["mysqldump", f"-h{host}", f"-u{user}", database],
|
|
73
|
+
stdout=backup_out,
|
|
74
|
+
env=env,
|
|
75
|
+
tool_hint=(
|
|
76
|
+
"Install the MariaDB/MySQL client tools (provides 'mysqldump'), "
|
|
77
|
+
"e.g. 'mysql-client' / 'mariadb-client'."
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
except click.ClickException:
|
|
81
|
+
# mysqldump missing/failed (often a connection failure): do not proceed
|
|
82
|
+
# to overwrite the live database without a safety net.
|
|
83
|
+
if os.path.exists(backup_filename):
|
|
84
|
+
os.remove(backup_filename)
|
|
85
|
+
click.secho(
|
|
86
|
+
f"❌ Could not create a pre-restore backup of '{database}' "
|
|
87
|
+
f"(check the database is reachable at '{host}' and credentials are correct).\n"
|
|
88
|
+
"Aborting before overwriting the live database.",
|
|
89
|
+
fg="red",
|
|
90
|
+
)
|
|
91
|
+
raise SystemExit(1)
|
|
92
|
+
|
|
93
|
+
click.secho(f"🛟 Pre-restore backup saved to: {backup_filename}", fg="cyan")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
with open(filename, "rb") as sql_file:
|
|
97
|
+
run(
|
|
98
|
+
["mysql", f"-h{host}", f"-u{user}", database],
|
|
99
|
+
stdin=sql_file,
|
|
100
|
+
env=env,
|
|
101
|
+
tool_hint=(
|
|
102
|
+
"Install the MariaDB/MySQL client tools (provides 'mysql'), "
|
|
103
|
+
"e.g. 'mysql-client' / 'mariadb-client'."
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
click.secho(f"✅ Database restored from: {filename}", fg="green")
|
|
107
|
+
except click.ClickException as e:
|
|
108
|
+
click.secho(f"❌ Error restoring database: {e.format_message()}", fg="red")
|
|
109
|
+
click.secho(
|
|
110
|
+
"The database may be in an inconsistent state. To roll back, restore "
|
|
111
|
+
f"the pre-restore backup:\n splent db:restore {backup_filename} --yes",
|
|
112
|
+
fg="yellow",
|
|
113
|
+
)
|
|
114
|
+
raise SystemExit(1)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
cli_command = db_restore
|
|
@@ -146,7 +146,11 @@ def db_rollback(feature, steps, cascade):
|
|
|
146
146
|
click.echo(" ❎ Cancelled.")
|
|
147
147
|
raise SystemExit(1)
|
|
148
148
|
|
|
149
|
-
# Rollback dependents first (reverse order)
|
|
149
|
+
# Rollback dependents first (reverse order).
|
|
150
|
+
# NOTE: there is no cross-feature transaction — if a later dependent
|
|
151
|
+
# fails, earlier ones have already been rolled back. Track progress so
|
|
152
|
+
# the failure message can tell the user the partial state to recover.
|
|
153
|
+
rolled_back: list[str] = []
|
|
150
154
|
for dep in reversed(migrated_dependents):
|
|
151
155
|
dep_mdir = MigrationManager.get_feature_migration_dir(dep)
|
|
152
156
|
if dep_mdir:
|
|
@@ -160,6 +164,7 @@ def db_rollback(feature, steps, cascade):
|
|
|
160
164
|
)
|
|
161
165
|
MigrationManager.update_feature_status(app, dep, dep_rev)
|
|
162
166
|
click.secho(f" ✅ {dep} → {dep_rev or 'base'}", fg="green")
|
|
167
|
+
rolled_back.append(dep)
|
|
163
168
|
|
|
164
169
|
for entry in get_features_from_pyproject() or []:
|
|
165
170
|
key, ns, name, version = resolve_feature_key_from_entry(entry)
|
|
@@ -177,6 +182,22 @@ def db_rollback(feature, steps, cascade):
|
|
|
177
182
|
break
|
|
178
183
|
except Exception as e:
|
|
179
184
|
click.secho(f" ❌ {dep}: {e}", fg="red")
|
|
185
|
+
click.secho(
|
|
186
|
+
f" ⚠️ Partial rollback: '{feature}' was NOT rolled back.",
|
|
187
|
+
fg="yellow",
|
|
188
|
+
)
|
|
189
|
+
if rolled_back:
|
|
190
|
+
click.secho(
|
|
191
|
+
" Already rolled back (not restored): "
|
|
192
|
+
+ ", ".join(rolled_back),
|
|
193
|
+
fg="yellow",
|
|
194
|
+
)
|
|
195
|
+
click.secho(
|
|
196
|
+
" To recover, re-apply migrations with "
|
|
197
|
+
"'flask db:migrate' for those features, "
|
|
198
|
+
"or fix the error above and re-run db:rollback.",
|
|
199
|
+
fg="yellow",
|
|
200
|
+
)
|
|
180
201
|
raise SystemExit(1)
|
|
181
202
|
|
|
182
203
|
# Rollback the target feature
|
|
@@ -206,7 +227,19 @@ def db_rollback(feature, steps, cascade):
|
|
|
206
227
|
)
|
|
207
228
|
break
|
|
208
229
|
except Exception as e:
|
|
209
|
-
click.secho(f" {feature}: {e}", fg="red")
|
|
230
|
+
click.secho(f" ❌ {feature}: {e}", fg="red")
|
|
231
|
+
if migrated_dependents:
|
|
232
|
+
click.secho(
|
|
233
|
+
" ⚠️ Partial rollback: dependent feature(s) were already "
|
|
234
|
+
"rolled back but '" + feature + "' failed.",
|
|
235
|
+
fg="yellow",
|
|
236
|
+
)
|
|
237
|
+
click.secho(
|
|
238
|
+
" The product is in a mixed state. Fix the error above and "
|
|
239
|
+
"re-run db:rollback, or re-apply migrations with 'flask "
|
|
240
|
+
"db:migrate' to restore the rolled-back dependents.",
|
|
241
|
+
fg="yellow",
|
|
242
|
+
)
|
|
210
243
|
raise SystemExit(1)
|
|
211
244
|
|
|
212
245
|
# Offer to delete migration files if fully rolled back to base
|
|
@@ -218,8 +251,16 @@ def db_rollback(feature, steps, cascade):
|
|
|
218
251
|
for f in os.listdir(versions_dir)
|
|
219
252
|
if f.endswith(".py") and not f.startswith("__")
|
|
220
253
|
]
|
|
254
|
+
if migration_files:
|
|
255
|
+
click.secho(
|
|
256
|
+
" ⚠️ Deleting migration files is IRREVERSIBLE — the "
|
|
257
|
+
"files cannot be recovered and the feature will need new "
|
|
258
|
+
"migrations to be re-applied.",
|
|
259
|
+
fg="yellow",
|
|
260
|
+
)
|
|
221
261
|
if migration_files and click.confirm(
|
|
222
|
-
f"
|
|
262
|
+
f" Permanently delete {len(migration_files)} migration "
|
|
263
|
+
"file(s)? (irreversible)",
|
|
223
264
|
default=False,
|
|
224
265
|
):
|
|
225
266
|
for f in migration_files:
|