splent-cli 1.7.0__tar.gz → 1.9.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.7.0/src/splent_cli.egg-info → splent_cli-1.9.0}/PKG-INFO +2 -1
- {splent_cli-1.7.0 → splent_cli-1.9.0}/pyproject.toml +2 -1
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/check/check_pyproject.py +53 -14
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/database/db_migrate.py +39 -3
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/export/export_puml.py +100 -25
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_release.py +40 -10
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_xray.py +10 -7
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_containers.py +3 -3
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_deploy.py +6 -2
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_env.py +6 -1
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/spl/spl_add_feature.py +25 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/services/preflight.py +50 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/services/release.py +25 -3
- {splent_cli-1.7.0 → splent_cli-1.9.0/src/splent_cli.egg-info}/PKG-INFO +2 -1
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli.egg-info/requires.txt +1 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/LICENSE +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/README.md +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/setup.cfg +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/cli.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/check/check_deps.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/check/check_env.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/check/check_features.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/check/check_infra.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/check/check_product.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/clear/__init__.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/command/__init__.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/command/command_create.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/coverage.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/database/db_console.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/database/db_dump.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/database/db_reset.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/database/db_restore.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/database/db_seed.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/database/db_status.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/doctor.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/env/env_set.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/export/__init__.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_test.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/lint.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/locust.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_build.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_clean.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_commands.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_config.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_configure.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_console.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_create.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_derive.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_down.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_drift.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_missing.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_port.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_release.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_restart.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_routes.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_run.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_select.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_signals.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_test.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_up.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/product/product_validate.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/release/release_core.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/selenium.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/spl/__init__.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/tokens_setup.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/commands/version.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/services/__init__.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/services/compose.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/services/context.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/utils/__init__.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/utils/cache_utils.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/utils/command_loader.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/utils/contract_freshness.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/utils/feature_installer.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/utils/feature_utils.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/utils/git_url.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/utils/integrity.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/utils/lifecycle.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/utils/manifest.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli/utils/template_drift.py +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli.egg-info/SOURCES.txt +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.7.0 → splent_cli-1.9.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.9.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
|
|
@@ -10,6 +10,7 @@ Description-Content-Type: text/markdown
|
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: click==8.1.8
|
|
12
12
|
Requires-Dist: packaging>=24.0
|
|
13
|
+
Requires-Dist: requests>=2.31.0
|
|
13
14
|
Provides-Extra: dev
|
|
14
15
|
Requires-Dist: setuptools==80.3.1; extra == "dev"
|
|
15
16
|
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.9.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"
|
|
@@ -14,6 +14,7 @@ license-files = ["LICENSE"]
|
|
|
14
14
|
dependencies = [
|
|
15
15
|
"click==8.1.8",
|
|
16
16
|
"packaging>=24.0",
|
|
17
|
+
"requests>=2.31.0",
|
|
17
18
|
]
|
|
18
19
|
|
|
19
20
|
[project.optional-dependencies]
|
|
@@ -9,7 +9,6 @@ import click
|
|
|
9
9
|
import tomllib
|
|
10
10
|
|
|
11
11
|
from splent_cli.services import context
|
|
12
|
-
from splent_cli.utils.feature_utils import read_features_from_data
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
def _find_missing_pkgs(deps: list) -> list:
|
|
@@ -88,29 +87,69 @@ def check_pyproject():
|
|
|
88
87
|
_ok(f"All {len(deps)} dependencies satisfied")
|
|
89
88
|
|
|
90
89
|
# Features
|
|
91
|
-
features = read_features_from_data(data)
|
|
92
|
-
_ok(f"{len(features)} features declared in [tool.splent.features]")
|
|
93
|
-
|
|
94
90
|
splent = data.get("tool", {}).get("splent", {})
|
|
91
|
+
base_feats = splent.get("features", [])
|
|
95
92
|
dev_feats = splent.get("features_dev", [])
|
|
96
93
|
prod_feats = splent.get("features_prod", [])
|
|
94
|
+
|
|
95
|
+
_ok(f"{len(base_feats)} features in [tool.splent].features")
|
|
97
96
|
if dev_feats:
|
|
98
97
|
_ok(f"{len(dev_feats)} dev-only features")
|
|
99
98
|
if prod_feats:
|
|
100
99
|
_ok(f"{len(prod_feats)} prod-only features")
|
|
101
100
|
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
# Check for duplicates across feature lists
|
|
102
|
+
def _bare_name(entry):
|
|
103
|
+
name = entry.split("/")[-1] if "/" in entry else entry
|
|
104
|
+
return name.split("@")[0]
|
|
105
|
+
|
|
106
|
+
all_entries = base_feats + dev_feats + prod_feats
|
|
107
|
+
seen_names = {}
|
|
108
|
+
for entry in all_entries:
|
|
109
|
+
bare = _bare_name(entry)
|
|
110
|
+
seen_names.setdefault(bare, []).append(entry)
|
|
111
|
+
|
|
112
|
+
for bare, entries in seen_names.items():
|
|
113
|
+
if len(entries) > 1:
|
|
114
|
+
short = bare.replace("splent_feature_", "")
|
|
115
|
+
_fail(f"Duplicate feature '{short}': {', '.join(entries)}")
|
|
116
|
+
|
|
117
|
+
# Check for inconsistent namespaces
|
|
118
|
+
namespaces = set()
|
|
119
|
+
for entry in all_entries:
|
|
120
|
+
if "/" in entry:
|
|
121
|
+
ns = entry.split("/")[0]
|
|
122
|
+
namespaces.add(ns)
|
|
123
|
+
if len(namespaces) > 1:
|
|
124
|
+
_warn(
|
|
125
|
+
f"Inconsistent namespaces: {', '.join(sorted(namespaces))} — use one format consistently"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# SPL / UVL
|
|
129
|
+
spl_name = splent.get("spl")
|
|
130
|
+
if spl_name:
|
|
131
|
+
uvl_path = os.path.join(
|
|
132
|
+
workspace, "splent_catalog", spl_name, f"{spl_name}.uvl"
|
|
133
|
+
)
|
|
134
|
+
if os.path.isfile(uvl_path):
|
|
135
|
+
_ok(f"SPL catalog: {spl_name} (UVL found)")
|
|
110
136
|
else:
|
|
111
|
-
_fail(
|
|
137
|
+
_fail(
|
|
138
|
+
f"SPL catalog: {spl_name} — UVL not found at splent_catalog/{spl_name}/{spl_name}.uvl"
|
|
139
|
+
)
|
|
112
140
|
else:
|
|
113
|
-
|
|
141
|
+
# Legacy fallback
|
|
142
|
+
uvl_cfg = splent.get("uvl", {})
|
|
143
|
+
uvl_file = uvl_cfg.get("file")
|
|
144
|
+
if uvl_file:
|
|
145
|
+
app_path = os.path.join(workspace, product)
|
|
146
|
+
uvl_path = os.path.join(app_path, "uvl", uvl_file)
|
|
147
|
+
if os.path.exists(uvl_path):
|
|
148
|
+
_ok(f"UVL file found: uvl/{uvl_file} (legacy)")
|
|
149
|
+
else:
|
|
150
|
+
_fail(f"UVL file not found: uvl/{uvl_file}")
|
|
151
|
+
else:
|
|
152
|
+
_warn("No SPL configured — set [tool.splent].spl in pyproject.toml")
|
|
114
153
|
|
|
115
154
|
click.echo()
|
|
116
155
|
if fail:
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
|
|
3
3
|
import click
|
|
4
4
|
from flask_migrate import migrate as alembic_migrate
|
|
5
|
+
from flask_migrate import revision as alembic_revision
|
|
5
6
|
|
|
6
7
|
from splent_cli.utils.decorators import requires_db
|
|
7
8
|
from splent_cli.services import context
|
|
@@ -54,14 +55,31 @@ def _is_empty_migration(path: str) -> bool:
|
|
|
54
55
|
@click.option(
|
|
55
56
|
"-m", "message", default=None, help="Migration message (defaults to feature name)."
|
|
56
57
|
)
|
|
58
|
+
@click.option(
|
|
59
|
+
"--empty",
|
|
60
|
+
is_flag=True,
|
|
61
|
+
help=(
|
|
62
|
+
"Generate a blank migration (no autogenerate) and keep it for manual "
|
|
63
|
+
"editing. Required for refinement features that add columns to a base "
|
|
64
|
+
"feature's table — Alembic cannot autodetect runtime-injected mixins."
|
|
65
|
+
),
|
|
66
|
+
)
|
|
57
67
|
@context.requires_product
|
|
58
|
-
def db_migrate(feature, message):
|
|
68
|
+
def db_migrate(feature, message, empty):
|
|
59
69
|
"""
|
|
60
70
|
Generate new migration files for features that have schema changes,
|
|
61
71
|
then apply all pending migrations.
|
|
62
72
|
|
|
63
|
-
Empty migrations (no schema changes detected) are automatically removed
|
|
73
|
+
Empty migrations (no schema changes detected) are automatically removed,
|
|
74
|
+
unless ``--empty`` is passed, in which case a blank migration is created
|
|
75
|
+
and kept so you can write ``upgrade()``/``downgrade()`` by hand.
|
|
64
76
|
"""
|
|
77
|
+
if empty and not feature:
|
|
78
|
+
click.echo(
|
|
79
|
+
click.style("❌ --empty requires a FEATURE argument.", fg="red")
|
|
80
|
+
)
|
|
81
|
+
raise SystemExit(1)
|
|
82
|
+
|
|
65
83
|
if feature:
|
|
66
84
|
dirs = {}
|
|
67
85
|
mdir = MigrationManager.get_feature_migration_dir(feature)
|
|
@@ -108,7 +126,10 @@ def db_migrate(feature, message):
|
|
|
108
126
|
alembic_logger.setLevel(logging.WARNING)
|
|
109
127
|
|
|
110
128
|
try:
|
|
111
|
-
|
|
129
|
+
if empty:
|
|
130
|
+
alembic_revision(directory=mdir, message=message or feat)
|
|
131
|
+
else:
|
|
132
|
+
alembic_migrate(directory=mdir, message=message or feat)
|
|
112
133
|
except Exception as e:
|
|
113
134
|
if os.getenv("SPLENT_DEBUG"):
|
|
114
135
|
click.secho(
|
|
@@ -119,6 +140,21 @@ def db_migrate(feature, message):
|
|
|
119
140
|
|
|
120
141
|
after = _count_versions(mdir)
|
|
121
142
|
|
|
143
|
+
# A blank migration requested with --empty is intentional: keep it so the
|
|
144
|
+
# user can fill in upgrade()/downgrade() by hand (e.g. refinement columns).
|
|
145
|
+
if empty:
|
|
146
|
+
if after > before:
|
|
147
|
+
click.echo(
|
|
148
|
+
click.style(
|
|
149
|
+
f" 📝 {feat}: blank migration generated — "
|
|
150
|
+
f"edit upgrade()/downgrade() and run 'splent db:upgrade'",
|
|
151
|
+
fg="cyan",
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
click.echo(click.style(f" ✔ {feat}: up to date", fg="green"))
|
|
156
|
+
continue
|
|
157
|
+
|
|
122
158
|
# If a new file was generated, check if it's empty (no real schema changes)
|
|
123
159
|
if after > before:
|
|
124
160
|
versions_dir = os.path.join(mdir, "versions")
|
|
@@ -149,8 +149,8 @@ def _parse_models(models_path: str) -> list[dict]:
|
|
|
149
149
|
text = "\n".join(joined_lines)
|
|
150
150
|
|
|
151
151
|
classes = []
|
|
152
|
-
# Split by class definition
|
|
153
|
-
class_blocks = re.split(r"^(class \w
|
|
152
|
+
# Split by class definition (with or without parentheses for mixins)
|
|
153
|
+
class_blocks = re.split(r"^(class \w+(?:\(.*?\))?:)", text, flags=re.MULTILINE)
|
|
154
154
|
|
|
155
155
|
i = 1
|
|
156
156
|
while i < len(class_blocks):
|
|
@@ -158,7 +158,7 @@ def _parse_models(models_path: str) -> list[dict]:
|
|
|
158
158
|
body = class_blocks[i + 1] if i + 1 < len(class_blocks) else ""
|
|
159
159
|
i += 2
|
|
160
160
|
|
|
161
|
-
class_m = re.match(r"class (\w+)
|
|
161
|
+
class_m = re.match(r"class (\w+)", header)
|
|
162
162
|
if not class_m:
|
|
163
163
|
continue
|
|
164
164
|
class_name = class_m.group(1)
|
|
@@ -213,8 +213,11 @@ def _parse_models(models_path: str) -> list[dict]:
|
|
|
213
213
|
if rel_m:
|
|
214
214
|
rel_name = rel_m.group(1)
|
|
215
215
|
rel_args = rel_m.group(2)
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
# Target can be unquoted (User) or quoted ("User" / 'User')
|
|
217
|
+
target_m = re.match(r"""[\"']?(\w+)[\"']?""", rel_args)
|
|
218
|
+
target = target_m.group(1) if target_m else None
|
|
219
|
+
if not target:
|
|
220
|
+
continue
|
|
218
221
|
uselist = "uselist=False" not in rel_args
|
|
219
222
|
relationships.append(
|
|
220
223
|
{
|
|
@@ -668,11 +671,16 @@ def _render_exports(
|
|
|
668
671
|
)
|
|
669
672
|
except subprocess.CalledProcessError as e:
|
|
670
673
|
click.secho("❌ PlantUML failed to render diagram.", fg="red")
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
click.secho(stderr
|
|
674
|
+
stderr = e.stderr
|
|
675
|
+
if isinstance(stderr, bytes):
|
|
676
|
+
stderr = stderr.decode(errors="replace")
|
|
677
|
+
if stderr:
|
|
678
|
+
click.secho(stderr.strip(), fg="bright_black")
|
|
679
|
+
stdout = e.stdout
|
|
680
|
+
if isinstance(stdout, bytes):
|
|
681
|
+
stdout = stdout.decode(errors="replace")
|
|
682
|
+
if stdout:
|
|
683
|
+
click.secho(stdout.strip(), fg="bright_black")
|
|
676
684
|
return
|
|
677
685
|
|
|
678
686
|
try:
|
|
@@ -780,21 +788,41 @@ def export_puml(
|
|
|
780
788
|
product = context.require_app()
|
|
781
789
|
product_dir = os.path.join(workspace, product)
|
|
782
790
|
|
|
783
|
-
# Read UVL
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
791
|
+
# Read UVL — try SPL catalog first, fall back to legacy [tool.splent.uvl]
|
|
792
|
+
import tomllib
|
|
793
|
+
|
|
794
|
+
uvl_path = None
|
|
795
|
+
pyproject_path = os.path.join(product_dir, "pyproject.toml")
|
|
796
|
+
if os.path.isfile(pyproject_path):
|
|
797
|
+
with open(pyproject_path, "rb") as f:
|
|
798
|
+
pydata = tomllib.load(f)
|
|
799
|
+
spl_name = pydata.get("tool", {}).get("splent", {}).get("spl")
|
|
800
|
+
if spl_name:
|
|
801
|
+
candidate = os.path.join(
|
|
802
|
+
workspace, "splent_catalog", spl_name, f"{spl_name}.uvl"
|
|
803
|
+
)
|
|
804
|
+
if os.path.isfile(candidate):
|
|
805
|
+
uvl_path = candidate
|
|
806
|
+
|
|
807
|
+
# Legacy fallback: [tool.splent.uvl].file
|
|
808
|
+
if not uvl_path:
|
|
809
|
+
try:
|
|
810
|
+
uvl_cfg = PyprojectReader.for_product(product_dir).uvl_config
|
|
811
|
+
uvl_file = uvl_cfg.get("file")
|
|
812
|
+
if uvl_file:
|
|
813
|
+
candidate = os.path.join(product_dir, "uvl", uvl_file)
|
|
814
|
+
if os.path.isfile(candidate):
|
|
815
|
+
uvl_path = candidate
|
|
816
|
+
except (FileNotFoundError, RuntimeError):
|
|
817
|
+
pass
|
|
818
|
+
|
|
819
|
+
if not uvl_path:
|
|
820
|
+
click.secho(
|
|
821
|
+
"❌ No UVL file found.\n"
|
|
822
|
+
" Set [tool.splent].spl in pyproject.toml (SPL catalog)\n"
|
|
823
|
+
" or [tool.splent.uvl].file (legacy).",
|
|
824
|
+
fg="red",
|
|
825
|
+
)
|
|
798
826
|
raise SystemExit(1)
|
|
799
827
|
|
|
800
828
|
click.echo(f"📖 Reading UVL model: {uvl_path}")
|
|
@@ -848,6 +876,53 @@ def export_puml(
|
|
|
848
876
|
if parsed:
|
|
849
877
|
all_models[package] = parsed
|
|
850
878
|
break
|
|
879
|
+
# Apply refinement mixins: merge mixin attributes into target models
|
|
880
|
+
for package, fpath in feature_paths.items():
|
|
881
|
+
pyproject_path = os.path.join(fpath, "pyproject.toml")
|
|
882
|
+
if not os.path.isfile(pyproject_path):
|
|
883
|
+
continue
|
|
884
|
+
import tomllib as _tomllib
|
|
885
|
+
|
|
886
|
+
with open(pyproject_path, "rb") as f:
|
|
887
|
+
feat_data = _tomllib.load(f)
|
|
888
|
+
extends = (
|
|
889
|
+
feat_data.get("tool", {})
|
|
890
|
+
.get("splent", {})
|
|
891
|
+
.get("refinement", {})
|
|
892
|
+
.get("extends", {})
|
|
893
|
+
.get("models", [])
|
|
894
|
+
)
|
|
895
|
+
if not extends:
|
|
896
|
+
continue
|
|
897
|
+
# Parse the mixin's models.py
|
|
898
|
+
mixin_models = []
|
|
899
|
+
src_root = os.path.join(fpath, "src")
|
|
900
|
+
if os.path.isdir(src_root):
|
|
901
|
+
for org_dir in os.listdir(src_root):
|
|
902
|
+
mpath = os.path.join(src_root, org_dir, package, "models.py")
|
|
903
|
+
if os.path.isfile(mpath):
|
|
904
|
+
mixin_models = _parse_models(mpath)
|
|
905
|
+
break
|
|
906
|
+
# Merge each mixin into its target model
|
|
907
|
+
for ext in extends:
|
|
908
|
+
target_name = ext.get("target")
|
|
909
|
+
mixin_name = ext.get("mixin")
|
|
910
|
+
if not target_name or not mixin_name:
|
|
911
|
+
continue
|
|
912
|
+
# Find the mixin class in parsed mixin_models
|
|
913
|
+
mixin_cls = next(
|
|
914
|
+
(m for m in mixin_models if m["name"] == mixin_name), None
|
|
915
|
+
)
|
|
916
|
+
if not mixin_cls:
|
|
917
|
+
continue
|
|
918
|
+
# Find the target model across all parsed features
|
|
919
|
+
for feat_models in all_models.values():
|
|
920
|
+
for model in feat_models:
|
|
921
|
+
if model["name"] == target_name:
|
|
922
|
+
model["attributes"].extend(mixin_cls["attributes"])
|
|
923
|
+
model["methods"].extend(mixin_cls["methods"])
|
|
924
|
+
break
|
|
925
|
+
|
|
851
926
|
click.echo(
|
|
852
927
|
f"📋 Parsed models from {len(all_models)}/{len(feature_paths)} features."
|
|
853
928
|
)
|
|
@@ -237,10 +237,27 @@ def _extract_translations(translations_dir: Path) -> list[str]:
|
|
|
237
237
|
return locales
|
|
238
238
|
|
|
239
239
|
|
|
240
|
+
def _strip_docstrings_and_comments(text: str) -> str:
|
|
241
|
+
"""Remove triple-quoted strings (docstrings) and ``#`` comments.
|
|
242
|
+
|
|
243
|
+
Signal inference scans for ``define_signal("...")`` / ``connect_signal("...")``
|
|
244
|
+
calls. The scaffolded ``signals.py`` ships *examples* of those calls inside a
|
|
245
|
+
module docstring; without stripping them the inferred contract would list
|
|
246
|
+
example signal names the feature never actually emits (e.g. ``item-created``),
|
|
247
|
+
causing spurious collisions in ``feature:xray --validate``.
|
|
248
|
+
"""
|
|
249
|
+
# Drop triple-quoted blocks (both ''' and """).
|
|
250
|
+
text = re.sub(r'""".*?"""', "", text, flags=re.DOTALL)
|
|
251
|
+
text = re.sub(r"'''.*?'''", "", text, flags=re.DOTALL)
|
|
252
|
+
# Drop full-line and trailing comments.
|
|
253
|
+
text = re.sub(r"#.*", "", text)
|
|
254
|
+
return text
|
|
255
|
+
|
|
256
|
+
|
|
240
257
|
def _extract_signals(signals_path: Path) -> tuple[list[str], list[str]]:
|
|
241
258
|
if not signals_path.exists():
|
|
242
259
|
return [], []
|
|
243
|
-
text = signals_path.read_text()
|
|
260
|
+
text = _strip_docstrings_and_comments(signals_path.read_text())
|
|
244
261
|
provided = sorted(
|
|
245
262
|
set(re.findall(r"""define_signal\s*\(\s*['"]([^'"]+)['"]""", text))
|
|
246
263
|
)
|
|
@@ -273,14 +290,24 @@ def _scan_dependencies(
|
|
|
273
290
|
if f"splent_feature_{short_name}" != own_feature_name:
|
|
274
291
|
required_features.add(short_name)
|
|
275
292
|
|
|
276
|
-
for
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
293
|
+
for line in text.splitlines():
|
|
294
|
+
stripped = line.lstrip()
|
|
295
|
+
# Skip comments and docstrings
|
|
296
|
+
if (
|
|
297
|
+
stripped.startswith("#")
|
|
298
|
+
or stripped.startswith('"""')
|
|
299
|
+
or stripped.startswith("'''")
|
|
300
|
+
):
|
|
301
|
+
continue
|
|
302
|
+
for var in re.findall(
|
|
303
|
+
r"""os\.(?:getenv|environ\.get)\s*\(\s*['"]([A-Z][A-Z0-9_]+)['"]""",
|
|
304
|
+
stripped,
|
|
305
|
+
):
|
|
306
|
+
env_vars.add(var)
|
|
307
|
+
for var in re.findall(
|
|
308
|
+
r"""os\.environ\s*\[\s*['"]([A-Z][A-Z0-9_]+)['"]""", stripped
|
|
309
|
+
):
|
|
310
|
+
env_vars.add(var)
|
|
284
311
|
|
|
285
312
|
return sorted(required_features), sorted(env_vars)
|
|
286
313
|
|
|
@@ -330,12 +357,14 @@ def write_contract(pyproject_path: str, contract: dict, feature_name: str) -> No
|
|
|
330
357
|
text = path.read_text()
|
|
331
358
|
|
|
332
359
|
existing_description = f"{feature_name} feature"
|
|
360
|
+
existing_env = None
|
|
333
361
|
try:
|
|
334
362
|
data = tomllib.loads(text)
|
|
335
363
|
splent_contract = data.get("tool", {}).get("splent", {}).get("contract", {})
|
|
336
364
|
desc = splent_contract.get("description")
|
|
337
365
|
if desc:
|
|
338
366
|
existing_description = desc
|
|
367
|
+
existing_env = splent_contract.get("env")
|
|
339
368
|
except Exception:
|
|
340
369
|
pass
|
|
341
370
|
|
|
@@ -373,7 +402,8 @@ def write_contract(pyproject_path: str, contract: dict, feature_name: str) -> No
|
|
|
373
402
|
"# Do not edit manually — re-run `splent feature:contract --write` to refresh.\n"
|
|
374
403
|
"[tool.splent.contract]\n"
|
|
375
404
|
f'description = "{existing_description}"\n'
|
|
376
|
-
"\n"
|
|
405
|
+
+ (f'env = "{existing_env}"\n' if existing_env else "")
|
|
406
|
+
+ "\n"
|
|
377
407
|
"[tool.splent.contract.provides]\n"
|
|
378
408
|
f"routes = {_toml_list(contract['routes'])}\n"
|
|
379
409
|
f"blueprints = {_toml_list(contract['blueprints'])}\n"
|
|
@@ -478,16 +478,19 @@ def feature_xray(feature_ref, filter_cat, validate, full):
|
|
|
478
478
|
if not bp_collisions:
|
|
479
479
|
ok += 1
|
|
480
480
|
|
|
481
|
-
# 5. Commands:
|
|
482
|
-
|
|
481
|
+
# 5. Commands: feature-contributed commands are namespaced under
|
|
482
|
+
# ``feature:<name> <cmd>``, so the same command name in two different
|
|
483
|
+
# features (e.g. the scaffold's example ``hello``) never actually collides.
|
|
484
|
+
# We only flag a real duplicate *within* a single feature.
|
|
485
|
+
cmd_collisions = {}
|
|
483
486
|
for name, fdata in feature_data.items():
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
487
|
+
cmds = fdata["contract"].get("provides", {}).get("commands", [])
|
|
488
|
+
dupes = {c for c in cmds if cmds.count(c) > 1}
|
|
489
|
+
for c in dupes:
|
|
490
|
+
cmd_collisions.setdefault(f"{name}:{c}", []).append(name)
|
|
488
491
|
for cmd, features_list in cmd_collisions.items():
|
|
489
492
|
click.secho(
|
|
490
|
-
f" ❌ Command '{cmd}' —
|
|
493
|
+
f" ❌ Command '{cmd}' — duplicated within feature",
|
|
491
494
|
fg="red",
|
|
492
495
|
)
|
|
493
496
|
fail += 1
|
|
@@ -235,10 +235,10 @@ def product_docker(env_dev, env_prod):
|
|
|
235
235
|
seen.add(key)
|
|
236
236
|
unique.append((label, port, kind))
|
|
237
237
|
|
|
238
|
-
http_services = [(
|
|
239
|
-
tcp_services = [(
|
|
238
|
+
http_services = [(lbl, p) for lbl, p, k in unique if k == "http"]
|
|
239
|
+
tcp_services = [(lbl, p) for lbl, p, k in unique if k == "tcp"]
|
|
240
240
|
all_services = http_services + tcp_services
|
|
241
|
-
label_width = max((len(
|
|
241
|
+
label_width = max((len(lbl) for lbl, _ in all_services), default=20) + 2
|
|
242
242
|
|
|
243
243
|
if http_services:
|
|
244
244
|
click.secho(" Accessible services:", bold=True)
|
|
@@ -268,13 +268,17 @@ def product_deploy(down, ci):
|
|
|
268
268
|
import time
|
|
269
269
|
|
|
270
270
|
healthy = False
|
|
271
|
-
|
|
271
|
+
# The production web container is named ``<product>_web_deploy`` —
|
|
272
|
+
# NOT ``<product>_web`` (that is the dev container). A cold start runs
|
|
273
|
+
# migrations and compiles assets before gunicorn binds, so allow a
|
|
274
|
+
# generous window (~90s) before declaring the app unhealthy.
|
|
275
|
+
for attempt in range(45):
|
|
272
276
|
try:
|
|
273
277
|
result = subprocess.run(
|
|
274
278
|
[
|
|
275
279
|
"docker",
|
|
276
280
|
"exec",
|
|
277
|
-
f"{product}
|
|
281
|
+
f"{product}_web_deploy",
|
|
278
282
|
"bash",
|
|
279
283
|
"-c",
|
|
280
284
|
"curl -s -o /dev/null -w '%{http_code}' http://localhost:5000/",
|
|
@@ -172,7 +172,12 @@ def product_env(generate, merge, env_name, process_all):
|
|
|
172
172
|
)
|
|
173
173
|
raise SystemExit(1)
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
# Each environment writes to its own file so a `--prod` merge never
|
|
176
|
+
# clobbers the development `.env` (which the dev containers read via
|
|
177
|
+
# env_file). dev → .env, prod → .env.deploy.
|
|
178
|
+
target_env = os.path.join(
|
|
179
|
+
docker_dir, ".env.deploy" if env_name == "prod" else ".env"
|
|
180
|
+
)
|
|
176
181
|
if base_env != target_env:
|
|
177
182
|
shutil.copyfile(base_env, target_env)
|
|
178
183
|
|
|
@@ -74,6 +74,31 @@ def _scan_feature_deps(
|
|
|
74
74
|
) -> set[str]:
|
|
75
75
|
"""Scan a feature's source for dependencies. Returns set of short_names."""
|
|
76
76
|
deps = set()
|
|
77
|
+
|
|
78
|
+
# Refinement features declare the feature they refine in pyproject.toml
|
|
79
|
+
# ([tool.splent.refinement].refines). That is a hard dependency — the base
|
|
80
|
+
# feature must load first — so surface it as a constraint even when there is
|
|
81
|
+
# no Python import or FK between them.
|
|
82
|
+
pyproject_path = os.path.join(feature_path, "pyproject.toml")
|
|
83
|
+
if os.path.isfile(pyproject_path):
|
|
84
|
+
try:
|
|
85
|
+
import tomllib
|
|
86
|
+
|
|
87
|
+
with open(pyproject_path, "rb") as fh:
|
|
88
|
+
pydata = tomllib.load(fh)
|
|
89
|
+
refines = (
|
|
90
|
+
pydata.get("tool", {})
|
|
91
|
+
.get("splent", {})
|
|
92
|
+
.get("refinement", {})
|
|
93
|
+
.get("refines")
|
|
94
|
+
)
|
|
95
|
+
if refines and refines != feature_pkg and refines in all_packages:
|
|
96
|
+
short = pkg_to_short.get(refines)
|
|
97
|
+
if short:
|
|
98
|
+
deps.add(short)
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
|
|
77
102
|
src_dir = os.path.join(feature_path, "src", "splent_io", feature_pkg)
|
|
78
103
|
if not os.path.isdir(src_dir):
|
|
79
104
|
return deps
|
|
@@ -106,6 +106,56 @@ def run_preflight(*, interactive: bool = True, build_mode: bool = False) -> bool
|
|
|
106
106
|
|
|
107
107
|
failed = False
|
|
108
108
|
|
|
109
|
+
# Phase 0: pyproject sanity (duplicates, namespaces, SPL)
|
|
110
|
+
splent_cfg = data.get("tool", {}).get("splent", {})
|
|
111
|
+
base_feats = splent_cfg.get("features", [])
|
|
112
|
+
dev_feats = splent_cfg.get("features_dev", [])
|
|
113
|
+
prod_feats = splent_cfg.get("features_prod", [])
|
|
114
|
+
|
|
115
|
+
all_entries = base_feats + dev_feats + prod_feats
|
|
116
|
+
seen = {}
|
|
117
|
+
for entry in all_entries:
|
|
118
|
+
bare = (
|
|
119
|
+
entry.split("/")[-1].split("@")[0] if "/" in entry else entry.split("@")[0]
|
|
120
|
+
)
|
|
121
|
+
seen.setdefault(bare, []).append(entry)
|
|
122
|
+
|
|
123
|
+
duplicates = {k: v for k, v in seen.items() if len(v) > 1}
|
|
124
|
+
if duplicates:
|
|
125
|
+
if interactive:
|
|
126
|
+
for bare, entries in duplicates.items():
|
|
127
|
+
short = bare.replace("splent_feature_", "")
|
|
128
|
+
click.secho(
|
|
129
|
+
f" pyproject duplicate '{short}': {', '.join(entries)}", fg="red"
|
|
130
|
+
)
|
|
131
|
+
failed = True
|
|
132
|
+
else:
|
|
133
|
+
namespaces = set()
|
|
134
|
+
for entry in all_entries:
|
|
135
|
+
if "/" in entry:
|
|
136
|
+
namespaces.add(entry.split("/")[0])
|
|
137
|
+
if len(namespaces) > 1:
|
|
138
|
+
if interactive:
|
|
139
|
+
click.secho(
|
|
140
|
+
f" pyproject inconsistent namespaces: {', '.join(sorted(namespaces))}",
|
|
141
|
+
fg="yellow",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
spl_name = splent_cfg.get("spl")
|
|
145
|
+
if spl_name:
|
|
146
|
+
uvl_check = os.path.join(
|
|
147
|
+
workspace, "splent_catalog", spl_name, f"{spl_name}.uvl"
|
|
148
|
+
)
|
|
149
|
+
if not os.path.isfile(uvl_check):
|
|
150
|
+
if interactive:
|
|
151
|
+
click.secho(
|
|
152
|
+
f" pyproject SPL '{spl_name}' — UVL not found in catalog", fg="red"
|
|
153
|
+
)
|
|
154
|
+
failed = True
|
|
155
|
+
|
|
156
|
+
if failed:
|
|
157
|
+
return False
|
|
158
|
+
|
|
109
159
|
# Phase 1: SAT
|
|
110
160
|
try:
|
|
111
161
|
sat_ok, selected, _, _ = _run_sat_check(workspace, app_name, data, None, False)
|
|
@@ -260,17 +260,39 @@ def build_and_upload_pypi(path: str):
|
|
|
260
260
|
raise SystemExit(1)
|
|
261
261
|
|
|
262
262
|
click.echo(" pypi uploading...")
|
|
263
|
+
|
|
264
|
+
# Expand dist/* ourselves: subprocess runs without a shell, so a literal
|
|
265
|
+
# "dist/*" would be passed verbatim to twine and never match any file.
|
|
266
|
+
import glob
|
|
267
|
+
|
|
268
|
+
dist_files = sorted(glob.glob(os.path.join(path, "dist", "*")))
|
|
269
|
+
if not dist_files:
|
|
270
|
+
click.secho(" error: no built artifacts found in dist/", fg="red")
|
|
271
|
+
raise SystemExit(1)
|
|
272
|
+
|
|
273
|
+
# twine reads TWINE_USERNAME / TWINE_PASSWORD from the environment. The CLI
|
|
274
|
+
# loads them from the workspace .env (load_dotenv), so they are already in
|
|
275
|
+
# os.environ — pass it through explicitly. --non-interactive makes twine fail
|
|
276
|
+
# loudly instead of blocking on a credentials prompt; --skip-existing keeps
|
|
277
|
+
# re-runs idempotent (a half-uploaded release can be retried safely).
|
|
263
278
|
try:
|
|
264
279
|
subprocess.run(
|
|
265
|
-
[
|
|
280
|
+
[
|
|
281
|
+
sys.executable,
|
|
282
|
+
"-m",
|
|
283
|
+
"twine",
|
|
284
|
+
"upload",
|
|
285
|
+
"--non-interactive",
|
|
286
|
+
"--skip-existing",
|
|
287
|
+
*dist_files,
|
|
288
|
+
],
|
|
266
289
|
env=os.environ.copy(),
|
|
267
290
|
check=True,
|
|
268
291
|
cwd=path,
|
|
269
|
-
capture_output=True,
|
|
270
292
|
)
|
|
271
293
|
except subprocess.CalledProcessError as e:
|
|
272
294
|
click.secho(
|
|
273
|
-
f" error: PyPI upload failed
|
|
295
|
+
f" error: PyPI upload failed (exit {e.returncode}).",
|
|
274
296
|
fg="red",
|
|
275
297
|
)
|
|
276
298
|
click.secho(
|