splent-cli 1.40.0__tar.gz → 1.41.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.40.0/src/splent_cli.egg-info → splent_cli-1.41.0}/PKG-INFO +1 -1
- {splent_cli-1.40.0 → splent_cli-1.41.0}/pyproject.toml +1 -1
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_status.py +18 -3
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_create.py +2 -1
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_test.py +115 -21
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_auto_require.py +24 -1
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_build.py +35 -10
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_configure.py +118 -6
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_deploy.py +8 -6
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_missing.py +26 -5
- splent_cli-1.41.0/src/splent_cli/commands/product/product_params.py +231 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_validate.py +3 -11
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/uvl/uvl_utils.py +27 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/neutral_app.py +107 -9
- splent_cli-1.41.0/src/splent_cli/templates/feature/feature_services_service.py.j2 +8 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +7 -0
- splent_cli-1.41.0/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +78 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +7 -3
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/feature_params.py +89 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/uvl_params.py +31 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli.egg-info/SOURCES.txt +2 -0
- splent_cli-1.40.0/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -38
- {splent_cli-1.40.0 → splent_cli-1.41.0}/COPYING +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/COPYING.LESSER +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/README.md +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/setup.cfg +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/cli.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_deps.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_env.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_features.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_infra.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_product.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_releases.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/check/check_vendor.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/clear/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/command/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/command/command_create.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/coverage.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_console.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_dump.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_reset.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_restore.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_seed.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/doctor.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/env/env_set.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/export/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/export/export_puml.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_release.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/lint.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/locust.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/marketplace/login.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_clean.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_commands.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_config.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_console.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_containers.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_create.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_derive.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_down.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_drift.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_env.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_port.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_release.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_restart.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_routes.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_run.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_select.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_signals.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_test.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_up.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/release/release_core.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/release/release_resume.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/selenium.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/tokens_setup.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/version.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/compose.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/context.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/credentials.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/deploy_bundle.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/image_registry.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/index_refresh.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/marketplace.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/marketplace_api.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/marketplace_url.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/preflight.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/pypi_quota.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/registry.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/release.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/release_dry_run.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/release_gate.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/spl_store.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/services/web_container.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/licenses/COPYING +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_400.html +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_401.html +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_404.html +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_500.html +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_app.css +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_app.js +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_own.css +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/__init__.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/archetype.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/cache_utils.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/ci_services.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/ci_workflow.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/command_loader.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/contract_freshness.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/db_client.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/feature_installer.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/feature_naming.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/feature_requirements.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/feature_resolution.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/feature_utils.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/forge.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/git_url.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/integrity.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/io_utils.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/lifecycle.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/manifest.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/menu_keys.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/migration_lock.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/proc.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/redact.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/refinement_facts.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/running_version.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/template_drift.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/toml_edit.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli.egg-info/requires.txt +0 -0
- {splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli.egg-info/top_level.txt +0 -0
|
@@ -58,9 +58,22 @@ def _get_filesystem_head(mdir: str) -> str | None:
|
|
|
58
58
|
"db:status",
|
|
59
59
|
short_help="Show migration status for all features.",
|
|
60
60
|
)
|
|
61
|
+
@click.option(
|
|
62
|
+
"--check",
|
|
63
|
+
is_flag=True,
|
|
64
|
+
help="Exit 1 when a feature's schema is not at the head of its migrations "
|
|
65
|
+
"(pending, not applied, or applied from scripts that are gone).",
|
|
66
|
+
)
|
|
61
67
|
@context.requires_product
|
|
62
|
-
def db_status():
|
|
63
|
-
"""Show migration status: DB revision vs filesystem head for each feature.
|
|
68
|
+
def db_status(check):
|
|
69
|
+
"""Show migration status: DB revision vs filesystem head for each feature.
|
|
70
|
+
|
|
71
|
+
\b
|
|
72
|
+
--check is for a start that must not serve on a schema older than its
|
|
73
|
+
code: the product's entrypoint runs it with SPLENT_MIGRATE_ON_START=check,
|
|
74
|
+
when a deploy agent migrates in a step of its own. An orphan (a feature
|
|
75
|
+
no longer declared) does not count.
|
|
76
|
+
"""
|
|
64
77
|
app = current_app
|
|
65
78
|
|
|
66
79
|
click.echo(click.style("\n📊 Migration Status\n", fg="cyan", bold=True))
|
|
@@ -151,7 +164,7 @@ def db_status():
|
|
|
151
164
|
if issues:
|
|
152
165
|
click.secho(
|
|
153
166
|
f" {issues} feature(s) out of sync. Run 'splent db:upgrade' to apply pending migrations.",
|
|
154
|
-
fg="yellow",
|
|
167
|
+
fg="red" if check else "yellow",
|
|
155
168
|
)
|
|
156
169
|
click.echo()
|
|
157
170
|
if orphans:
|
|
@@ -164,6 +177,8 @@ def db_status():
|
|
|
164
177
|
fg="red",
|
|
165
178
|
)
|
|
166
179
|
click.echo()
|
|
180
|
+
if check and issues:
|
|
181
|
+
raise SystemExit(1)
|
|
167
182
|
|
|
168
183
|
|
|
169
184
|
cli_command = db_status
|
|
@@ -276,7 +276,8 @@ def _build_file_map(feature_type, short_name, docker=False, js=False):
|
|
|
276
276
|
src = {
|
|
277
277
|
"__init__.py": f"{T}feature_init_service.py.j2",
|
|
278
278
|
"config.py": f"{T}feature_config.py.j2",
|
|
279
|
-
|
|
279
|
+
# No models, so no repository for the service to wrap.
|
|
280
|
+
"services.py": f"{T}feature_services_service.py.j2",
|
|
280
281
|
"signals.py": f"{T}feature_signals.py.j2",
|
|
281
282
|
"commands.py": f"{T}feature_commands.py.j2",
|
|
282
283
|
}
|
|
@@ -46,12 +46,15 @@ def _tied_to_the_cli(command: str, lock: str = TEST_LOCK) -> str:
|
|
|
46
46
|
The CLI holds the exec's stdin open while it runs (see
|
|
47
47
|
_delegate_to_web_container). When the CLI goes, stdin closes, and the
|
|
48
48
|
watcher stops the whole run. A run that finds another one holding the
|
|
49
|
-
lock waits for it instead of running beside it.
|
|
49
|
+
lock waits for it instead of running beside it. The lock is opened for
|
|
50
|
+
reading, which flock needs no more than: a run as the workspace's user
|
|
51
|
+
shares the file a run as root created.
|
|
50
52
|
"""
|
|
51
53
|
return f"""set -m
|
|
52
54
|
exec 3<&0
|
|
53
55
|
if command -v flock >/dev/null 2>&1; then
|
|
54
|
-
|
|
56
|
+
[ -e {lock} ] || : > {lock}
|
|
57
|
+
exec 9<{lock}
|
|
55
58
|
if ! flock -n 9; then
|
|
56
59
|
echo " Another test run is using this product's test database. Waiting for it to finish..."
|
|
57
60
|
flock 9
|
|
@@ -84,13 +87,78 @@ def _delegate_to_web_container(argv: list[str]) -> None:
|
|
|
84
87
|
)
|
|
85
88
|
|
|
86
89
|
|
|
90
|
+
def workspace_user(container: str) -> list[str]:
|
|
91
|
+
"""The docker exec options that run a command in CONTAINER as the user
|
|
92
|
+
who owns the workspace.
|
|
93
|
+
|
|
94
|
+
An exec runs as the image's user, root in a development image, and a
|
|
95
|
+
test run writes into the working copies: bytecode, .pytest_cache, what
|
|
96
|
+
the tests leave. As root they stayed root's, and the next release of
|
|
97
|
+
the feature could not rewrite them. The owner is read from the
|
|
98
|
+
workspace's files, whoever this process runs as. A development image
|
|
99
|
+
has no account for that uid as a rule; Docker runs a bare uid all the
|
|
100
|
+
same, and the exec then names it in USER and LOGNAME, which
|
|
101
|
+
getpass.getuser() reads before the account it does not find (pymysql
|
|
102
|
+
asks it), with HOME a directory of its own under /tmp. A workspace root
|
|
103
|
+
owns stays root's, and so does a container where the account cannot be
|
|
104
|
+
looked up.
|
|
105
|
+
"""
|
|
106
|
+
owner = _workspace_owner()
|
|
107
|
+
if owner is None or owner[0] == 0:
|
|
108
|
+
return []
|
|
109
|
+
uid, gid = owner
|
|
110
|
+
found = run(
|
|
111
|
+
["docker", "exec", container, "getent", "passwd", str(uid)],
|
|
112
|
+
check=False,
|
|
113
|
+
capture=True,
|
|
114
|
+
)
|
|
115
|
+
if found.returncode not in (0, 2):
|
|
116
|
+
# 2 is getent's "no such entry"; anything else is a container that
|
|
117
|
+
# cannot answer, where the exec keeps the image's user.
|
|
118
|
+
return []
|
|
119
|
+
options = ["-u", f"{uid}:{gid}"]
|
|
120
|
+
fields = (found.stdout or "").strip().split(":")
|
|
121
|
+
if found.returncode != 0 or len(fields) < 6 or not fields[5]:
|
|
122
|
+
user = _account_name(uid)
|
|
123
|
+
options += [
|
|
124
|
+
"-e",
|
|
125
|
+
f"HOME=/tmp/splent-home-{uid}",
|
|
126
|
+
"-e",
|
|
127
|
+
f"USER={user}",
|
|
128
|
+
"-e",
|
|
129
|
+
f"LOGNAME={user}",
|
|
130
|
+
]
|
|
131
|
+
return options
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _account_name(uid: int) -> str:
|
|
135
|
+
"""The name this machine gives UID, ``splent`` when it gives none."""
|
|
136
|
+
import pwd
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
return pwd.getpwuid(uid).pw_name
|
|
140
|
+
except KeyError:
|
|
141
|
+
return "splent"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _workspace_owner() -> tuple[int, int] | None:
|
|
145
|
+
"""``(uid, gid)`` of the workspace's root directory, or None."""
|
|
146
|
+
try:
|
|
147
|
+
owner = context.workspace().stat()
|
|
148
|
+
except OSError:
|
|
149
|
+
return None
|
|
150
|
+
return owner.st_uid, owner.st_gid
|
|
151
|
+
|
|
152
|
+
|
|
87
153
|
def run_tests_in_web_container(product: str, argv: list[str]) -> int:
|
|
88
154
|
"""Run ``splent <argv>`` in PRODUCT's web container, as a test run.
|
|
89
155
|
|
|
90
156
|
Returns its exit code, or 1 when the container is not running. The run
|
|
91
157
|
waits for any other test run in that container and stops when this
|
|
92
|
-
process does (see :func:`_tied_to_the_cli`)
|
|
93
|
-
this way, and so
|
|
158
|
+
process does (see :func:`_tied_to_the_cli`), and runs as the workspace's
|
|
159
|
+
owner (:func:`workspace_user`). feature:test runs there this way, and so
|
|
160
|
+
do the product's own tests (product:test) and the release gate, which
|
|
161
|
+
runs feature:test.
|
|
94
162
|
"""
|
|
95
163
|
name = web_container.name(product)
|
|
96
164
|
if not _container_running(name):
|
|
@@ -121,6 +189,7 @@ def run_tests_in_web_container(product: str, argv: list[str]) -> int:
|
|
|
121
189
|
"docker",
|
|
122
190
|
"exec",
|
|
123
191
|
"-i",
|
|
192
|
+
*workspace_user(name),
|
|
124
193
|
"-e",
|
|
125
194
|
f"SPLENT_APP={product}",
|
|
126
195
|
*passthrough,
|
|
@@ -694,15 +763,19 @@ def _run_neutral(
|
|
|
694
763
|
problem = neutral_app.database_problem(dict(os.environ))
|
|
695
764
|
if problem:
|
|
696
765
|
_neutral_refusal(problem)
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
766
|
+
product = context.require_app()
|
|
767
|
+
unmet = neutral_app.unmet_requirements(members)
|
|
768
|
+
from_workspace = [str(u) for u in unmet if not u.installable]
|
|
769
|
+
if from_workspace:
|
|
770
|
+
# The framework, the CLI and the features are the workspace's
|
|
771
|
+
# copies, which a run never replaces with a release.
|
|
700
772
|
_neutral_refusal(
|
|
701
|
-
f"The neutral app needs packages that {product}'s
|
|
702
|
-
" have at the version asked for:\n"
|
|
703
|
-
+ "\n".join(f" {line}" for line in
|
|
704
|
-
+ "\n
|
|
705
|
-
" that declares the
|
|
773
|
+
f"The neutral app needs SPLENT packages that {product}'s container\n"
|
|
774
|
+
" does not have at the version asked for:\n"
|
|
775
|
+
+ "\n".join(f" {line}" for line in from_workspace)
|
|
776
|
+
+ "\n The container has the workspace's copies of them. Update the\n"
|
|
777
|
+
" working copy, or run the tests in a product that declares the\n"
|
|
778
|
+
" feature."
|
|
706
779
|
)
|
|
707
780
|
|
|
708
781
|
test_paths = _collect_test_paths([(feature.name, feature.root)])
|
|
@@ -710,15 +783,33 @@ def _run_neutral(
|
|
|
710
783
|
click.secho("⚠️ No test directories found.", fg="yellow")
|
|
711
784
|
return
|
|
712
785
|
|
|
713
|
-
click.secho(
|
|
714
|
-
f"\n🧪 Running tests for {feature_ref} in a neutral app "
|
|
715
|
-
f"[{', '.join(levels)}]...",
|
|
716
|
-
fg="cyan",
|
|
717
|
-
)
|
|
718
786
|
with tempfile.TemporaryDirectory(prefix="splent-neutral-") as tmp:
|
|
787
|
+
venv_dir = None
|
|
788
|
+
if unmet:
|
|
789
|
+
click.echo(
|
|
790
|
+
f" {product}'s image lacks what these need, so the run installs "
|
|
791
|
+
"it in a\n virtual environment of its own, removed after it:"
|
|
792
|
+
)
|
|
793
|
+
for line in unmet:
|
|
794
|
+
click.echo(f" {line}")
|
|
795
|
+
try:
|
|
796
|
+
venv_dir = neutral_app.install_requirements(Path(tmp), unmet)
|
|
797
|
+
except neutral_app.NeutralAppError as e:
|
|
798
|
+
_neutral_refusal(
|
|
799
|
+
f"{e}\n Install them in the container, or run the tests "
|
|
800
|
+
"in a product\n that declares the feature."
|
|
801
|
+
)
|
|
802
|
+
click.secho(
|
|
803
|
+
f"\n🧪 Running tests for {feature_ref} in a neutral app "
|
|
804
|
+
f"[{', '.join(levels)}]...",
|
|
805
|
+
fg="cyan",
|
|
806
|
+
)
|
|
719
807
|
neutral_app.write_app(Path(tmp), members)
|
|
720
|
-
env = neutral_app.environment(Path(tmp), members)
|
|
721
|
-
|
|
808
|
+
env = neutral_app.environment(Path(tmp), members, venv_dir=venv_dir)
|
|
809
|
+
python = str(venv_dir / "bin" / "python") if venv_dir else None
|
|
810
|
+
_run_pytest(
|
|
811
|
+
test_paths, levels, keyword, verbose, environment=env, python=python
|
|
812
|
+
)
|
|
722
813
|
|
|
723
814
|
|
|
724
815
|
def _resolve_levels(unit, integration, functional, e2e, load) -> tuple[str, ...]:
|
|
@@ -744,12 +835,15 @@ def _run_pytest(
|
|
|
744
835
|
verbose: bool,
|
|
745
836
|
extra_sources: list[tuple[str, Path]] = (),
|
|
746
837
|
environment: dict | None = None,
|
|
838
|
+
python: str | None = None,
|
|
747
839
|
):
|
|
748
840
|
"""Run each feature's tests, level by level, and exit 1 if any failed.
|
|
749
841
|
|
|
750
842
|
ENVIRONMENT, when given, is the whole environment of every run (the
|
|
751
843
|
neutral app's). Otherwise each run gets this process's, with the import
|
|
752
|
-
path and the feature sources of the active product.
|
|
844
|
+
path and the feature sources of the active product. PYTHON, when given,
|
|
845
|
+
is the interpreter that runs pytest (the neutral app's virtual
|
|
846
|
+
environment).
|
|
753
847
|
"""
|
|
754
848
|
passed = failed = skipped = 0
|
|
755
849
|
for pkg, test_dir, src_dir in test_paths:
|
|
@@ -790,7 +884,7 @@ def _run_pytest(
|
|
|
790
884
|
)
|
|
791
885
|
|
|
792
886
|
cmd = [
|
|
793
|
-
"pytest",
|
|
887
|
+
*([python, "-m", "pytest"] if python else ["pytest"]),
|
|
794
888
|
str(level_dir),
|
|
795
889
|
"--rootdir=.",
|
|
796
890
|
"-W",
|
{splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_auto_require.py
RENAMED
|
@@ -15,6 +15,8 @@ from splent_cli.commands.uvl.uvl_utils import (
|
|
|
15
15
|
list_all_features_from_uvl as _list_all_features_from_uvl,
|
|
16
16
|
extract_implications_from_uvl_text as _extract_implications_from_uvl_text,
|
|
17
17
|
print_uvl_header as _print_uvl_header,
|
|
18
|
+
parameter_nodes_of as _parameter_nodes_of,
|
|
19
|
+
parameters_to_set as _parameters_to_set,
|
|
18
20
|
)
|
|
19
21
|
from splent_cli.utils.ci_workflow import keeps_ci_workflows
|
|
20
22
|
|
|
@@ -172,10 +174,17 @@ def product_complete(pyproject, default_org, yes, dry_run):
|
|
|
172
174
|
|
|
173
175
|
# current selection from pyproject: the one product:validate checks, the
|
|
174
176
|
# base list plus the list of the current environment (dev when unset)
|
|
175
|
-
|
|
177
|
+
env = context.resolve_env()
|
|
178
|
+
deps = read_features_from_data(data, env)
|
|
176
179
|
selected_names = {_normalize_feature_name(d) for d in deps}
|
|
177
180
|
selected_names.add(root_name) # root always selected
|
|
178
181
|
|
|
182
|
+
# The nodes the product's values select count as selected (P7), and a
|
|
183
|
+
# parameter node a constraint asks for is a value to set in
|
|
184
|
+
# [tool.splent.config]: it has no package, and is never added.
|
|
185
|
+
nodes, chosen = _parameter_nodes_of(workspace, data, local_uvl, selected_names, env)
|
|
186
|
+
selected_names |= set(chosen)
|
|
187
|
+
|
|
179
188
|
# compute missing via implications
|
|
180
189
|
pairs = _extract_implications_from_uvl_text(uvl_text)
|
|
181
190
|
req_graph = _build_req_graph(pairs)
|
|
@@ -185,6 +194,18 @@ def product_complete(pyproject, default_org, yes, dry_run):
|
|
|
185
194
|
|
|
186
195
|
# filter out root just in case
|
|
187
196
|
missing_names = [f for f in missing_names if f != root_name]
|
|
197
|
+
missing_values = [f for f in missing_names if f in nodes]
|
|
198
|
+
missing_names = [f for f in missing_names if f not in nodes]
|
|
199
|
+
|
|
200
|
+
def say_values():
|
|
201
|
+
if missing_values:
|
|
202
|
+
to_set = _parameters_to_set(missing_values, nodes, workspace, data, env)
|
|
203
|
+
click.secho(
|
|
204
|
+
"Not added, values to set in [tool.splent.config]: "
|
|
205
|
+
+ ", ".join(to_set),
|
|
206
|
+
fg="yellow",
|
|
207
|
+
)
|
|
208
|
+
click.echo()
|
|
188
209
|
|
|
189
210
|
# sanity
|
|
190
211
|
unknown_missing = sorted([f for f in missing_names if f not in universe_set])
|
|
@@ -195,6 +216,7 @@ def product_complete(pyproject, default_org, yes, dry_run):
|
|
|
195
216
|
|
|
196
217
|
if not missing_names:
|
|
197
218
|
_print_uvl_header("complete", app_name, local_uvl, len(universe))
|
|
219
|
+
say_values()
|
|
198
220
|
click.echo("OK: nothing to add (no missing dependencies).")
|
|
199
221
|
click.echo()
|
|
200
222
|
return
|
|
@@ -213,6 +235,7 @@ def product_complete(pyproject, default_org, yes, dry_run):
|
|
|
213
235
|
to_add_specs.append(spec)
|
|
214
236
|
|
|
215
237
|
_print_uvl_header("complete", app_name, local_uvl, len(universe))
|
|
238
|
+
say_values()
|
|
216
239
|
click.echo("Missing features (by name):")
|
|
217
240
|
for f in missing_names:
|
|
218
241
|
click.echo(f"- {f}")
|
|
@@ -824,16 +824,34 @@ def _reset_features_dir(docker_path: str) -> None:
|
|
|
824
824
|
# with a one-shot service must declare restart: "no", quoted.
|
|
825
825
|
RESTART_POLICY = "unless-stopped"
|
|
826
826
|
|
|
827
|
-
# The web container's health probe
|
|
828
|
-
# production image has, so the probe
|
|
829
|
-
#
|
|
830
|
-
#
|
|
831
|
-
# the
|
|
827
|
+
# The web container's health probe, which product:deploy waits on too.
|
|
828
|
+
# Python is the one interpreter every production image has, so the probe
|
|
829
|
+
# needs no extra package (curl is not guaranteed). It asks the framework's
|
|
830
|
+
# /readyz (splent_framework 1.21 and later): 200 when the database and the
|
|
831
|
+
# services the product declares answer, 503 when they do not. The framework
|
|
832
|
+
# answers it before the app's own code runs, so TRUSTED_HOSTS never sees
|
|
833
|
+
# it. "/" did not do: with TRUSTED_HOSTS set it answers 400 to a request
|
|
834
|
+
# for 127.0.0.1, and a broken database could still serve a cached page.
|
|
835
|
+
# Any other answer at /readyz comes from the app's own routes: a framework
|
|
836
|
+
# older than 1.21, or a product that turned the probe off. That app is
|
|
837
|
+
# healthy when "/" answers below 500, the probe it had before.
|
|
832
838
|
WEB_HEALTH_PROBE = (
|
|
839
|
+
"import http.client as h\n"
|
|
840
|
+
"def status(path):\n"
|
|
841
|
+
" c = h.HTTPConnection('127.0.0.1', 5000, timeout=4)\n"
|
|
842
|
+
" c.request('GET', path)\n"
|
|
843
|
+
" return c.getresponse().status\n"
|
|
844
|
+
"ready = status('/readyz')\n"
|
|
845
|
+
"ok = ready == 200 if ready in (200, 503) else status('/') < 500\n"
|
|
846
|
+
"raise SystemExit(not ok)\n"
|
|
847
|
+
)
|
|
848
|
+
# The probe CLIs before 1.41 wrote, which product:build replaces with the
|
|
849
|
+
# one above: a product created with them has it in its prod compose file.
|
|
850
|
+
LEGACY_WEB_HEALTH_PROBES = (
|
|
833
851
|
"import http.client as h; "
|
|
834
852
|
"c = h.HTTPConnection('127.0.0.1', 5000, timeout=4); "
|
|
835
853
|
"c.request('GET', '/'); "
|
|
836
|
-
"raise SystemExit(c.getresponse().status >= 500)"
|
|
854
|
+
"raise SystemExit(c.getresponse().status >= 500)",
|
|
837
855
|
)
|
|
838
856
|
WEB_HEALTHCHECK = {
|
|
839
857
|
"test": ["CMD", "python3", "-c", WEB_HEALTH_PROBE],
|
|
@@ -906,9 +924,11 @@ def local_or_registry_image(product: str) -> str:
|
|
|
906
924
|
|
|
907
925
|
def apply_production_policies(compose_data: dict, product: str) -> dict:
|
|
908
926
|
"""Give every deployed service a restart policy, and the web and database
|
|
909
|
-
services a healthcheck, unless the compose file already set one.
|
|
910
|
-
|
|
911
|
-
|
|
927
|
+
services a healthcheck, unless the compose file already set one. A web
|
|
928
|
+
healthcheck that is the probe an older CLI wrote (it asked "/") is
|
|
929
|
+
brought up to WEB_HEALTH_PROBE. The web image becomes
|
|
930
|
+
``${SPLENT_IMAGE:-<product>:latest}``, so a host can run the image a
|
|
931
|
+
release published instead of building its own.
|
|
912
932
|
|
|
913
933
|
Applied by product:build to the merged deploy compose, so every product
|
|
914
934
|
gets it on its next build, whatever its docker-compose.prod.yml says and
|
|
@@ -924,7 +944,12 @@ def apply_production_policies(compose_data: dict, product: str) -> dict:
|
|
|
924
944
|
None,
|
|
925
945
|
)
|
|
926
946
|
if web_name and isinstance(services[web_name], dict):
|
|
927
|
-
services[web_name].setdefault(
|
|
947
|
+
healthcheck = services[web_name].setdefault(
|
|
948
|
+
"healthcheck", dict(WEB_HEALTHCHECK)
|
|
949
|
+
)
|
|
950
|
+
test = healthcheck.get("test") if isinstance(healthcheck, dict) else None
|
|
951
|
+
if isinstance(test, list) and test and test[-1] in LEGACY_WEB_HEALTH_PROBES:
|
|
952
|
+
healthcheck["test"] = list(WEB_HEALTHCHECK["test"])
|
|
928
953
|
# The local build, unless deploy/.env names a registry image. Only the
|
|
929
954
|
# plain local name is rewritten: an image the product chose on purpose
|
|
930
955
|
# is left alone.
|
{splent_cli-1.40.0 → splent_cli-1.41.0}/src/splent_cli/commands/product/product_configure.py
RENAMED
|
@@ -26,9 +26,11 @@ from dataclasses import dataclass, field
|
|
|
26
26
|
|
|
27
27
|
import click
|
|
28
28
|
|
|
29
|
+
from splent_cli.commands.uvl.uvl_utils import ModelTree
|
|
29
30
|
from splent_cli.services import context
|
|
30
31
|
from splent_cli.utils.feature_utils import normalize_namespace
|
|
31
32
|
from splent_cli.utils.ci_workflow import keeps_ci_workflows
|
|
33
|
+
from splent_cli.utils.uvl_params import PARAM, VALUE
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
# ═══════════════════════════════════════════════════════════════════
|
|
@@ -47,11 +49,19 @@ class SPLFeature:
|
|
|
47
49
|
groups: list[SPLGroup] = field(default_factory=list)
|
|
48
50
|
card_min: int = 1 # feature cardinality lower bound
|
|
49
51
|
card_max: int = 1 # feature cardinality upper bound
|
|
52
|
+
attributes: dict[str, str] = field(default_factory=dict)
|
|
50
53
|
|
|
51
54
|
@property
|
|
52
55
|
def is_abstract(self) -> bool:
|
|
53
56
|
return not self.package
|
|
54
57
|
|
|
58
|
+
@property
|
|
59
|
+
def is_parameter(self) -> bool:
|
|
60
|
+
"""Whether the node stands for a parameter or one of its values (P7,
|
|
61
|
+
splent_cli.utils.uvl_params): the product's [tool.splent.config]
|
|
62
|
+
selects it, and it is never offered as a feature."""
|
|
63
|
+
return bool({PARAM, VALUE} & set(self.attributes))
|
|
64
|
+
|
|
55
65
|
|
|
56
66
|
@dataclass
|
|
57
67
|
class SPLGroup:
|
|
@@ -157,6 +167,18 @@ class SPLModel:
|
|
|
157
167
|
stack.append(ch)
|
|
158
168
|
return result
|
|
159
169
|
|
|
170
|
+
def is_parameter(self, name: str) -> bool:
|
|
171
|
+
f = self.features.get(name)
|
|
172
|
+
return bool(f and f.is_parameter)
|
|
173
|
+
|
|
174
|
+
def tree(self) -> ModelTree:
|
|
175
|
+
"""The model as uvl_params reads it: parents and attributes."""
|
|
176
|
+
return ModelTree(
|
|
177
|
+
root=self.root_name,
|
|
178
|
+
parents={n: f.parent for n, f in self.features.items() if f.parent},
|
|
179
|
+
attributes={n: dict(f.attributes) for n, f in self.features.items()},
|
|
180
|
+
)
|
|
181
|
+
|
|
160
182
|
def groups_needing_selection(self) -> list[tuple[str, SPLGroup]]:
|
|
161
183
|
"""Return (parent_name, group) pairs that require interactive selection.
|
|
162
184
|
|
|
@@ -305,6 +327,7 @@ def _load_spl_model(uvl_path: str, spl_name: str) -> SPLModel:
|
|
|
305
327
|
org=attrs.get("org", "splent-io"),
|
|
306
328
|
package=attrs.get("package", ""),
|
|
307
329
|
parent=parent_name,
|
|
330
|
+
attributes=attrs,
|
|
308
331
|
)
|
|
309
332
|
model.features[node.name] = feat
|
|
310
333
|
|
|
@@ -376,6 +399,10 @@ def propagate(
|
|
|
376
399
|
Backward:
|
|
377
400
|
4. A selected ∧ (A excludes B) → B excluded
|
|
378
401
|
5. B excluded ∧ (A ⇒ B) → A excluded (contrapositive)
|
|
402
|
+
|
|
403
|
+
A parameter node B is never selected by rule 1: only a value of the
|
|
404
|
+
parameter selects it (:func:`_parameter_selection`), and the solver
|
|
405
|
+
says when the values do not.
|
|
379
406
|
"""
|
|
380
407
|
result = set(selected) | mandatory
|
|
381
408
|
excl = set(excluded) if excluded else set()
|
|
@@ -386,8 +413,8 @@ def propagate(
|
|
|
386
413
|
# Forward implication
|
|
387
414
|
for c in model.constraints:
|
|
388
415
|
if c.kind == "implies" and c.source in result and c.target not in result:
|
|
389
|
-
if c.target in excl:
|
|
390
|
-
continue # conflict — SAT validation will catch
|
|
416
|
+
if c.target in excl or model.is_parameter(c.target):
|
|
417
|
+
continue # conflict, or a value to set — SAT validation will catch
|
|
391
418
|
result.add(c.target)
|
|
392
419
|
changed = True
|
|
393
420
|
|
|
@@ -649,7 +676,9 @@ def _propagate_and_report(
|
|
|
649
676
|
"""Run propagation, update sets in-place, and report changes."""
|
|
650
677
|
new_sel, new_excl = propagate(selected, model, mandatory, excluded)
|
|
651
678
|
|
|
652
|
-
|
|
679
|
+
# A parameter's node (one that holds its choices, as a mandatory child)
|
|
680
|
+
# is not news: its value is the product's to set.
|
|
681
|
+
auto = {n for n in new_sel - selected - mandatory if not model.is_parameter(n)}
|
|
653
682
|
for name in sorted(auto):
|
|
654
683
|
click.echo(
|
|
655
684
|
f"{prefix}\u21b3 {name}" + click.style(" (auto-selected)", fg="cyan")
|
|
@@ -708,6 +737,9 @@ def _configure_subtree(
|
|
|
708
737
|
# Only prompt if this feature is active
|
|
709
738
|
if name not in selected and name not in mandatory:
|
|
710
739
|
continue
|
|
740
|
+
# The values of a parameter: [tool.splent.config] picks them.
|
|
741
|
+
if all(model.is_parameter(ch) for ch in group.children):
|
|
742
|
+
continue
|
|
711
743
|
|
|
712
744
|
label_map = {
|
|
713
745
|
"alternative": "pick exactly one",
|
|
@@ -761,7 +793,10 @@ def _configure_subtree(
|
|
|
761
793
|
has_optionals = any(
|
|
762
794
|
g.group_type == "optional"
|
|
763
795
|
for g in feat.groups
|
|
764
|
-
if any(
|
|
796
|
+
if any(
|
|
797
|
+
ch not in mandatory and ch not in selected and not model.is_parameter(ch)
|
|
798
|
+
for ch in g.children
|
|
799
|
+
)
|
|
765
800
|
)
|
|
766
801
|
showed_header = False
|
|
767
802
|
|
|
@@ -769,6 +804,10 @@ def _configure_subtree(
|
|
|
769
804
|
if group.group_type != "optional":
|
|
770
805
|
continue
|
|
771
806
|
for ch in group.children:
|
|
807
|
+
# A parameter, which [tool.splent.config] sets, not a feature.
|
|
808
|
+
if model.is_parameter(ch):
|
|
809
|
+
continue
|
|
810
|
+
|
|
772
811
|
# Already handled by mandatory set
|
|
773
812
|
if ch in mandatory:
|
|
774
813
|
_configure_subtree(ch, selected, excluded, model, mandatory, indent)
|
|
@@ -832,6 +871,55 @@ def _configure_subtree(
|
|
|
832
871
|
# ═══════════════════════════════════════════════════════════════════
|
|
833
872
|
|
|
834
873
|
|
|
874
|
+
def _feature_root(workspace: str, org: str, package: str) -> str | None:
|
|
875
|
+
"""The copy of a feature this workspace has: its working copy at the
|
|
876
|
+
root, else its newest release in the cache, else None."""
|
|
877
|
+
from splent_cli.services.neutral_app import cache_dir, cached_versions
|
|
878
|
+
|
|
879
|
+
root = os.path.join(workspace, package)
|
|
880
|
+
if os.path.isfile(os.path.join(root, "pyproject.toml")):
|
|
881
|
+
return root
|
|
882
|
+
namespace = normalize_namespace(org)
|
|
883
|
+
for version in cached_versions(workspace, namespace, package):
|
|
884
|
+
return str(cache_dir(workspace, namespace, package, version))
|
|
885
|
+
return None
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _parameter_selection(
|
|
889
|
+
workspace: str, data: dict, model: SPLModel, selected: set[str]
|
|
890
|
+
) -> dict[str, str]:
|
|
891
|
+
"""The parameter nodes the product's values select (P7), each with what
|
|
892
|
+
selected it, for the features in ``selected``.
|
|
893
|
+
|
|
894
|
+
The values are the product's [tool.splent.config], else each feature's
|
|
895
|
+
default, read from the copy of the feature this workspace has. The
|
|
896
|
+
configurator asks for none of them: [tool.splent.config] is where they
|
|
897
|
+
change, and product:params shows them.
|
|
898
|
+
"""
|
|
899
|
+
from splent_framework.params.schema import read_pyproject
|
|
900
|
+
|
|
901
|
+
from splent_cli.utils.uvl_params import selection
|
|
902
|
+
|
|
903
|
+
params: dict = {}
|
|
904
|
+
for name in sorted(selected):
|
|
905
|
+
feat = model.features.get(name)
|
|
906
|
+
if not feat or feat.is_abstract:
|
|
907
|
+
continue
|
|
908
|
+
root = _feature_root(workspace, feat.org, feat.package)
|
|
909
|
+
if root is None:
|
|
910
|
+
continue
|
|
911
|
+
try:
|
|
912
|
+
with open(os.path.join(root, "pyproject.toml"), "rb") as f:
|
|
913
|
+
declared = read_pyproject(feat.package, tomllib.load(f))
|
|
914
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
915
|
+
continue
|
|
916
|
+
for key, param in declared.params.items():
|
|
917
|
+
params.setdefault(key, param)
|
|
918
|
+
splent = data.get("tool", {}).get("splent", {})
|
|
919
|
+
config = splent.get("config", {}) if isinstance(splent, dict) else {}
|
|
920
|
+
return selection(model.tree(), params, config if isinstance(config, dict) else {})
|
|
921
|
+
|
|
922
|
+
|
|
835
923
|
def _read_contract_env(
|
|
836
924
|
workspace: str,
|
|
837
925
|
cache_dir: str,
|
|
@@ -939,7 +1027,7 @@ def product_configure():
|
|
|
939
1027
|
# ── Show mandatory leaf features ───────────────────────
|
|
940
1028
|
click.secho(" Mandatory (auto-selected):", fg="green")
|
|
941
1029
|
for name in sorted(mandatory):
|
|
942
|
-
if name == model.root_name:
|
|
1030
|
+
if name == model.root_name or model.is_parameter(name):
|
|
943
1031
|
continue
|
|
944
1032
|
feat = model.features[name]
|
|
945
1033
|
has_selection_group = any(
|
|
@@ -955,6 +1043,15 @@ def product_configure():
|
|
|
955
1043
|
# ── Build final selection ──────────────────────────────
|
|
956
1044
|
all_selected, _ = propagate(selected, model, mandatory, excluded)
|
|
957
1045
|
|
|
1046
|
+
# ── The parameter nodes the product's values select ────
|
|
1047
|
+
# They count as selected, as product:validate counts them, and what
|
|
1048
|
+
# they imply comes with them (a home ordered by rating needs rating).
|
|
1049
|
+
chosen = _parameter_selection(workspace, data, model, all_selected)
|
|
1050
|
+
if chosen:
|
|
1051
|
+
all_selected, _ = propagate(
|
|
1052
|
+
all_selected | set(chosen), model, mandatory, excluded
|
|
1053
|
+
)
|
|
1054
|
+
|
|
958
1055
|
# ── Summary ────────────────────────────────────────────
|
|
959
1056
|
click.echo()
|
|
960
1057
|
click.secho(" \u2500\u2500\u2500 Configuration \u2500\u2500\u2500", bold=True)
|
|
@@ -968,7 +1065,9 @@ def product_configure():
|
|
|
968
1065
|
pass # skip root display
|
|
969
1066
|
elif name in all_selected:
|
|
970
1067
|
tag = ""
|
|
971
|
-
if name in
|
|
1068
|
+
if name in chosen:
|
|
1069
|
+
tag = click.style(f" ({chosen[name]})", fg="cyan")
|
|
1070
|
+
elif name in mandatory:
|
|
972
1071
|
tag = click.style(" (mandatory)", fg="bright_black")
|
|
973
1072
|
click.echo(f"{pfx}\u2713 {name}{tag}")
|
|
974
1073
|
else:
|
|
@@ -980,6 +1079,13 @@ def product_configure():
|
|
|
980
1079
|
|
|
981
1080
|
render_tree(model.root_name)
|
|
982
1081
|
click.echo()
|
|
1082
|
+
if any(f.is_parameter for f in model.features.values()):
|
|
1083
|
+
click.secho(
|
|
1084
|
+
" The nodes of parameters follow the product's [tool.splent.config], "
|
|
1085
|
+
"or each feature's\n default. splent product:params shows the values.",
|
|
1086
|
+
fg="bright_black",
|
|
1087
|
+
)
|
|
1088
|
+
click.echo()
|
|
983
1089
|
|
|
984
1090
|
# ── Check excludes ─────────────────────────────────────
|
|
985
1091
|
violations = check_excludes(all_selected, model)
|
|
@@ -1013,6 +1119,12 @@ def product_configure():
|
|
|
1013
1119
|
if not ok:
|
|
1014
1120
|
click.secho("UNSATISFIABLE", fg="red", bold=True)
|
|
1015
1121
|
click.echo(" The selected configuration violates UVL constraints.")
|
|
1122
|
+
if any(f.is_parameter for f in model.features.values()):
|
|
1123
|
+
click.echo(
|
|
1124
|
+
" A value of a parameter may need a feature left out, or a feature "
|
|
1125
|
+
"a value\n [tool.splent.config] does not set: splent product:params "
|
|
1126
|
+
"shows the values."
|
|
1127
|
+
)
|
|
1016
1128
|
raise SystemExit(1)
|
|
1017
1129
|
|
|
1018
1130
|
click.secho("satisfiable", fg="green", bold=True)
|